1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Compare commits

...

23 Commits

Author SHA1 Message Date
purian23
80025804ab theme: Tweaks to Auto Color Mode 2026-01-24 20:43:45 -05:00
bbedward
028d3b4e61 workspaces: fix index numbers with show apps on vBar + animation 2026-01-24 20:31:45 -05:00
purian23
9cce5ccfe6 autoThemeMode: Add transition time & layout update 2026-01-24 19:33:37 -05:00
purian23
a260b8060e Merge branch 'master' into auto-theme 2026-01-24 18:19:13 -05:00
purian23
f945307232 cleanup: Auto theme switcher 2026-01-24 17:48:34 -05:00
bbedward
8f44d52cb2 launcher v2: allow categories in plugins 2026-01-24 16:58:55 -05:00
purian23
3413cb7b89 feat: Create new Auto theme mode based on region / time of day 2026-01-24 16:38:45 -05:00
bbedward
4e3b24ffbb settings: migrate vpnLastConnected to session
fixes #1488
2026-01-24 16:08:15 -05:00
bbedward
03cfa55e0b ipc: ass toast IPCs
fixes #964
2026-01-24 12:53:51 -05:00
bbedward
a887e60f40 keybinds: fix MangoWC config traversal in provider
fixes #1464
2026-01-24 12:23:59 -05:00
bbedward
816819bf9f dankinstall: fix xero color typo 2026-01-23 23:10:24 -05:00
bbedward
78f3bb3812 dankinstall: support XeroLinux
fixes #1474
2026-01-23 22:39:14 -05:00
bbedward
01d7ed5dd8 launcher v2: ability to toggle visibility in modal 2026-01-23 22:17:35 -05:00
Lucas
50311db280 nix: add qt-imageformats to DMS qml dependencies (#1479)
* nix: add qt-imageformats to DMS qml dependencies

* nix: update flake.lock
2026-01-23 21:53:35 -05:00
bbedward
01b1a276c5 launcher v2: support ScreenCopy in tiles 2026-01-23 21:29:48 -05:00
IChengHo
6d4c31492c fix: pass query string to launcher v2 during IPC toggle (#1477)
Ensure toggleQuery forwards the query parameter to the launcher v2
2026-01-23 19:43:42 -05:00
Jon Rogers
f8c5f07e9f Fix: Add view mode persistence for xdg-open picker modals (#1465)
* fix: Add browserPickerViewMode persistence to settings spec

The BrowserPickerModal (used by xdg-open feature) was not persisting
view mode selection between sessions. While the modal had code to save
the view mode preference, the browserPickerViewMode property was not
registered in SettingsSpec.js, preventing it from being saved to disk.

Added browserPickerViewMode and browserUsageHistory to SettingsSpec.js
to ensure user's view preference (list/grid) is properly persisted.

Fixes view mode reverting to grid after restarting DMS/QuickShell.

* fix: Add view mode persistence for both browser and file pickers

Extended the fix to include both picker modals used by xdg-open:

BrowserPickerModal (URLs):
- Added browserPickerViewMode and browserUsageHistory to SettingsSpec.js
- Already had save logic in BrowserPickerModal.qml

AppPickerModal/filePickerModal (files):
- Added appPickerViewMode and filePickerUsageHistory to SettingsSpec.js
- Added appPickerViewMode and filePickerUsageHistory properties to SettingsData.qml
- Added viewMode binding and onViewModeChanged handler to filePickerModal

Both modals now properly persist user's view preference (list/grid) and
usage history between sessions.

Fixes view mode reverting to default grid after restarting DMS/QuickShell
for both 'dms open https://...' and 'dms open file.pdf' workflows.
2026-01-23 19:39:13 -05:00
Ethan Todd
11e23feb0e lockscreen/greetd: add 0 in front of single digit hours for 12 hour format. greetd: add option to hide profile image (#1247)
* greetd: add lockScreenShowProfileImage option

* lockscreen/greetd: for non 24 hour formats, add 0 in front of single digit hours to ensure that everything is always centered properly - previously, it would only appear centered if on a double digit hour. also add getEffectiveTimeFormat function to GreetdSettings.

* clock: made pad 12 hour formats optional

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-23 14:47:59 -05:00
bbedward
b4ba2dac37 launcher v2: fix nvidia dgpu race condition 2026-01-23 14:15:46 -05:00
bbedward
d013c3b718 workspace: fix rename modal 2026-01-23 14:03:02 -05:00
Kamil Chmielewski
b3ea28c5c4 feat: add workspace rename dialog (#1429)
* feat: add workspace rename dialog

- Adds a modal dialog to rename the current workspace
- Supports both Niri (via IPC socket) and Hyprland (via hyprctl dispatch)
- Default keybinding: Ctrl+Shift+R to open the dialog
- Pre-fills with current workspace name
- Allows setting empty name to reset to default

* refactor: wrap WorkspaceRenameModal in LazyLoader

Reduces memory footprint when the modal is not in use.
2026-01-23 13:46:34 -05:00
bbedward
775b381987 lock: add disable media player option
fixes #1470
2026-01-23 13:34:25 -05:00
bbedward
3a41f2f1ed greeter+lock: remove random facts
fixes #1475
2026-01-23 13:25:42 -05:00
53 changed files with 3460 additions and 941 deletions

View File

@@ -6,6 +6,8 @@ This file is more of a quick reference so I know what to account for before next
- dbus API for plugins, KDEConnect
- new dank16 algorithm
- launcher actions, customize env, args, name, icon
- launcher v2 - omega stuff, GIF search, supa powerful
- dock on bar
# 1.2.0

View File

@@ -91,6 +91,9 @@ bind = SUPER CTRL, up, movetoworkspace, e-1
bind = SUPER CTRL, U, movetoworkspace, e+1
bind = SUPER CTRL, I, movetoworkspace, e-1
# === Workspace Management ===
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
# === Move Workspaces ===
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1

View File

@@ -133,6 +133,11 @@ binds {
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Workspace Management ===
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
spawn "dms" "ipc" "call" "workspace-rename" "open";
}
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }

View File

@@ -41,6 +41,9 @@ func init() {
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
}
type ArchDistribution struct {

View File

@@ -502,17 +502,17 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
p.dmsProcessed = true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
expanded, err := utils.ExpandPath(sourcePath)
if err != nil {
return
}
includedBinds, err := p.parseFileWithSource(expanded)
fullPath := expanded
if !filepath.IsAbs(expanded) {
fullPath = filepath.Join(baseDir, expanded)
}
includedBinds, err := p.parseFileWithSource(fullPath)
if err != nil {
return
}
@@ -521,33 +521,10 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
}
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
data, err := os.ReadFile(dmsBindsPath)
keybinds, err := p.parseFileWithSource(dmsBindsPath)
if err != nil {
return nil
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
var keybinds []MangoWCKeyBinding
lines := strings.Split(string(data), "\n")
for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.getKeybindAtLineContent(line, lineNum)
if kb == nil {
continue
}
kb.Source = dmsBindsPath
p.addBind(kb)
keybinds = append(keybinds, *kb)
}
p.currentSource = prevSource
p.dmsProcessed = true
return keybinds
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
@@ -44,6 +45,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
return
}
if strings.HasPrefix(req.Method, "theme.auto.") {
if themeModeManager == nil {
models.RespondError(conn, req.ID, "theme mode manager not initialized")
return
}
thememode.HandleRequest(conn, req, themeModeManager)
return
}
if strings.HasPrefix(req.Method, "loginctl.") {
if loginctlManager == nil {
models.RespondError(conn, req.ID, "loginctl manager not initialized")

View File

@@ -28,6 +28,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
@@ -68,6 +69,7 @@ var evdevManager *evdev.Manager
var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager
const dbusClientID = "dms-dbus-client"
@@ -380,6 +382,14 @@ func InitializeDbusManager() error {
return nil
}
func InitializeThemeModeManager() error {
manager := thememode.NewManager()
themeModeManager = manager
log.Info("Theme mode automation manager initialized")
return nil
}
func handleConnection(conn net.Conn) {
defer conn.Close()
@@ -457,6 +467,10 @@ func getCapabilities() Capabilities {
caps = append(caps, "clipboard")
}
if themeModeManager != nil {
caps = append(caps, "theme.auto")
}
if dbusManager != nil {
caps = append(caps, "dbus")
}
@@ -519,6 +533,10 @@ func getServerInfo() ServerInfo {
caps = append(caps, "clipboard")
}
if themeModeManager != nil {
caps = append(caps, "theme.auto")
}
if dbusManager != nil {
caps = append(caps, "dbus")
}
@@ -791,6 +809,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}()
}
if shouldSubscribe("theme.auto") && themeModeManager != nil {
wg.Add(1)
themeAutoChan := themeModeManager.Subscribe(clientID + "-theme-auto")
go func() {
defer wg.Done()
defer themeModeManager.Unsubscribe(clientID + "-theme-auto")
initialState := themeModeManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "theme.auto", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-themeAutoChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "theme.auto", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("bluetooth") && bluezManager != nil {
wg.Add(1)
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
@@ -1251,6 +1301,9 @@ func cleanupManagers() {
if dbusManager != nil {
dbusManager.Close()
}
if themeModeManager != nil {
themeModeManager.Close()
}
if wlContext != nil {
wlContext.Close()
}
@@ -1346,6 +1399,15 @@ func Start(printDocs bool) error {
log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)")
log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)")
log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)")
log.Info("Theme automation:")
log.Info(" theme.auto.getState - Get current theme automation state")
log.Info(" theme.auto.setEnabled - Enable/disable theme automation (params: enabled)")
log.Info(" theme.auto.setMode - Set automation mode (params: mode [time|location])")
log.Info(" theme.auto.setSchedule - Set time schedule (params: startHour, startMinute, endHour, endMinute)")
log.Info(" theme.auto.setLocation - Set location (params: latitude, longitude)")
log.Info(" theme.auto.setUseIPLocation - Use IP location (params: use)")
log.Info(" theme.auto.trigger - Trigger immediate re-evaluation")
log.Info(" theme.auto.subscribe - Subscribe to theme automation state changes (streaming)")
log.Info("Bluetooth:")
log.Info(" bluetooth.getState - Get current bluetooth state")
log.Info(" bluetooth.startDiscovery - Start device discovery")
@@ -1503,6 +1565,12 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err)
}
if err := InitializeThemeModeManager(); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil {
go func() {

View File

@@ -0,0 +1,154 @@
package thememode
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "theme mode manager not initialized")
return
}
switch req.Method {
case "theme.auto.getState":
handleGetState(conn, req, manager)
case "theme.auto.setEnabled":
handleSetEnabled(conn, req, manager)
case "theme.auto.setMode":
handleSetMode(conn, req, manager)
case "theme.auto.setSchedule":
handleSetSchedule(conn, req, manager)
case "theme.auto.setLocation":
handleSetLocation(conn, req, manager)
case "theme.auto.setUseIPLocation":
handleSetUseIPLocation(conn, req, manager)
case "theme.auto.trigger":
handleTrigger(conn, req, manager)
case "theme.auto.subscribe":
handleSubscribe(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
enabled, err := params.Bool(req.Params, "enabled")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetEnabled(enabled)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto enabled set"})
}
func handleSetMode(conn net.Conn, req models.Request, manager *Manager) {
mode, err := params.String(req.Params, "mode")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if mode != "time" && mode != "location" {
models.RespondError(conn, req.ID, "invalid mode")
return
}
manager.SetMode(mode)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto mode set"})
}
func handleSetSchedule(conn net.Conn, req models.Request, manager *Manager) {
startHour, err := params.Int(req.Params, "startHour")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
startMinute, err := params.Int(req.Params, "startMinute")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
endHour, err := params.Int(req.Params, "endHour")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
endMinute, err := params.Int(req.Params, "endMinute")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := manager.ValidateSchedule(startHour, startMinute, endHour, endMinute); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetSchedule(startHour, startMinute, endHour, endMinute)
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
lat, err := params.Float(req.Params, "latitude")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
lon, err := params.Float(req.Params, "longitude")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetLocation(lat, lon)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto location set"})
}
func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
use, err := params.Bool(req.Params, "use")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
manager.SetUseIPLocation(use)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto IP location set"})
}
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
manager.TriggerUpdate()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto update triggered"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
return
}
for state := range stateChan {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
Result: &state,
}); err != nil {
return
}
}
}

View File

@@ -0,0 +1,432 @@
package thememode
import (
"errors"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const (
defaultStartHour = 18
defaultStartMinute = 0
defaultEndHour = 6
defaultEndMinute = 0
defaultElevationTwilight = -6.0
defaultElevationDaylight = 3.0
)
type Manager struct {
config Config
configMutex sync.RWMutex
state *State
stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan State]
locationMutex sync.RWMutex
cachedIPLat *float64
cachedIPLon *float64
stopChan chan struct{}
updateTrigger chan struct{}
wg sync.WaitGroup
}
func NewManager() *Manager {
m := &Manager{
config: Config{
Enabled: false,
Mode: "time",
StartHour: defaultStartHour,
StartMinute: defaultStartMinute,
EndHour: defaultEndHour,
EndMinute: defaultEndMinute,
ElevationTwilight: defaultElevationTwilight,
ElevationDaylight: defaultElevationDaylight,
},
stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1),
}
m.updateState(time.Now())
m.wg.Add(1)
go m.schedulerLoop()
return m
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{Config: m.getConfig()}
}
stateCopy := *m.state
return stateCopy
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) SetEnabled(enabled bool) {
m.configMutex.Lock()
if m.config.Enabled == enabled {
m.configMutex.Unlock()
return
}
m.config.Enabled = enabled
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetMode(mode string) {
m.configMutex.Lock()
if m.config.Mode == mode {
m.configMutex.Unlock()
return
}
m.config.Mode = mode
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetSchedule(startHour, startMinute, endHour, endMinute int) {
m.configMutex.Lock()
changed := m.config.StartHour != startHour ||
m.config.StartMinute != startMinute ||
m.config.EndHour != endHour ||
m.config.EndMinute != endMinute
if !changed {
m.configMutex.Unlock()
return
}
m.config.StartHour = startHour
m.config.StartMinute = startMinute
m.config.EndHour = endHour
m.config.EndMinute = endMinute
m.configMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetLocation(lat, lon float64) {
m.configMutex.Lock()
if m.config.Latitude != nil && m.config.Longitude != nil &&
*m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
m.configMutex.Unlock()
return
}
m.config.Latitude = &lat
m.config.Longitude = &lon
m.config.UseIPLocation = false
m.configMutex.Unlock()
m.locationMutex.Lock()
m.cachedIPLat = nil
m.cachedIPLon = nil
m.locationMutex.Unlock()
m.TriggerUpdate()
}
func (m *Manager) SetUseIPLocation(use bool) {
m.configMutex.Lock()
if m.config.UseIPLocation == use {
m.configMutex.Unlock()
return
}
m.config.UseIPLocation = use
if use {
m.config.Latitude = nil
m.config.Longitude = nil
}
m.configMutex.Unlock()
if use {
m.locationMutex.Lock()
m.cachedIPLat = nil
m.cachedIPLon = nil
m.locationMutex.Unlock()
}
m.TriggerUpdate()
}
func (m *Manager) TriggerUpdate() {
select {
case m.updateTrigger <- struct{}{}:
default:
}
}
func (m *Manager) Close() {
select {
case <-m.stopChan:
return
default:
close(m.stopChan)
}
m.wg.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
}
func (m *Manager) schedulerLoop() {
defer m.wg.Done()
var timer *time.Timer
for {
config := m.getConfig()
now := time.Now()
var isLight bool
var next time.Time
if config.Enabled {
isLight, next = m.computeSchedule(now, config)
} else {
m.stateMutex.RLock()
if m.state != nil {
isLight = m.state.IsLight
}
m.stateMutex.RUnlock()
next = now.Add(24 * time.Hour)
}
m.updateStateWithValues(config, isLight, next)
waitDur := time.Until(next)
if !config.Enabled {
waitDur = 24 * time.Hour
}
if waitDur < time.Second {
waitDur = time.Second
}
if timer != nil {
timer.Stop()
}
timer = time.NewTimer(waitDur)
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.updateTrigger:
timer.Stop()
continue
case <-timer.C:
continue
}
}
}
func (m *Manager) updateState(now time.Time) {
config := m.getConfig()
var isLight bool
var next time.Time
if config.Enabled {
isLight, next = m.computeSchedule(now, config)
} else {
m.stateMutex.RLock()
if m.state != nil {
isLight = m.state.IsLight
}
m.stateMutex.RUnlock()
next = now.Add(24 * time.Hour)
}
m.updateStateWithValues(config, isLight, next)
}
func (m *Manager) updateStateWithValues(config Config, isLight bool, next time.Time) {
newState := State{
Config: config,
IsLight: isLight,
NextTransition: next,
}
m.stateMutex.Lock()
if m.state != nil && statesEqual(m.state, &newState) {
m.stateMutex.Unlock()
return
}
m.state = &newState
m.stateMutex.Unlock()
m.notifySubscribers()
}
func (m *Manager) notifySubscribers() {
state := m.GetState()
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- state:
default:
}
return true
})
}
func (m *Manager) getConfig() Config {
m.configMutex.RLock()
defer m.configMutex.RUnlock()
return m.config
}
func (m *Manager) getLocation(config Config) (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude
}
if !config.UseIPLocation {
return nil, nil
}
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
m.locationMutex.RUnlock()
return lat, lon
}
m.locationMutex.RUnlock()
lat, lon, err := wayland.FetchIPLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = lat
m.cachedIPLon = lon
m.locationMutex.Unlock()
return lat, lon
}
func statesEqual(a, b *State) bool {
if a == nil || b == nil {
return a == b
}
if a.IsLight != b.IsLight || !a.NextTransition.Equal(b.NextTransition) {
return false
}
return a.Config == b.Config
}
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
if config.Mode == "location" {
return m.computeLocationSchedule(now, config)
}
return computeTimeSchedule(now, config)
}
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
startMinutes := config.StartHour*60 + config.StartMinute
endMinutes := config.EndHour*60 + config.EndMinute
currentMinutes := now.Hour()*60 + now.Minute()
startTime := time.Date(now.Year(), now.Month(), now.Day(), config.StartHour, config.StartMinute, 0, 0, now.Location())
endTime := time.Date(now.Year(), now.Month(), now.Day(), config.EndHour, config.EndMinute, 0, 0, now.Location())
if startMinutes == endMinutes {
next := startTime
if !next.After(now) {
next = next.Add(24 * time.Hour)
}
return true, next
}
if startMinutes < endMinutes {
if currentMinutes < startMinutes {
return true, startTime
}
if currentMinutes >= endMinutes {
return true, startTime.Add(24 * time.Hour)
}
return false, endTime
}
if currentMinutes >= startMinutes {
return false, endTime.Add(24 * time.Hour)
}
if currentMinutes < endMinutes {
return false, endTime
}
return true, startTime
}
func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, time.Time) {
lat, lon := m.getLocation(config)
if lat == nil || lon == nil {
currentIsLight := false
m.stateMutex.RLock()
if m.state != nil {
currentIsLight = m.state.IsLight
}
m.stateMutex.RUnlock()
return currentIsLight, now.Add(10 * time.Minute)
}
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
if cond != wayland.SunNormal {
if cond == wayland.SunMidnightSun {
return true, startOfNextDay(now)
}
return false, startOfNextDay(now)
}
if now.Before(times.Sunrise) {
return false, times.Sunrise
}
if now.Before(times.Sunset) {
return true, times.Sunset
}
nextDay := startOfNextDay(now)
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
if nextCond != wayland.SunNormal {
if nextCond == wayland.SunMidnightSun {
return true, startOfNextDay(nextDay)
}
return false, startOfNextDay(nextDay)
}
return false, nextTimes.Sunrise
}
func startOfNextDay(t time.Time) time.Time {
next := t.Add(24 * time.Hour)
return time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
}
func validateHourMinute(hour, minute int) bool {
if hour < 0 || hour > 23 {
return false
}
if minute < 0 || minute > 59 {
return false
}
return true
}
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
if !validateHourMinute(startHour, startMinute) || !validateHourMinute(endHour, endMinute) {
return errInvalidTime
}
return nil
}
var errInvalidTime = errors.New("invalid schedule time")

View File

@@ -0,0 +1,23 @@
package thememode
import "time"
type Config struct {
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
StartHour int `json:"startHour"`
StartMinute int `json:"startMinute"`
EndHour int `json:"endHour"`
EndMinute int `json:"endMinute"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
UseIPLocation bool `json:"useIPLocation"`
ElevationTwilight float64 `json:"elevationTwilight"`
ElevationDaylight float64 `json:"elevationDaylight"`
}
type State struct {
Config Config `json:"config"`
IsLight bool `json:"isLight"`
NextTransition time.Time `json:"nextTransition"`
}

View File

@@ -626,6 +626,7 @@ func (m *Manager) schedulerLoop() {
m.schedule.calcDay = time.Time{}
m.scheduleMutex.Unlock()
m.recalcSchedule(time.Now())
m.updateStateFromSchedule()
m.configMutex.RLock()
enabled := m.config.Enabled
m.configMutex.RUnlock()

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1766651565,
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
"lastModified": 1769018530,
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
"type": "github"
},
"original": {

View File

@@ -47,6 +47,7 @@
kirigami.unwrapped
sonnet
qtmultimedia
qtimageformats
];
in
{

View File

@@ -1,53 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property var facts: [
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
"There's a nebula out there that's actually colder than empty space itself.",
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
"Distant galaxies can move away from us faster than light because space itself is stretching.",
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
"A day on Venus lasts longer than its entire year around the Sun.",
"On Mercury, the time between sunrises is 176 Earth days long.",
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
"Counting to a billion at one number per second would take over 31 years.",
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
"Only around 5% of galaxies are ever reachable—even at light-speed.",
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
"The Moon moves 3.8 centimeters farther from Earth every year.",
"The universe creates 275 million new stars every single day.",
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
]
function getRandomFact() {
return facts[Math.floor(Math.random() * facts.length)]
}
}

View File

@@ -100,7 +100,8 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call hypr openOverview", label: "Hyprland: Open Overview", compositor: "hyprland" },
{ id: "spawn dms ipc call hypr closeOverview", label: "Hyprland: Close Overview", compositor: "hyprland" },
{ id: "spawn dms ipc call wallpaper next", label: "Wallpaper: Next" },
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" }
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" },
{ id: "spawn dms ipc call workspace-rename open", label: "Workspace: Rename" }
];
const NIRI_ACTIONS = {

View File

@@ -13,7 +13,7 @@ import "settings/SessionStore.js" as Store
Singleton {
id: root
readonly property int sessionConfigVersion: 2
readonly property int sessionConfigVersion: 3
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
property bool _parseError: false
@@ -82,6 +82,15 @@ Singleton {
property bool nightModeUseIPLocation: false
property string nightModeLocationProvider: ""
property bool themeModeAutoEnabled: false
property string themeModeAutoMode: "time"
property int themeModeStartHour: 18
property int themeModeStartMinute: 0
property int themeModeEndHour: 6
property int themeModeEndMinute: 0
property bool themeModeShareGammaSettings: true
property string themeModeNextTransition: ""
property var pinnedApps: []
property var barPinnedApps: []
property int dockLauncherPosition: 0
@@ -109,6 +118,8 @@ Singleton {
property var appOverrides: ({})
property bool searchAppActions: true
property string vpnLastConnected: ""
Component.onCompleted: {
if (!isGreeterMode) {
loadSettings();
@@ -172,7 +183,7 @@ Singleton {
} catch (e) {
_parseError = true;
const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
}
}
@@ -186,14 +197,10 @@ Singleton {
_isReadOnly = !writable;
if (_isReadOnly) {
_hasUnsavedChanges = _checkForUnsavedChanges();
if (!wasReadOnly)
console.info("SessionData: session.json is now read-only");
} else {
_loadedSessionSnapshot = getCurrentSessionJson();
_hasUnsavedChanges = false;
if (wasReadOnly)
console.info("SessionData: session.json is now writable");
if (_pendingMigration)
if (wasReadOnly && _pendingMigration)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
}
_pendingMigration = null;
@@ -255,7 +262,7 @@ Singleton {
} catch (e) {
_parseError = true;
const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
}
}
@@ -273,7 +280,6 @@ Singleton {
}
function migrateFromUndefinedToV1(settings) {
console.info("SessionData: Migrating configuration from undefined to version 1");
if (typeof SettingsData !== "undefined") {
if (settings.acMonitorTimeout !== undefined) {
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout);
@@ -448,7 +454,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -545,7 +551,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -583,7 +589,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -621,7 +627,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -659,7 +665,7 @@ Singleton {
}
if (!screen) {
console.warn("SessionData: Screen not found:", screenName);
console.warn("SessionData: Screen not found");
return;
}
@@ -702,7 +708,6 @@ Singleton {
}
function setNightModeAutoEnabled(enabled) {
console.log("SessionData: Setting nightModeAutoEnabled to", enabled);
nightModeAutoEnabled = enabled;
saveSettings();
}
@@ -738,13 +743,11 @@ Singleton {
}
function setLatitude(lat) {
console.log("SessionData: Setting latitude to", lat);
latitude = lat;
saveSettings();
}
function setLongitude(lng) {
console.log("SessionData: Setting longitude to", lng);
longitude = lng;
saveSettings();
}
@@ -754,6 +757,41 @@ Singleton {
saveSettings();
}
function setThemeModeAutoEnabled(enabled) {
themeModeAutoEnabled = enabled;
saveSettings();
}
function setThemeModeAutoMode(mode) {
themeModeAutoMode = mode;
saveSettings();
}
function setThemeModeStartHour(hour) {
themeModeStartHour = hour;
saveSettings();
}
function setThemeModeStartMinute(minute) {
themeModeStartMinute = minute;
saveSettings();
}
function setThemeModeEndHour(hour) {
themeModeEndHour = hour;
saveSettings();
}
function setThemeModeEndMinute(minute) {
themeModeEndMinute = minute;
saveSettings();
}
function setThemeModeShareGammaSettings(share) {
themeModeShareGammaSettings = share;
saveSettings();
}
function setPinnedApps(apps) {
pinnedApps = apps;
saveSettings();
@@ -1003,6 +1041,11 @@ Singleton {
saveSettings();
}
function setVpnLastConnected(uuid) {
vpnLastConnected = uuid || "";
saveSettings();
}
function syncWallpaperForCurrentMode() {
if (!perModeWallpaper)
return;

View File

@@ -146,6 +146,7 @@ Singleton {
property bool use24HourClock: true
property bool showSeconds: false
property bool padHours12Hour: false
property bool useFahrenheit: false
property string windSpeedUnit: "kmh"
property bool nightModeEnabled: false
@@ -273,6 +274,8 @@ Singleton {
property string spotlightModalViewMode: "list"
property string browserPickerViewMode: "grid"
property var browserUsageHistory: ({})
property string appPickerViewMode: "grid"
property var filePickerUsageHistory: ({})
property bool sortAppsAlphabetically: false
property int appLauncherGridColumns: 4
property bool spotlightCloseNiriOverview: true
@@ -289,13 +292,13 @@ Singleton {
property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
property string _legacyVpnLastConnected: ""
readonly property string weatherLocation: SessionData.weatherLocation
readonly property string weatherCoordinates: SessionData.weatherCoordinates
property bool useAutoLocation: false
property bool weatherEnabled: true
property string networkPreference: "auto"
property string vpnLastConnected: ""
property string iconTheme: "System Default"
property var availableIconThemes: ["System Default"]
@@ -452,6 +455,7 @@ Singleton {
property bool lockScreenShowDate: true
property bool lockScreenShowProfileImage: true
property bool lockScreenShowPasswordField: true
property bool lockScreenShowMediaPlayer: true
property bool lockScreenPowerOffMonitorsOnLock: false
property bool enableFprint: false
@@ -1074,6 +1078,11 @@ Singleton {
_legacyWeatherLocation = obj.weatherLocation;
if (obj?.weatherCoordinates !== undefined)
_legacyWeatherCoordinates = obj.weatherCoordinates;
if (obj?.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
_legacyVpnLastConnected = obj.vpnLastConnected;
SessionData.vpnLastConnected = _legacyVpnLastConnected;
SessionData.saveSettings();
}
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasLoaded = true;
@@ -1251,11 +1260,11 @@ Singleton {
}
function getEffectiveTimeFormat() {
if (use24HourClock) {
if (use24HourClock)
return showSeconds ? "hh:mm:ss" : "hh:mm";
} else {
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
}
if (padHours12Hour)
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
}
function getEffectiveClockDateFormat() {
@@ -2307,6 +2316,11 @@ Singleton {
_legacyWeatherLocation = obj.weatherLocation;
if (obj.weatherCoordinates !== undefined)
_legacyWeatherCoordinates = obj.weatherCoordinates;
if (obj.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
_legacyVpnLastConnected = obj.vpnLastConnected;
SessionData.vpnLastConnected = _legacyVpnLastConnected;
SessionData.saveSettings();
}
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasLoaded = true;

View File

@@ -94,6 +94,9 @@ Singleton {
property var matugenColors: ({})
property var _pendingGenerateParams: null
property bool themeModeAutomationActive: false
property bool dmsServiceWasDisconnected: true
readonly property var dank16: {
const raw = matugenColors?.dank16;
if (!raw)
@@ -176,6 +179,237 @@ Singleton {
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
switchTheme(SettingsData.currentThemeName, false, false);
}
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
startThemeModeAutomation();
}
}
Connections {
target: SessionData
enabled: typeof SessionData !== "undefined"
function onThemeModeAutoEnabledChanged() {
if (SessionData.themeModeAutoEnabled) {
root.startThemeModeAutomation();
} else {
root.stopThemeModeAutomation();
}
}
function onThemeModeAutoModeChanged() {
if (root.themeModeAutomationActive) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
}
}
function onThemeModeStartHourChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeStartMinuteChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeEndHourChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeEndMinuteChanged() {
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onThemeModeShareGammaSettingsChanged() {
if (root.themeModeAutomationActive) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
}
}
function onNightModeStartHourChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeStartMinuteChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeEndHourChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onNightModeEndMinuteChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
root.evaluateThemeMode();
root.syncTimeThemeSchedule();
}
}
function onLatitudeChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (!SessionData.nightModeUseIPLocation &&
SessionData.latitude !== 0.0 &&
SessionData.longitude !== 0.0 &&
typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
function onLongitudeChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (!SessionData.nightModeUseIPLocation &&
SessionData.latitude !== 0.0 &&
SessionData.longitude !== 0.0 &&
typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
function onNightModeUseIPLocationChanged() {
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
if (typeof DMSService !== "undefined") {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": SessionData.nightModeUseIPLocation
}, response => {
if (!response.error && !SessionData.nightModeUseIPLocation &&
SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
});
}
root.evaluateThemeMode();
root.syncLocationThemeSchedule();
}
}
}
// React to gamma backend's isDay state changes for location-based mode
Connections {
target: DisplayService
enabled: typeof DisplayService !== "undefined" &&
typeof SessionData !== "undefined" &&
SessionData.themeModeAutoEnabled &&
SessionData.themeModeAutoMode === "location" &&
!themeAutoBackendAvailable()
function onGammaIsDayChanged() {
if (root.isLightMode !== DisplayService.gammaIsDay) {
root.setLightMode(DisplayService.gammaIsDay, true, true);
}
}
}
Connections {
target: DMSService
enabled: typeof DMSService !== "undefined" && typeof SessionData !== "undefined"
function onLoginctlEvent(event) {
if (!SessionData.themeModeAutoEnabled) return;
if (event.event === "unlock" || event.event === "resume") {
if (!themeAutoBackendAvailable()) {
root.evaluateThemeMode();
return;
}
DMSService.sendRequest("theme.auto.trigger", {});
}
}
function onThemeAutoStateUpdate(data) {
if (!SessionData.themeModeAutoEnabled) {
return;
}
applyThemeAutoState(data);
}
function onConnectionStateChanged() {
if (DMSService.isConnected && SessionData.themeModeAutoMode === "time") {
root.syncTimeThemeSchedule();
}
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
root.syncLocationThemeSchedule();
}
if (themeAutoBackendAvailable() && SessionData.themeModeAutoEnabled) {
DMSService.sendRequest("theme.auto.getState", null, response => {
if (response && response.result) {
applyThemeAutoState(response.result);
}
});
}
if (!SessionData.themeModeAutoEnabled) {
return;
}
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": true
}, response => {
if (!response.error) {
console.info("Theme automation: IP location enabled after connection");
}
});
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false
}, response => {
if (!response.error) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
}, locationResponse => {
if (locationResponse?.error) {
console.warn("Theme automation: Failed to set location", locationResponse.error);
}
});
}
});
} else {
console.warn("Theme automation: No location configured");
}
}
}
}
function applyGreeterTheme(themeName) {
@@ -491,7 +725,9 @@ Singleton {
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
function screenTransition() {
CompositorService.isNiri && NiriService.doScreenTransition();
if (CompositorService.isNiri) {
NiriService.doScreenTransition();
}
}
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
@@ -543,8 +779,10 @@ Singleton {
}
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
SessionData.setLightMode(light);
}
if (!isGreeterMode) {
// Skip with matugen because, our script runner will do it.
if (!matugenAvailable) {
@@ -552,6 +790,7 @@ Singleton {
}
generateSystemThemesFromCurrentTheme();
}
}
function toggleLightMode(savePrefs = true) {
@@ -1233,7 +1472,7 @@ Singleton {
return `#${invR}${invG}${invB}`;
}
property string baseLogoColor: {
property var baseLogoColor: {
if (typeof SettingsData === "undefined")
return "";
const colorOverride = SettingsData.launcherLogoColorOverride;
@@ -1246,7 +1485,7 @@ Singleton {
return colorOverride;
}
property string effectiveLogoColor: {
property var effectiveLogoColor: {
if (typeof SettingsData === "undefined")
return "";
@@ -1453,4 +1692,297 @@ Singleton {
root.switchTheme(defaultTheme, true, false);
}
}
// Theme mode automation functions
function themeAutoBackendAvailable() {
return typeof DMSService !== "undefined" &&
DMSService.isConnected &&
Array.isArray(DMSService.capabilities) &&
DMSService.capabilities.includes("theme.auto");
}
function applyThemeAutoState(state) {
if (!state) {
return;
}
if (state.config && state.config.mode && state.config.mode !== SessionData.themeModeAutoMode) {
return;
}
if (typeof SessionData !== "undefined" && state.nextTransition !== undefined) {
SessionData.themeModeNextTransition = state.nextTransition || "";
}
if (state.isLight !== undefined && root.isLightMode !== state.isLight) {
root.setLightMode(state.isLight, true, true);
}
}
function syncTimeThemeSchedule() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return;
}
if (!DMSService.isConnected) {
return;
}
const timeModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "time";
if (!timeModeActive) {
return;
}
DMSService.sendRequest("theme.auto.setMode", {"mode": "time"});
const shareSettings = SessionData.themeModeShareGammaSettings;
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
DMSService.sendRequest("theme.auto.setSchedule", {
"startHour": startHour,
"startMinute": startMinute,
"endHour": endHour,
"endMinute": endMinute
}, response => {
if (response && response.error) {
console.error("Theme automation: Failed to sync time schedule:", response.error);
}
});
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": true});
DMSService.sendRequest("theme.auto.trigger", {});
}
function syncLocationThemeSchedule() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return;
}
if (!DMSService.isConnected) {
return;
}
const locationModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location";
if (!locationModeActive) {
return;
}
DMSService.sendRequest("theme.auto.setMode", {"mode": "location"});
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("theme.auto.setUseIPLocation", {"use": true});
} else {
DMSService.sendRequest("theme.auto.setUseIPLocation", {"use": false});
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("theme.auto.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
});
}
}
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": true});
DMSService.sendRequest("theme.auto.trigger", {});
}
function evaluateThemeMode() {
if (typeof SessionData === "undefined" || !SessionData.themeModeAutoEnabled) {
return;
}
if (themeAutoBackendAvailable()) {
DMSService.sendRequest("theme.auto.getState", null, response => {
if (response && response.result) {
applyThemeAutoState(response.result);
}
});
return;
}
const mode = SessionData.themeModeAutoMode;
if (mode === "location") {
evaluateLocationBasedThemeMode();
} else {
evaluateTimeBasedThemeMode();
}
}
function evaluateLocationBasedThemeMode() {
if (typeof DisplayService !== "undefined") {
const shouldBeLight = DisplayService.gammaIsDay;
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
return;
}
if (!SessionData.nightModeUseIPLocation &&
SessionData.latitude !== 0.0 &&
SessionData.longitude !== 0.0) {
const shouldBeLight = calculateIsDaytime(
SessionData.latitude,
SessionData.longitude
);
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
return;
}
if (root.themeModeAutomationActive) {
if (SessionData.nightModeUseIPLocation) {
console.warn("Theme automation: Waiting for IP location from backend");
} else {
console.warn("Theme automation: Location mode requires coordinates");
}
}
}
function evaluateTimeBasedThemeMode() {
const shareSettings = SessionData.themeModeShareGammaSettings;
const startHour = shareSettings ?
SessionData.nightModeStartHour : SessionData.themeModeStartHour;
const startMinute = shareSettings ?
SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
const endHour = shareSettings ?
SessionData.nightModeEndHour : SessionData.themeModeEndHour;
const endMinute = shareSettings ?
SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const startMinutes = startHour * 60 + startMinute;
const endMinutes = endHour * 60 + endMinute;
let shouldBeLight;
if (startMinutes < endMinutes) {
shouldBeLight = currentMinutes < startMinutes || currentMinutes >= endMinutes;
} else {
shouldBeLight = currentMinutes >= endMinutes && currentMinutes < startMinutes;
}
if (root.isLightMode !== shouldBeLight) {
root.setLightMode(shouldBeLight, true, true);
}
}
function calculateIsDaytime(lat, lng) {
const now = new Date();
const start = new Date(now.getFullYear(), 0, 0);
const diff = now - start;
const dayOfYear = Math.floor(diff / 86400000);
const latRad = lat * Math.PI / 180;
const declination = 23.45 * Math.sin((360/365) * (dayOfYear - 81) * Math.PI / 180);
const declinationRad = declination * Math.PI / 180;
const cosHourAngle = -Math.tan(latRad) * Math.tan(declinationRad);
if (cosHourAngle > 1) {
return false; // Polar night
}
if (cosHourAngle < -1) {
return true; // Midnight sun
}
const hourAngle = Math.acos(cosHourAngle);
const hourAngleDeg = hourAngle * 180 / Math.PI;
const sunriseHour = 12 - hourAngleDeg / 15;
const sunsetHour = 12 + hourAngleDeg / 15;
const timeZoneOffset = now.getTimezoneOffset() / 60;
const localSunrise = sunriseHour - lng / 15 - timeZoneOffset;
const localSunset = sunsetHour - lng / 15 - timeZoneOffset;
const currentHour = now.getHours() + now.getMinutes() / 60;
const normalizeSunrise = ((localSunrise % 24) + 24) % 24;
const normalizeSunset = ((localSunset % 24) + 24) % 24;
return currentHour >= normalizeSunrise && currentHour < normalizeSunset;
}
// Helper function to send location to backend
function sendLocationToBackend() {
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
return false;
}
if (!DMSService.isConnected) {
return false;
}
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {"use": true}, response => {
if (response?.error) {
console.warn("Theme automation: Failed to enable IP location", response.error);
}
});
return true;
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {"use": false}, response => {
if (!response.error) {
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
}, locResp => {
if (locResp?.error) {
console.warn("Theme automation: Failed to set location", locResp.error);
}
});
}
});
return true;
}
return false;
}
Timer {
id: locationRetryTimer
interval: 1000
repeat: true
running: false
property int retryCount: 0
onTriggered: {
if (root.sendLocationToBackend()) {
stop();
retryCount = 0;
root.evaluateThemeMode();
} else {
retryCount++;
if (retryCount >= 10) {
stop();
retryCount = 0;
}
}
}
}
function startThemeModeAutomation() {
root.themeModeAutomationActive = true;
root.syncTimeThemeSchedule();
root.syncLocationThemeSchedule();
const sent = root.sendLocationToBackend();
if (!sent && typeof SessionData !== "undefined" && SessionData.themeModeAutoMode === "location") {
locationRetryTimer.start();
} else {
root.evaluateThemeMode();
}
}
function stopThemeModeAutomation() {
root.themeModeAutomationActive = false;
if (typeof DMSService !== "undefined" && DMSService.isConnected) {
DMSService.sendRequest("theme.auto.setEnabled", {"enabled": false});
}
}
}

View File

@@ -35,6 +35,14 @@ var SPEC = {
nightModeUseIPLocation: { def: false },
nightModeLocationProvider: { def: "" },
themeModeAutoEnabled: { def: false },
themeModeAutoMode: { def: "time" },
themeModeStartHour: { def: 18 },
themeModeStartMinute: { def: 0 },
themeModeEndHour: { def: 6 },
themeModeEndMinute: { def: 0 },
themeModeShareGammaSettings: { def: true },
weatherLocation: { def: "New York, NY" },
weatherCoordinates: { def: "40.7128,-74.0060" },
@@ -61,7 +69,9 @@ var SPEC = {
hiddenApps: { def: [] },
appOverrides: { def: {} },
searchAppActions: { def: true }
searchAppActions: { def: true },
vpnLastConnected: { def: "" }
};
function getValidKeys() {

View File

@@ -1,6 +1,6 @@
.pragma library
.import "./SessionSpec.js" as SpecModule
.import "./SessionSpec.js" as SpecModule
function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC;
@@ -68,6 +68,11 @@ function migrateToVersion(obj, targetVersion, settingsData) {
session.configVersion = 2;
}
if (currentVersion < 3) {
console.info("SessionData: Migrating session to version 3");
session.configVersion = 3;
}
return session;
}

View File

@@ -32,6 +32,7 @@ var SPEC = {
use24HourClock: { def: true },
showSeconds: { def: false },
padHours12Hour: { def: false },
useFahrenheit: { def: false },
windSpeedUnit: { def: "kmh" },
nightModeEnabled: { def: false },
@@ -78,16 +79,18 @@ var SPEC = {
privacyShowCameraIcon: { def: false },
privacyShowScreenShareIcon: { def: false },
controlCenterWidgets: { def: [
{ id: "volumeSlider", enabled: true, width: 50 },
{ id: "brightnessSlider", enabled: true, width: 50 },
{ id: "wifi", enabled: true, width: 50 },
{ id: "bluetooth", enabled: true, width: 50 },
{ id: "audioOutput", enabled: true, width: 50 },
{ id: "audioInput", enabled: true, width: 50 },
{ id: "nightMode", enabled: true, width: 50 },
{ id: "darkMode", enabled: true, width: 50 }
]},
controlCenterWidgets: {
def: [
{ id: "volumeSlider", enabled: true, width: 50 },
{ id: "brightnessSlider", enabled: true, width: 50 },
{ id: "wifi", enabled: true, width: 50 },
{ id: "bluetooth", enabled: true, width: 50 },
{ id: "audioOutput", enabled: true, width: 50 },
{ id: "audioInput", enabled: true, width: 50 },
{ id: "nightMode", enabled: true, width: 50 },
{ id: "darkMode", enabled: true, width: 50 }
]
},
showWorkspaceIndex: { def: false },
showWorkspaceName: { def: false },
@@ -118,13 +121,15 @@ var SPEC = {
keyboardLayoutNameCompactMode: { def: false },
runningAppsCurrentWorkspace: { def: false },
runningAppsGroupByApp: { def: false },
appIdSubstitutions: { def: [
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
]},
appIdSubstitutions: {
def: [
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
]
},
centeringMode: { def: "index" },
clockDateFormat: { def: "" },
lockDateFormat: { def: "" },
@@ -132,6 +137,10 @@ var SPEC = {
appLauncherViewMode: { def: "list" },
spotlightModalViewMode: { def: "list" },
browserPickerViewMode: { def: "grid" },
browserUsageHistory: { def: {} },
appPickerViewMode: { def: "grid" },
filePickerUsageHistory: { def: {} },
sortAppsAlphabetically: { def: false },
appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true },
@@ -148,7 +157,6 @@ var SPEC = {
weatherEnabled: { def: true },
networkPreference: { def: "auto" },
vpnLastConnected: { def: "" },
iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" },
availableIconThemes: { def: ["System Default"], persist: false },
@@ -276,6 +284,7 @@ var SPEC = {
lockScreenShowDate: { def: true },
lockScreenShowProfileImage: { def: true },
lockScreenShowPasswordField: { def: true },
lockScreenShowMediaPlayer: { def: true },
lockScreenPowerOffMonitorsOnLock: { def: false },
enableFprint: { def: false },
maxFprintTries: { def: 15 },
@@ -300,7 +309,7 @@ var SPEC = {
osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 },
osdVolumeEnabled: { def: true },
osdMediaVolumeEnabled : { def: true },
osdMediaVolumeEnabled: { def: true },
osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true },
@@ -331,52 +340,54 @@ var SPEC = {
niriOutputSettings: { def: {} },
hyprlandOutputSettings: { def: {} },
barConfigs: { def: [{
id: "default",
name: "Main Bar",
enabled: true,
position: 0,
screenPreferences: ["all"],
showOnLastDisplay: true,
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
centerWidgets: ["music", "clock", "weather"],
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
spacing: 4,
innerPadding: 4,
bottomGap: 0,
transparency: 1.0,
widgetTransparency: 1.0,
squareCorners: false,
noBackground: false,
gothCornersEnabled: false,
gothCornerRadiusOverride: false,
gothCornerRadiusValue: 12,
borderEnabled: false,
borderColor: "surfaceText",
borderOpacity: 1.0,
borderThickness: 1,
widgetOutlineEnabled: false,
widgetOutlineColor: "primary",
widgetOutlineOpacity: 1.0,
widgetOutlineThickness: 1,
fontScale: 1.0,
autoHide: false,
autoHideDelay: 250,
showOnWindowsOpen: false,
openOnOverview: false,
visible: true,
popupGapsAuto: true,
popupGapsManual: 4,
maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace",
shadowIntensity: 0,
shadowOpacity: 60,
shadowColorMode: "text",
shadowCustomColor: "#000000",
clickThrough: false
}], onChange: "updateBarConfigs" },
barConfigs: {
def: [{
id: "default",
name: "Main Bar",
enabled: true,
position: 0,
screenPreferences: ["all"],
showOnLastDisplay: true,
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
centerWidgets: ["music", "clock", "weather"],
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
spacing: 4,
innerPadding: 4,
bottomGap: 0,
transparency: 1.0,
widgetTransparency: 1.0,
squareCorners: false,
noBackground: false,
gothCornersEnabled: false,
gothCornerRadiusOverride: false,
gothCornerRadiusValue: 12,
borderEnabled: false,
borderColor: "surfaceText",
borderOpacity: 1.0,
borderThickness: 1,
widgetOutlineEnabled: false,
widgetOutlineColor: "primary",
widgetOutlineOpacity: 1.0,
widgetOutlineThickness: 1,
fontScale: 1.0,
autoHide: false,
autoHideDelay: 250,
showOnWindowsOpen: false,
openOnOverview: false,
visible: true,
popupGapsAuto: true,
popupGapsManual: 4,
maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace",
shadowIntensity: 0,
shadowOpacity: 60,
shadowColorMode: "text",
shadowCustomColor: "#000000",
clickThrough: false
}], onChange: "updateBarConfigs"
},
desktopClockEnabled: { def: false },
desktopClockStyle: { def: "analog" },
@@ -431,7 +442,7 @@ var SPEC = {
};
function getValidKeys() {
return Object.keys(SPEC).filter(function(k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
return Object.keys(SPEC).filter(function (k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
}
function set(root, key, value, saveFn, hooks) {

View File

@@ -1,6 +1,6 @@
.pragma library
.import "./SettingsSpec.js" as SpecModule
.import "./SettingsSpec.js" as SpecModule
function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC;

View File

@@ -550,6 +550,11 @@ Item {
AppPickerModal {
id: filePickerModal
title: I18n.tr("Open with...")
viewMode: SettingsData.appPickerViewMode || "grid"
onViewModeChanged: {
SettingsData.set("appPickerViewMode", viewMode)
}
function shellEscape(str) {
return "'" + str.replace(/'/g, "'\\''") + "'";
@@ -644,6 +649,18 @@ Item {
}
}
LazyLoader {
id: workspaceRenameModalLoader
active: false
Component.onCompleted: PopoutService.workspaceRenameModalLoader = workspaceRenameModalLoader
WorkspaceRenameModal {
id: workspaceRenameModal
}
}
LazyLoader {
id: processListModalLoader
@@ -769,6 +786,7 @@ Item {
hyprKeybindsModalLoader: hyprKeybindsModalLoader
dankBarRepeater: dankBarRepeater
hyprlandOverviewLoader: hyprlandOverviewLoader
workspaceRenameModalLoader: workspaceRenameModalLoader
}
Variants {

View File

@@ -15,6 +15,7 @@ Item {
required property var hyprKeybindsModalLoader
required property var dankBarRepeater
required property var hyprlandOverviewLoader
required property var workspaceRenameModalLoader
function getFirstBar() {
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
@@ -1062,7 +1063,7 @@ Item {
}
function toggleQuery(query: string): string {
PopoutService.toggleDankLauncherV2();
PopoutService.toggleDankLauncherV2WithQuery(query);
return "LAUNCHER_TOGGLE_QUERY_SUCCESS";
}
@@ -1106,13 +1107,86 @@ Item {
}
function toggleQuery(query: string): string {
PopoutService.toggleDankLauncherV2();
PopoutService.toggleDankLauncherV2WithQuery(query);
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
}
target: "spotlight"
}
IpcHandler {
function info(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showInfo(message);
return "TOAST_INFO_SUCCESS";
}
function infoWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showInfo(message, details, command, category);
return "TOAST_INFO_SUCCESS";
}
function warn(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showWarning(message);
return "TOAST_WARN_SUCCESS";
}
function warnWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showWarning(message, details, command, category);
return "TOAST_WARN_SUCCESS";
}
function error(message: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showError(message);
return "TOAST_ERROR_SUCCESS";
}
function errorWith(message: string, details: string, command: string, category: string): string {
if (!message)
return "ERROR: No message specified";
ToastService.showError(message, details, command, category);
return "TOAST_ERROR_SUCCESS";
}
function hide(): string {
ToastService.hideToast();
return "TOAST_HIDE_SUCCESS";
}
function dismiss(category: string): string {
if (!category)
return "ERROR: No category specified";
ToastService.dismissCategory(category);
return "TOAST_DISMISS_SUCCESS";
}
function status(): string {
if (!ToastService.toastVisible)
return "hidden";
const levels = ["info", "warn", "error"];
return `visible:${levels[ToastService.currentLevel]}:${ToastService.currentMessage}`;
}
target: "toast"
}
IpcHandler {
function open(): string {
FirstLaunchService.showWelcome();
@@ -1292,4 +1366,40 @@ Item {
target: "desktopWidget"
}
IpcHandler {
function open(): string {
root.workspaceRenameModalLoader.active = true;
if (root.workspaceRenameModalLoader.item) {
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
root.workspaceRenameModalLoader.item.show(ws?.name || "");
return "WORKSPACE_RENAME_MODAL_OPENED";
}
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
}
function close(): string {
if (root.workspaceRenameModalLoader.item) {
root.workspaceRenameModalLoader.item.hide();
return "WORKSPACE_RENAME_MODAL_CLOSED";
}
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
}
function toggle(): string {
root.workspaceRenameModalLoader.active = true;
if (root.workspaceRenameModalLoader.item) {
if (root.workspaceRenameModalLoader.item.visible) {
root.workspaceRenameModalLoader.item.hide();
return "WORKSPACE_RENAME_MODAL_CLOSED";
}
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
root.workspaceRenameModalLoader.item.show(ws?.name || "");
return "WORKSPACE_RENAME_MODAL_OPENED";
}
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
}
target: "workspace-rename"
}
}

View File

@@ -33,7 +33,8 @@ Rectangle {
result.push(selectedItem.primaryAction);
}
if (selectedItem?.type === "plugin") {
switch (selectedItem?.type) {
case "plugin":
var pluginActions = getPluginContextMenuActions();
for (var i = 0; i < pluginActions.length; i++) {
var act = pluginActions[i];
@@ -44,24 +45,45 @@ Rectangle {
pluginAction: act.action
});
}
} else if (selectedItem?.type === "app" && !selectedItem?.isCore) {
break;
case "plugin_browse":
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
}
}
break;
case "app":
if (selectedItem?.isCore)
break;
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
}
}
if (SessionService.nvidiaCommand) {
result.push({
name: I18n.tr("Launch on dGPU"),
icon: "memory",
action: "launch_dgpu"
});
}
break;
}
return result;
}
readonly property bool hasActions: {
if (selectedItem?.type === "app" && !selectedItem?.isCore)
return true;
if (selectedItem?.type === "plugin") {
var pluginActions = getPluginContextMenuActions();
return pluginActions.length > 0;
switch (selectedItem?.type) {
case "app":
return !selectedItem?.isCore;
case "plugin":
return getPluginContextMenuActions().length > 0;
case "plugin_browse":
return selectedItem?.actions?.length > 0;
default:
return actions.length > 1;
}
return actions.length > 1;
}
width: parent?.width ?? 200

View File

@@ -6,6 +6,9 @@ import Quickshell.Io
import qs.Common
import qs.Services
import "Scorer.js" as Scorer
import "ControllerUtils.js" as Utils
import "NavigationHelpers.js" as Nav
import "ItemTransformers.js" as Transform
Item {
id: root
@@ -45,9 +48,14 @@ Item {
Connections {
target: PluginService
function onRequestLauncherUpdate(pluginId) {
if (activePluginId === pluginId || searchQuery) {
if (activePluginId === pluginId) {
if (activePluginCategories.length <= 1)
loadPluginCategories(pluginId);
performSearch();
return;
}
if (searchQuery)
performSearch();
}
}
@@ -130,6 +138,8 @@ Item {
property string pluginFilter: ""
property string activePluginName: ""
property var activePluginCategories: []
property string activePluginCategory: ""
function getSectionViewMode(sectionId) {
if (sectionId === "browse_plugins")
@@ -147,6 +157,10 @@ Item {
if (sectionDefinitions[i].id === sectionId)
return sectionDefinitions[i].defaultViewMode || "list";
}
if (pluginViewPreferences[sectionId]?.mode)
return pluginViewPreferences[sectionId].mode;
return "list";
}
@@ -300,10 +314,33 @@ Item {
isSearching = false;
activePluginId = "";
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
pluginFilter = "";
collapsedSections = {};
}
function loadPluginCategories(pluginId) {
if (!pluginId) {
activePluginCategories = [];
activePluginCategory = "";
return;
}
const categories = AppSearchService.getPluginLauncherCategories(pluginId);
activePluginCategories = categories;
activePluginCategory = "";
AppSearchService.setPluginLauncherCategory(pluginId, "");
}
function setActivePluginCategory(categoryId) {
if (activePluginCategory === categoryId)
return;
activePluginCategory = categoryId;
AppSearchService.setPluginLauncherCategory(activePluginId, categoryId);
performSearch();
}
function clearPluginFilter() {
if (pluginFilter) {
pluginFilter = "";
@@ -313,14 +350,30 @@ Item {
return false;
}
function preserveSelectionAfterUpdate() {
var previousSelectedId = selectedItem?.id || "";
return function (newFlatModel) {
if (!previousSelectedId)
return getFirstItemIndex();
for (var i = 0; i < newFlatModel.length; i++) {
if (!newFlatModel[i].isHeader && newFlatModel[i].item?.id === previousSelectedId)
return i;
}
return getFirstItemIndex();
};
}
function performSearch() {
var currentVersion = _searchVersion;
isSearching = true;
var restoreSelection = preserveSelectionAfterUpdate();
var cachedSections = AppSearchService.getCachedDefaultSections();
if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
activePluginId = "";
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
clearActivePluginViewPreference();
sections = cachedSections.map(function (s) {
var copy = Object.assign({}, s, {
@@ -331,7 +384,7 @@ Item {
return copy;
});
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
searchCompleted();
@@ -342,10 +395,14 @@ Item {
var triggerMatch = detectTrigger(searchQuery);
if (triggerMatch.pluginId) {
var pluginChanged = activePluginId !== triggerMatch.pluginId;
activePluginId = triggerMatch.pluginId;
activePluginName = getPluginName(triggerMatch.pluginId, triggerMatch.isBuiltIn);
applyActivePluginViewPreference(triggerMatch.pluginId, triggerMatch.isBuiltIn);
if (pluginChanged && !triggerMatch.isBuiltIn)
loadPluginCategories(triggerMatch.pluginId);
var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query);
allItems = allItems.concat(pluginItems);
@@ -370,7 +427,7 @@ Item {
}
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -380,6 +437,8 @@ Item {
activePluginId = "";
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
clearActivePluginViewPreference();
if (searchMode === "files") {
@@ -409,7 +468,7 @@ Item {
return copy;
});
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
searchCompleted();
@@ -434,7 +493,7 @@ Item {
}
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -489,7 +548,7 @@ Item {
}
flatModel = Scorer.flattenSections(sections);
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -573,7 +632,7 @@ Item {
AppSearchService.setCachedDefaultSections(sections, flatModel);
}
selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
@@ -674,249 +733,26 @@ Item {
function transformApp(app) {
var appId = app.id || app.execString || app.exec || "";
var override = SessionData.getAppOverride(appId);
var actions = [];
if (app.actions && app.actions.length > 0) {
for (var i = 0; i < app.actions.length; i++) {
actions.push({
name: app.actions[i].name,
icon: "play_arrow",
actionData: app.actions[i]
});
}
}
if (SessionService.nvidiaCommand) {
actions.push({
name: I18n.tr("Launch on dGPU"),
icon: "memory",
action: "launch_dgpu"
});
}
return {
id: appId,
type: "app",
name: override?.name || app.name || "",
subtitle: override?.comment || app.comment || "",
icon: override?.icon || app.icon || "application-x-executable",
iconType: "image",
section: "apps",
data: app,
keywords: app.keywords || [],
actions: actions,
primaryAction: {
name: I18n.tr("Launch"),
icon: "open_in_new",
action: "launch"
}
};
return Transform.transformApp(app, override, [], I18n.tr("Launch"));
}
function transformCoreApp(app) {
var iconName = "apps";
var iconType = "material";
if (app.icon) {
if (app.icon.startsWith("svg+corner:")) {
iconType = "composite";
} else if (app.icon.startsWith("material:")) {
iconName = app.icon.substring(9);
} else {
iconName = app.icon;
iconType = "image";
}
}
return {
id: app.builtInPluginId || app.action || "",
type: "app",
name: app.name || "",
subtitle: app.comment || "",
icon: iconName,
iconType: iconType,
iconFull: app.icon,
section: "apps",
data: app,
isCore: true,
actions: [],
primaryAction: {
name: I18n.tr("Open"),
icon: "open_in_new",
action: "launch"
}
};
return Transform.transformCoreApp(app, I18n.tr("Open"));
}
function transformBuiltInLauncherItem(item, pluginId) {
var rawIcon = item.icon || "extension";
var icon = stripIconPrefix(rawIcon);
var iconType = item.iconType;
if (!iconType) {
if (rawIcon.startsWith("material:"))
iconType = "material";
else if (rawIcon.startsWith("unicode:"))
iconType = "unicode";
else
iconType = "image";
}
return {
id: item.action || "",
type: "plugin",
name: item.name || "",
subtitle: item.comment || "",
icon: icon,
iconType: iconType,
section: "plugin_" + pluginId,
data: item,
pluginId: pluginId,
isBuiltInLauncher: true,
keywords: item.keywords || [],
actions: [],
primaryAction: {
name: I18n.tr("Open"),
icon: "open_in_new",
action: "execute"
}
};
return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open"));
}
function transformFileResult(file) {
var filename = file.path ? file.path.split("/").pop() : "";
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
return {
id: file.path || "",
type: "file",
name: filename,
subtitle: dirname,
icon: getFileIcon(filename),
iconType: "material",
section: "files",
data: file,
actions: [
{
name: I18n.tr("Open folder"),
icon: "folder_open",
action: "open_folder"
},
{
name: I18n.tr("Copy path"),
icon: "content_copy",
action: "copy_path"
}
],
primaryAction: {
name: I18n.tr("Open"),
icon: "open_in_new",
action: "open"
}
};
}
function getFileIcon(filename) {
var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
var iconMap = {
"pdf": "picture_as_pdf",
"doc": "description",
"docx": "description",
"odt": "description",
"xls": "table_chart",
"xlsx": "table_chart",
"ods": "table_chart",
"ppt": "slideshow",
"pptx": "slideshow",
"odp": "slideshow",
"txt": "article",
"md": "article",
"rst": "article",
"jpg": "image",
"jpeg": "image",
"png": "image",
"gif": "image",
"svg": "image",
"webp": "image",
"mp3": "audio_file",
"wav": "audio_file",
"flac": "audio_file",
"ogg": "audio_file",
"mp4": "video_file",
"mkv": "video_file",
"avi": "video_file",
"webm": "video_file",
"zip": "folder_zip",
"tar": "folder_zip",
"gz": "folder_zip",
"7z": "folder_zip",
"rar": "folder_zip",
"js": "code",
"ts": "code",
"py": "code",
"rs": "code",
"go": "code",
"java": "code",
"c": "code",
"cpp": "code",
"h": "code",
"html": "web",
"css": "web",
"htm": "web",
"json": "data_object",
"xml": "data_object",
"yaml": "data_object",
"yml": "data_object",
"sh": "terminal",
"bash": "terminal",
"zsh": "terminal"
};
return iconMap[ext] || "insert_drive_file";
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"));
}
function evaluateCalculator(query) {
if (!query || query.length === 0)
var calc = Utils.evaluateCalculator(query);
if (!calc)
return null;
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
if (mathExpr.length < 2)
return null;
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
if (!hasMath)
return null;
try {
var sanitized = mathExpr.replace(/\^/g, "**");
var result = Function('"use strict"; return (' + sanitized + ')')();
if (typeof result === "number" && isFinite(result)) {
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
return {
id: "calculator_result",
type: "calculator",
name: displayResult,
subtitle: query + " =",
icon: "calculate",
iconType: "material",
section: "calculator",
data: {
expression: query,
result: result
},
actions: [],
primaryAction: {
name: I18n.tr("Copy"),
icon: "content_copy",
action: "copy"
}
};
}
} catch (e) {}
return null;
return Transform.createCalculatorItem(calc, query, I18n.tr("Copy"));
}
function detectTrigger(query) {
@@ -988,17 +824,7 @@ Item {
}
function sortPluginIdsByOrder(pluginIds) {
var order = SettingsData.launcherPluginOrder || [];
if (order.length === 0)
return pluginIds;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return pluginIds.slice().sort(function (a, b) {
var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
return aOrder - bOrder;
});
return Utils.sortPluginIdsByOrder(pluginIds, SettingsData.launcherPluginOrder || []);
}
function getAllVisiblePluginsOrdered() {
@@ -1019,17 +845,7 @@ Item {
isBuiltIn: true
});
}
var order = SettingsData.launcherPluginOrder || [];
if (order.length === 0)
return all;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return all.sort(function (a, b) {
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
return aOrder - bOrder;
});
return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []);
}
function getEmptyTriggerPluginsOrdered() {
@@ -1052,72 +868,27 @@ Item {
isBuiltIn: true
});
}
var order = SettingsData.launcherPluginOrder || [];
if (order.length === 0)
return all;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return all.sort(function (a, b) {
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
return aOrder - bOrder;
});
return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []);
}
function getPluginBrowseItems() {
var items = [];
var browseLabel = I18n.tr("Browse");
var triggerLabel = I18n.tr("Trigger: %1");
var noTriggerLabel = I18n.tr("No trigger");
var launchers = PluginService.getLauncherPlugins();
for (var pluginId in launchers) {
var plugin = launchers[pluginId];
var trigger = PluginService.getPluginTrigger(pluginId);
var rawIcon = plugin.icon || "extension";
items.push({
id: "browse_" + pluginId,
type: "plugin_browse",
name: plugin.name || pluginId,
subtitle: trigger ? I18n.tr("Trigger: %1").arg(trigger) : I18n.tr("No trigger"),
icon: stripIconPrefix(rawIcon),
iconType: detectIconType(rawIcon),
section: "browse_plugins",
data: {
pluginId: pluginId,
plugin: plugin
},
actions: [],
primaryAction: {
name: I18n.tr("Browse"),
icon: "arrow_forward",
action: "browse_plugin"
}
});
var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId);
items.push(Transform.createPluginBrowseItem(pluginId, launchers[pluginId], trigger, false, isAllowed, browseLabel, triggerLabel, noTriggerLabel));
}
var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins();
for (var pluginId in builtInLaunchers) {
var plugin = builtInLaunchers[pluginId];
var trigger = AppSearchService.getBuiltInPluginTrigger(pluginId);
items.push({
id: "browse_" + pluginId,
type: "plugin_browse",
name: plugin.name || pluginId,
subtitle: trigger ? I18n.tr("Trigger: %1").arg(trigger) : I18n.tr("No trigger"),
icon: plugin.cornerIcon || "extension",
iconType: "material",
section: "browse_plugins",
data: {
pluginId: pluginId,
plugin: plugin,
isBuiltIn: true
},
actions: [],
primaryAction: {
name: I18n.tr("Browse"),
icon: "arrow_forward",
action: "browse_plugin"
}
});
var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId);
items.push(Transform.createPluginBrowseItem(pluginId, builtInLaunchers[pluginId], trigger, true, isAllowed, browseLabel, triggerLabel, noTriggerLabel));
}
return items;
@@ -1142,34 +913,6 @@ Item {
return transformed;
}
function detectIconType(iconName) {
if (!iconName)
return "material";
if (iconName.startsWith("unicode:"))
return "unicode";
if (iconName.startsWith("material:"))
return "material";
if (iconName.startsWith("image:"))
return "image";
if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
return "image";
if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
return "image";
return "material";
}
function stripIconPrefix(iconName) {
if (!iconName)
return "extension";
if (iconName.startsWith("unicode:"))
return iconName.substring(8);
if (iconName.startsWith("material:"))
return iconName.substring(9);
if (iconName.startsWith("image:"))
return iconName.substring(6);
return iconName;
}
function getPluginName(pluginId, isBuiltIn) {
if (isBuiltIn) {
var plugin = AppSearchService.builtInPlugins[pluginId];
@@ -1195,7 +938,7 @@ Item {
var rawIcon = launchers[pluginId].icon || "extension";
return {
name: launchers[pluginId].name || pluginId,
icon: stripIconPrefix(rawIcon)
icon: Utils.stripIconPrefix(rawIcon)
};
}
return {
@@ -1227,9 +970,8 @@ Item {
defaultViewMode: viewPref.mode || "list"
};
if (viewPref.enforced) {
setPluginViewPreference(section, viewPref.mode, true);
}
if (viewPref.mode)
setPluginViewPreference(section, viewPref.mode, viewPref.enforced);
basePriority += 0.01;
}
@@ -1265,36 +1007,7 @@ Item {
}
function transformPluginItem(item, pluginId) {
var rawIcon = item.icon || "extension";
var icon = stripIconPrefix(rawIcon);
var iconType = item.iconType;
if (!iconType) {
if (rawIcon.startsWith("material:"))
iconType = "material";
else if (rawIcon.startsWith("unicode:"))
iconType = "unicode";
else
iconType = "image";
}
return {
id: item.id || item.name || "",
type: "plugin",
name: item.name || "",
subtitle: item.comment || item.description || "",
icon: icon,
iconType: iconType,
section: "plugin_" + pluginId,
data: item,
pluginId: pluginId,
keywords: item.keywords || [],
actions: item.actions || [],
primaryAction: item.primaryAction || {
name: I18n.tr("Select"),
icon: "check",
action: "execute"
}
};
return Transform.transformPluginItem(item, pluginId, I18n.tr("Select"));
}
function getFrecencyForItem(item) {
@@ -1320,11 +1033,7 @@ Item {
}
function getFirstItemIndex() {
for (var i = 0; i < flatModel.length; i++) {
if (!flatModel[i].isHeader)
return i;
}
return 0;
return Nav.getFirstItemIndex(flatModel);
}
function updateSelectedItem() {
@@ -1345,266 +1054,67 @@ Item {
return getSectionViewMode(entry.sectionId);
}
function findNextNonHeaderIndex(startIndex) {
for (var i = startIndex; i < flatModel.length; i++) {
if (!flatModel[i].isHeader)
return i;
}
return -1;
}
function findPrevNonHeaderIndex(startIndex) {
for (var i = startIndex; i >= 0; i--) {
if (!flatModel[i].isHeader)
return i;
}
return -1;
}
function getSectionBounds(sectionId) {
var start = -1, end = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
start = i + 1;
} else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) {
end = i;
} else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) {
break;
}
}
return {
start: start,
end: end,
count: end >= start ? end - start + 1 : 0
};
}
function getGridColumns(sectionId) {
var mode = getSectionViewMode(sectionId);
if (mode === "tile")
return 3;
if (mode === "grid")
return gridColumns;
return 1;
return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns);
}
function selectNext() {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var next = findNextNonHeaderIndex(selectedFlatIndex + 1);
if (next !== -1) {
selectedFlatIndex = next;
updateSelectedItem();
}
return;
}
var viewMode = getSectionViewMode(entry.sectionId);
if (viewMode === "list") {
var next = findNextNonHeaderIndex(selectedFlatIndex + 1);
if (next !== -1) {
selectedFlatIndex = next;
updateSelectedItem();
}
return;
}
var bounds = getSectionBounds(entry.sectionId);
var cols = getGridColumns(entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
var newPosInSection = posInSection + cols;
if (newPosInSection < bounds.count) {
selectedFlatIndex = bounds.start + newPosInSection;
var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
} else {
var nextSection = findNextNonHeaderIndex(bounds.end + 1);
if (nextSection !== -1) {
selectedFlatIndex = nextSection;
updateSelectedItem();
}
}
}
function selectPrevious() {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1);
if (prev !== -1) {
selectedFlatIndex = prev;
updateSelectedItem();
}
return;
}
var viewMode = getSectionViewMode(entry.sectionId);
if (viewMode === "list") {
var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1);
if (prev !== -1) {
selectedFlatIndex = prev;
updateSelectedItem();
}
return;
}
var bounds = getSectionBounds(entry.sectionId);
var cols = getGridColumns(entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
var newPosInSection = posInSection - cols;
if (newPosInSection >= 0) {
selectedFlatIndex = bounds.start + newPosInSection;
var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
} else {
var prevItem = findPrevNonHeaderIndex(bounds.start - 1);
if (prevItem !== -1) {
selectedFlatIndex = prevItem;
updateSelectedItem();
}
}
}
function selectRight() {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var next = findNextNonHeaderIndex(selectedFlatIndex + 1);
if (next !== -1) {
selectedFlatIndex = next;
updateSelectedItem();
}
return;
}
var viewMode = getSectionViewMode(entry.sectionId);
if (viewMode === "list") {
var next = findNextNonHeaderIndex(selectedFlatIndex + 1);
if (next !== -1) {
selectedFlatIndex = next;
updateSelectedItem();
}
return;
}
var bounds = getSectionBounds(entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
if (posInSection + 1 < bounds.count) {
selectedFlatIndex = bounds.start + posInSection + 1;
var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
}
}
function selectLeft() {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1);
if (prev !== -1) {
selectedFlatIndex = prev;
updateSelectedItem();
}
return;
}
var viewMode = getSectionViewMode(entry.sectionId);
if (viewMode === "list") {
var prev = findPrevNonHeaderIndex(selectedFlatIndex - 1);
if (prev !== -1) {
selectedFlatIndex = prev;
updateSelectedItem();
}
return;
}
var bounds = getSectionBounds(entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
if (posInSection > 0) {
selectedFlatIndex = bounds.start + posInSection - 1;
var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
}
}
function selectNextSection() {
keyboardNavigationActive = true;
var currentSection = null;
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
currentSection = flatModel[selectedFlatIndex].sectionId;
}
var foundCurrent = false;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader) {
if (foundCurrent) {
for (var j = i + 1; j < flatModel.length; j++) {
if (!flatModel[j].isHeader) {
selectedFlatIndex = j;
updateSelectedItem();
return;
}
}
}
if (flatModel[i].section.id === currentSection) {
foundCurrent = true;
}
}
var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
}
}
function selectPreviousSection() {
keyboardNavigationActive = true;
var currentSection = null;
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
currentSection = flatModel[selectedFlatIndex].sectionId;
}
var lastSectionStart = -1;
var prevSectionStart = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader) {
if (flatModel[i].section.id === currentSection) {
break;
}
prevSectionStart = lastSectionStart;
lastSectionStart = i;
}
}
if (prevSectionStart >= 0) {
for (var j = prevSectionStart + 1; j < flatModel.length; j++) {
if (!flatModel[j].isHeader) {
selectedFlatIndex = j;
updateSelectedItem();
return;
}
}
var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
}
}
function selectPageDown(visibleItems) {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var itemsToSkip = visibleItems || 8;
var newIndex = selectedFlatIndex;
for (var i = 0; i < itemsToSkip; i++) {
var next = findNextNonHeaderIndex(newIndex + 1);
if (next === -1)
break;
newIndex = next;
}
var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
@@ -1613,18 +1123,7 @@ Item {
function selectPageUp(visibleItems) {
keyboardNavigationActive = true;
if (flatModel.length === 0)
return;
var itemsToSkip = visibleItems || 8;
var newIndex = selectedFlatIndex;
for (var i = 0; i < itemsToSkip; i++) {
var prev = findPrevNonHeaderIndex(newIndex - 1);
if (prev === -1)
break;
newIndex = prev;
}
var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems);
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
@@ -1755,6 +1254,14 @@ Item {
launchAppWithNvidia(item.data);
}
break;
case "toggle_all_visibility":
if (item.type === "plugin_browse" && item.data?.pluginId) {
var pluginId = item.data.pluginId;
var currentState = SettingsData.getPluginAllowWithoutTrigger(pluginId);
SettingsData.setPluginAllowWithoutTrigger(pluginId, !currentState);
performSearch();
}
return;
default:
if (item.type === "app" && action.actionData) {
launchAppAction({

View File

@@ -0,0 +1,157 @@
.pragma library
function getFileIcon(filename) {
var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
switch (ext) {
case "pdf":
return "picture_as_pdf";
case "doc":
case "docx":
case "odt":
return "description";
case "xls":
case "xlsx":
case "ods":
return "table_chart";
case "ppt":
case "pptx":
case "odp":
return "slideshow";
case "txt":
case "md":
case "rst":
return "article";
case "jpg":
case "jpeg":
case "png":
case "gif":
case "svg":
case "webp":
return "image";
case "mp3":
case "wav":
case "flac":
case "ogg":
return "audio_file";
case "mp4":
case "mkv":
case "avi":
case "webm":
return "video_file";
case "zip":
case "tar":
case "gz":
case "7z":
case "rar":
return "folder_zip";
case "js":
case "ts":
case "py":
case "rs":
case "go":
case "java":
case "c":
case "cpp":
case "h":
return "code";
case "html":
case "css":
case "htm":
return "web";
case "json":
case "xml":
case "yaml":
case "yml":
return "data_object";
case "sh":
case "bash":
case "zsh":
return "terminal";
default:
return "insert_drive_file";
}
}
function stripIconPrefix(iconName) {
if (!iconName)
return "extension";
if (iconName.startsWith("unicode:"))
return iconName.substring(8);
if (iconName.startsWith("material:"))
return iconName.substring(9);
if (iconName.startsWith("image:"))
return iconName.substring(6);
return iconName;
}
function detectIconType(iconName) {
if (!iconName)
return "material";
if (iconName.startsWith("unicode:"))
return "unicode";
if (iconName.startsWith("material:"))
return "material";
if (iconName.startsWith("image:"))
return "image";
if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
return "image";
if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
return "image";
return "material";
}
function evaluateCalculator(query) {
if (!query || query.length === 0)
return null;
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
if (mathExpr.length < 2)
return null;
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
if (!hasMath)
return null;
try {
var sanitized = mathExpr.replace(/\^/g, "**");
var result = Function('"use strict"; return (' + sanitized + ')')();
if (typeof result === "number" && isFinite(result)) {
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
return {
expression: query,
result: result,
displayResult: displayResult
};
}
} catch (e) { }
return null;
}
function sortPluginIdsByOrder(pluginIds, order) {
if (!order || order.length === 0)
return pluginIds;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return pluginIds.slice().sort(function (a, b) {
var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
return aOrder - bOrder;
});
}
function sortPluginsOrdered(plugins, order) {
if (!order || order.length === 0)
return plugins;
var orderMap = {};
for (var i = 0; i < order.length; i++)
orderMap[order[i]] = i;
return plugins.sort(function (a, b) {
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
return aOrder - bOrder;
});
}

View File

@@ -186,6 +186,14 @@ Item {
}
}
function toggleWithQuery(query) {
if (spotlightOpen) {
hide();
} else {
showWithQuery(query);
}
}
Timer {
id: closeCleanupTimer
interval: Theme.expressiveDurations.expressiveFastSpatial + 50

View File

@@ -0,0 +1,223 @@
.pragma library
.import "ControllerUtils.js" as Utils
function transformApp(app, override, defaultActions, primaryActionLabel) {
var appId = app.id || app.execString || app.exec || "";
var actions = [];
if (app.actions && app.actions.length > 0) {
for (var i = 0; i < app.actions.length; i++) {
actions.push({
name: app.actions[i].name,
icon: "play_arrow",
actionData: app.actions[i]
});
}
}
return {
id: appId,
type: "app",
name: override?.name || app.name || "",
subtitle: override?.comment || app.comment || "",
icon: override?.icon || app.icon || "application-x-executable",
iconType: "image",
section: "apps",
data: app,
keywords: app.keywords || [],
actions: actions,
primaryAction: {
name: primaryActionLabel,
icon: "open_in_new",
action: "launch"
}
};
}
function transformCoreApp(app, openLabel) {
var iconName = "apps";
var iconType = "material";
if (app.icon) {
if (app.icon.startsWith("svg+corner:")) {
iconType = "composite";
} else if (app.icon.startsWith("material:")) {
iconName = app.icon.substring(9);
} else {
iconName = app.icon;
iconType = "image";
}
}
return {
id: app.builtInPluginId || app.action || "",
type: "app",
name: app.name || "",
subtitle: app.comment || "",
icon: iconName,
iconType: iconType,
iconFull: app.icon,
section: "apps",
data: app,
isCore: true,
actions: [],
primaryAction: {
name: openLabel,
icon: "open_in_new",
action: "launch"
}
};
}
function transformBuiltInLauncherItem(item, pluginId, openLabel) {
var rawIcon = item.icon || "extension";
var icon = Utils.stripIconPrefix(rawIcon);
var iconType = item.iconType;
if (!iconType) {
if (rawIcon.startsWith("material:"))
iconType = "material";
else if (rawIcon.startsWith("unicode:"))
iconType = "unicode";
else
iconType = "image";
}
return {
id: item.action || "",
type: "plugin",
name: item.name || "",
subtitle: item.comment || "",
icon: icon,
iconType: iconType,
section: "plugin_" + pluginId,
data: item,
pluginId: pluginId,
isBuiltInLauncher: true,
keywords: item.keywords || [],
actions: [],
primaryAction: {
name: openLabel,
icon: "open_in_new",
action: "execute"
}
};
}
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
var filename = file.path ? file.path.split("/").pop() : "";
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
return {
id: file.path || "",
type: "file",
name: filename,
subtitle: dirname,
icon: Utils.getFileIcon(filename),
iconType: "material",
section: "files",
data: file,
actions: [
{
name: openFolderLabel,
icon: "folder_open",
action: "open_folder"
},
{
name: copyPathLabel,
icon: "content_copy",
action: "copy_path"
}
],
primaryAction: {
name: openLabel,
icon: "open_in_new",
action: "open"
}
};
}
function transformPluginItem(item, pluginId, selectLabel) {
var rawIcon = item.icon || "extension";
var icon = Utils.stripIconPrefix(rawIcon);
var iconType = item.iconType;
if (!iconType) {
if (rawIcon.startsWith("material:"))
iconType = "material";
else if (rawIcon.startsWith("unicode:"))
iconType = "unicode";
else
iconType = "image";
}
return {
id: item.id || item.name || "",
type: "plugin",
name: item.name || "",
subtitle: item.comment || item.description || "",
icon: icon,
iconType: iconType,
section: "plugin_" + pluginId,
data: item,
pluginId: pluginId,
keywords: item.keywords || [],
actions: item.actions || [],
primaryAction: item.primaryAction || {
name: selectLabel,
icon: "check",
action: "execute"
}
};
}
function createCalculatorItem(calc, query, copyLabel) {
return {
id: "calculator_result",
type: "calculator",
name: calc.displayResult,
subtitle: query + " =",
icon: "calculate",
iconType: "material",
section: "calculator",
data: {
expression: calc.expression,
result: calc.result
},
actions: [],
primaryAction: {
name: copyLabel,
icon: "content_copy",
action: "copy"
}
};
}
function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
return {
id: "browse_" + pluginId,
type: "plugin_browse",
name: plugin.name || pluginId,
subtitle: trigger ? triggerLabel.replace("%1", trigger) : noTriggerLabel,
icon: isBuiltIn ? rawIcon : Utils.stripIconPrefix(rawIcon),
iconType: isBuiltIn ? "material" : Utils.detectIconType(rawIcon),
section: "browse_plugins",
data: {
pluginId: pluginId,
plugin: plugin,
isBuiltIn: isBuiltIn
},
actions: [
{
name: "All",
icon: isAllowed ? "visibility" : "visibility_off",
action: "toggle_all_visibility"
}
],
primaryAction: {
name: browseLabel,
icon: "arrow_forward",
action: "browse_plugin"
}
};
}

View File

@@ -483,9 +483,64 @@ FocusScope {
}
}
Row {
id: categoryRow
width: parent.width
height: controller.activePluginCategories.length > 0 ? 36 : 0
visible: controller.activePluginCategories.length > 0
spacing: Theme.spacingS
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
DankDropdown {
id: categoryDropdown
width: Math.min(200, parent.width)
compactMode: true
dropdownWidth: 200
popupWidth: 240
maxPopupHeight: 300
enableFuzzySearch: controller.activePluginCategories.length > 8
currentValue: {
const cats = controller.activePluginCategories;
const current = controller.activePluginCategory;
if (!current)
return cats.length > 0 ? cats[0].name : "";
for (let i = 0; i < cats.length; i++) {
if (cats[i].id === current)
return cats[i].name;
}
return cats.length > 0 ? cats[0].name : "";
}
options: {
const cats = controller.activePluginCategories;
const names = [];
for (let i = 0; i < cats.length; i++)
names.push(cats[i].name);
return names;
}
onValueChanged: value => {
const cats = controller.activePluginCategories;
for (let i = 0; i < cats.length; i++) {
if (cats[i].name === value) {
controller.setActivePluginCategory(cats[i].id);
return;
}
}
}
}
}
Item {
width: parent.width
height: parent.height - searchField.height - actionPanel.height - Theme.spacingXS * 2
height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 2)
opacity: root.parentModal?.isClosing ? 0 : 1
ResultsList {

View File

@@ -131,6 +131,16 @@ Popup {
items.push({
type: "separator"
});
if (isRegularApp && SessionService.nvidiaCommand) {
items.push({
type: "item",
icon: "memory",
text: I18n.tr("Launch on dGPU"),
action: launchWithNvidia
});
}
items.push({
type: "item",
icon: "launch",

View File

@@ -0,0 +1,245 @@
.pragma library
function getFirstItemIndex(flatModel) {
for (var i = 0; i < flatModel.length; i++) {
if (!flatModel[i].isHeader)
return i;
}
return 0;
}
function findNextNonHeaderIndex(flatModel, startIndex) {
for (var i = startIndex; i < flatModel.length; i++) {
if (!flatModel[i].isHeader)
return i;
}
return -1;
}
function findPrevNonHeaderIndex(flatModel, startIndex) {
for (var i = startIndex; i >= 0; i--) {
if (!flatModel[i].isHeader)
return i;
}
return -1;
}
function getSectionBounds(flatModel, sectionId) {
var start = -1, end = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
start = i + 1;
} else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) {
end = i;
} else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) {
break;
}
}
return {
start: start,
end: end,
count: end >= start ? end - start + 1 : 0
};
}
function getGridColumns(viewMode, gridColumns) {
switch (viewMode) {
case "tile":
return 3;
case "grid":
return gridColumns;
default:
return 1;
}
}
function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
if (flatModel.length === 0)
return selectedFlatIndex;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
return next !== -1 ? next : selectedFlatIndex;
}
var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
if (actualViewMode === "list") {
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
return next !== -1 ? next : selectedFlatIndex;
}
var bounds = getSectionBounds(flatModel, entry.sectionId);
var cols = getGridColumns(actualViewMode, gridColumns);
var posInSection = selectedFlatIndex - bounds.start;
var newPosInSection = posInSection + cols;
if (newPosInSection < bounds.count) {
return bounds.start + newPosInSection;
}
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
return nextSection !== -1 ? nextSection : selectedFlatIndex;
}
function calculatePrevIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
if (flatModel.length === 0)
return selectedFlatIndex;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
return prev !== -1 ? prev : selectedFlatIndex;
}
var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
if (actualViewMode === "list") {
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
return prev !== -1 ? prev : selectedFlatIndex;
}
var bounds = getSectionBounds(flatModel, entry.sectionId);
var cols = getGridColumns(actualViewMode, gridColumns);
var posInSection = selectedFlatIndex - bounds.start;
var newPosInSection = posInSection - cols;
if (newPosInSection >= 0) {
return bounds.start + newPosInSection;
}
var prevItem = findPrevNonHeaderIndex(flatModel, bounds.start - 1);
return prevItem !== -1 ? prevItem : selectedFlatIndex;
}
function calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
if (flatModel.length === 0)
return selectedFlatIndex;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
return next !== -1 ? next : selectedFlatIndex;
}
var viewMode = getSectionViewModeFn(entry.sectionId);
if (viewMode === "list") {
var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
return next !== -1 ? next : selectedFlatIndex;
}
var bounds = getSectionBounds(flatModel, entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
if (posInSection + 1 < bounds.count) {
return bounds.start + posInSection + 1;
}
return selectedFlatIndex;
}
function calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
if (flatModel.length === 0)
return selectedFlatIndex;
var entry = flatModel[selectedFlatIndex];
if (!entry || entry.isHeader) {
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
return prev !== -1 ? prev : selectedFlatIndex;
}
var viewMode = getSectionViewModeFn(entry.sectionId);
if (viewMode === "list") {
var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
return prev !== -1 ? prev : selectedFlatIndex;
}
var bounds = getSectionBounds(flatModel, entry.sectionId);
var posInSection = selectedFlatIndex - bounds.start;
if (posInSection > 0) {
return bounds.start + posInSection - 1;
}
return selectedFlatIndex;
}
function calculateNextSectionIndex(flatModel, selectedFlatIndex) {
var currentSection = null;
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
currentSection = flatModel[selectedFlatIndex].sectionId;
}
var foundCurrent = false;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader) {
if (foundCurrent) {
for (var j = i + 1; j < flatModel.length; j++) {
if (!flatModel[j].isHeader)
return j;
}
}
if (flatModel[i].section.id === currentSection) {
foundCurrent = true;
}
}
}
return selectedFlatIndex;
}
function calculatePrevSectionIndex(flatModel, selectedFlatIndex) {
var currentSection = null;
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
currentSection = flatModel[selectedFlatIndex].sectionId;
}
var lastSectionStart = -1;
var prevSectionStart = -1;
for (var i = 0; i < flatModel.length; i++) {
if (flatModel[i].isHeader) {
if (flatModel[i].section.id === currentSection) {
break;
}
prevSectionStart = lastSectionStart;
lastSectionStart = i;
}
}
if (prevSectionStart >= 0) {
for (var j = prevSectionStart + 1; j < flatModel.length; j++) {
if (!flatModel[j].isHeader)
return j;
}
}
return selectedFlatIndex;
}
function calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems) {
if (flatModel.length === 0)
return selectedFlatIndex;
var itemsToSkip = visibleItems || 8;
var newIndex = selectedFlatIndex;
for (var i = 0; i < itemsToSkip; i++) {
var next = findNextNonHeaderIndex(flatModel, newIndex + 1);
if (next === -1)
break;
newIndex = next;
}
return newIndex;
}
function calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems) {
if (flatModel.length === 0)
return selectedFlatIndex;
var itemsToSkip = visibleItems || 8;
var newIndex = selectedFlatIndex;
for (var i = 0; i < itemsToSkip; i++) {
var prev = findPrevNonHeaderIndex(flatModel, newIndex - 1);
if (prev === -1)
break;
newIndex = prev;
}
return newIndex;
}

View File

@@ -9,7 +9,7 @@ Rectangle {
property var item: null
property bool isSelected: false
property bool isHovered: itemArea.containsMouse
property bool isHovered: itemArea.containsMouse || allModeToggleArea.containsMouse
property var controller: null
property int flatIndex: -1
@@ -38,6 +38,29 @@ Rectangle {
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
radius: Theme.cornerRadius
MouseArea {
id: itemArea
anchors.fill: parent
anchors.rightMargin: root.item?.type === "plugin_browse" ? 40 : 0
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
var scenePos = mapToItem(null, mouse.x, mouse.y);
root.rightClicked(scenePos.x, scenePos.y);
} else {
root.clicked();
}
}
onPositionChanged: {
if (root.controller)
root.controller.keyboardNavigationActive = false;
}
}
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
@@ -86,7 +109,47 @@ Rectangle {
spacing: Theme.spacingS
Rectangle {
visible: root.item?.type && root.item.type !== "app"
id: allModeToggle
visible: root.item?.type === "plugin_browse"
width: 28
height: 28
radius: 14
anchors.verticalCenter: parent.verticalCenter
color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
property bool isAllowed: {
if (root.item?.type !== "plugin_browse")
return false;
var pluginId = root.item?.data?.pluginId;
if (!pluginId)
return false;
SettingsData.launcherPluginVisibility;
return SettingsData.getPluginAllowWithoutTrigger(pluginId);
}
DankIcon {
anchors.centerIn: parent
name: allModeToggle.isAllowed ? "visibility" : "visibility_off"
size: 18
color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: allModeToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
var pluginId = root.item?.data?.pluginId;
if (!pluginId)
return;
SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed);
}
}
}
Rectangle {
visible: root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
width: typeBadge.implicitWidth + Theme.spacingS * 2
height: 20
radius: 10
@@ -116,27 +179,4 @@ Rectangle {
}
}
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
var scenePos = mapToItem(null, mouse.x, mouse.y);
root.rightClicked(scenePos.x, scenePos.y);
} else {
root.clicked();
}
}
onPositionChanged: {
if (root.controller) {
root.controller.keyboardNavigationActive = false;
}
}
}
}

View File

@@ -16,7 +16,7 @@ const Weights = {
}
function tokenize(text) {
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function(w) { return w.length > 0 })
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { return w.length > 0 })
}
function hasWordBoundaryMatch(text, query) {
@@ -164,7 +164,7 @@ function scoreItems(items, query, getFrecencyFn) {
}
}
scored.sort(function(a, b) {
scored.sort(function (a, b) {
return b.score - a.score
})
@@ -204,7 +204,7 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec
var section = sections[sectionOrder[i].id]
if (section && section.items.length > 0) {
if (sortAlphabetically && section.id === "apps") {
section.items.sort(function(a, b) {
section.items.sort(function (a, b) {
return (a.name || "").localeCompare(b.name || "")
})
}

View File

@@ -1,7 +1,9 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
@@ -21,9 +23,22 @@ Rectangle {
border.width: isSelected ? 2 : 0
border.color: Theme.primary
readonly property string toplevelId: item?.data?.toplevelId ?? ""
readonly property var waylandToplevel: {
if (!toplevelId || !item?.pluginId)
return null;
const pluginInstance = PluginService.pluginInstances[item.pluginId];
if (!pluginInstance?.getToplevelById)
return null;
return pluginInstance.getToplevelById(toplevelId);
}
readonly property bool hasScreencopy: waylandToplevel !== null
readonly property string iconValue: {
if (!item)
return "";
if (hasScreencopy)
return "";
var data = item.data;
if (data?.imageUrl)
return "image:" + data.imageUrl;
@@ -63,12 +78,26 @@ Rectangle {
color: Theme.surfaceContainerHigh
clip: true
ScreencopyView {
id: screencopyView
anchors.fill: parent
captureSource: root.waylandToplevel
live: root.hasScreencopy
visible: root.hasScreencopy
Rectangle {
anchors.fill: parent
color: root.isHovered ? Theme.withAlpha(Theme.surfaceVariant, 0.2) : "transparent"
}
}
AppIconRenderer {
anchors.fill: parent
iconValue: root.iconValue
iconSize: Math.min(parent.width, parent.height)
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
materialIconSizeAdjustment: iconSize * 0.3
visible: !root.hasScreencopy
}
Rectangle {
@@ -110,16 +139,26 @@ Rectangle {
}
}
Image {
Rectangle {
id: attributionBadge
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Theme.spacingXS
width: 40
height: 16
fillMode: Image.PreserveAspectFit
source: root.item?.data?.attribution || ""
visible: source !== ""
opacity: 0.9
width: root.hasScreencopy ? 28 : 40
height: root.hasScreencopy ? 28 : 16
radius: root.hasScreencopy ? 14 : 4
color: root.hasScreencopy ? Theme.surfaceContainer : "transparent"
visible: attributionImage.status === Image.Ready
opacity: 0.95
Image {
id: attributionImage
anchors.fill: parent
anchors.margins: root.hasScreencopy ? 4 : 0
fillMode: Image.PreserveAspectFit
source: root.item?.data?.attribution || ""
mipmap: true
}
}
}
}

View File

@@ -0,0 +1,229 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
FloatingWindow {
id: root
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
objectName: "workspaceRenameModal"
title: I18n.tr("Rename Workspace")
minimumSize: Qt.size(400, 160)
maximumSize: Qt.size(400, 160)
color: Theme.surfaceContainer
visible: false
function show(name) {
nameInput.text = name;
visible = true;
Qt.callLater(() => nameInput.forceActiveFocus());
}
function hide() {
visible = false;
}
function submitAndClose() {
renameWorkspace(nameInput.text);
hide();
}
function renameWorkspace(name) {
if (CompositorService.isNiri) {
NiriService.renameWorkspace(name);
} else if (CompositorService.isHyprland) {
HyprlandService.renameWorkspace(name);
} else {
console.warn("WorkspaceRenameModal: rename not supported for this compositor");
}
}
onVisibleChanged: {
if (visible) {
Qt.callLater(() => nameInput.forceActiveFocus());
return;
}
nameInput.text = "";
}
FocusScope {
id: contentFocusScope
anchors.fill: parent
focus: true
Keys.onEscapePressed: event => {
hide();
event.accepted = true;
}
Column {
id: contentCol
anchors.centerIn: parent
width: parent.width - Theme.spacingL * 2
spacing: Theme.spacingM
Item {
width: contentCol.width
height: Math.max(headerText.height, buttonRow.height)
MouseArea {
anchors.left: parent.left
anchors.right: buttonRow.left
anchors.rightMargin: Theme.spacingM
height: parent.height
onPressed: windowControls.tryStartMove()
onDoubleClicked: windowControls.tryToggleMaximize()
}
StyledText {
id: headerText
text: I18n.tr("Enter a new name for this workspace")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
anchors.verticalCenter: parent.verticalCenter
width: parent.width - buttonRow.width - Theme.spacingM
}
Row {
id: buttonRow
anchors.right: parent.right
spacing: Theme.spacingXS
DankActionButton {
visible: windowControls.supported && windowControls.canMaximize
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: windowControls.tryToggleMaximize()
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: hide()
}
}
}
Rectangle {
width: parent.width
height: inputFieldHeight
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: nameInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: nameInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: nameInput.forceActiveFocus()
}
DankTextField {
id: nameInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
placeholderText: I18n.tr("Workspace name")
backgroundColor: "transparent"
enabled: root.visible
onAccepted: submitAndClose()
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: cancelText
anchors.centerIn: parent
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: hide()
}
}
Rectangle {
width: Math.max(80, renameText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: renameArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
StyledText {
id: renameText
anchors.centerIn: parent
text: I18n.tr("Rename")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: renameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: submitAndClose()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
FloatingWindowControls {
id: windowControls
targetWindow: root
}
IpcHandler {
target: "workspace-rename"
function open(): string {
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
show(ws?.name || "");
return "WORKSPACE_RENAME_MODAL_OPENED";
}
function close(): string {
hide();
return "WORKSPACE_RENAME_MODAL_CLOSED";
}
}
}

View File

@@ -30,13 +30,13 @@ BasePill {
StyledText {
text: {
if (SettingsData.use24HourClock) {
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0);
} else {
const hours = systemClock?.date?.getHours();
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
const hours = systemClock?.date?.getHours();
if (SettingsData.use24HourClock)
return String(hours).padStart(2, '0').charAt(0);
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
if (SettingsData.padHours12Hour)
return String(display).padStart(2, '0').charAt(0);
}
return display >= 10 ? String(display).charAt(0) : "";
}
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
color: Theme.widgetTextColor
@@ -47,13 +47,13 @@ BasePill {
StyledText {
text: {
if (SettingsData.use24HourClock) {
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1);
} else {
const hours = systemClock?.date?.getHours();
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
const hours = systemClock?.date?.getHours();
if (SettingsData.use24HourClock)
return String(hours).padStart(2, '0').charAt(1);
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
if (SettingsData.padHours12Hour)
return String(display).padStart(2, '0').charAt(1);
}
return display >= 10 ? String(display).charAt(1) : String(display);
}
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
color: Theme.widgetTextColor

View File

@@ -508,18 +508,17 @@ Item {
function getRealWorkspaces() {
return root.workspaceList.filter(ws => {
if (useExtWorkspace)
return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
if (CompositorService.isNiri)
return ws && ws.idx !== -1;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (CompositorService.isDwl)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll)
return ws && ws.num !== -1;
return ws !== -1;
if (useExtWorkspace)
return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
if (CompositorService.isNiri)
return ws && ws.idx !== -1;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (CompositorService.isDwl)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll)
return ws && ws.num !== -1;
return ws !== -1;
});
}
@@ -864,6 +863,60 @@ Item {
property bool loadedHasIcon: false
property var loadedIcons: []
readonly property int stableIconCount: {
if (!SettingsData.showWorkspaceApps || isPlaceholder)
return 0;
let targetWorkspaceId;
if (root.useExtWorkspace) {
targetWorkspaceId = modelData?.id || modelData?.name;
} else if (CompositorService.isNiri) {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isHyprland) {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) {
targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) {
targetWorkspaceId = modelData?.num;
}
if (targetWorkspaceId === undefined || targetWorkspaceId === null)
return 0;
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels;
const seen = {};
let groupedCount = 0;
let totalCount = 0;
for (let i = 0; i < wins.length; i++) {
const w = wins[i];
if (!w)
continue;
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) {
winWs = w.workspace?.num;
} else if (CompositorService.isHyprland) {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w);
winWs = hyprToplevel?.workspace?.id;
}
if (winWs !== targetWorkspaceId)
continue;
totalCount++;
const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown";
if (!seen[appKey]) {
seen[appKey] = true;
groupedCount++;
}
}
return SettingsData.groupWorkspaceApps ? groupedCount : totalCount;
}
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
@@ -872,27 +925,29 @@ Item {
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
readonly property real iconsExtraWidth: {
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons);
if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
}
return 0;
}
readonly property real iconsExtraHeight: {
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons);
if (root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons);
return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0);
}
return 0;
}
readonly property real visualWidth: {
if (contentImplicitWidth <= 0) return baseWidth + iconsExtraWidth;
if (contentImplicitWidth <= 0)
return baseWidth + iconsExtraWidth;
const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS;
return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding);
}
readonly property real visualHeight: {
if (contentImplicitHeight <= 0) return baseHeight + iconsExtraHeight;
if (contentImplicitHeight <= 0)
return baseHeight + iconsExtraHeight;
const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS;
return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding);
}
@@ -1080,6 +1135,20 @@ Item {
width: root.isVertical ? root.widgetHeight : visualWidth
height: root.isVertical ? visualHeight : root.widgetHeight
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Rectangle {
id: focusedBorderRing
x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2
@@ -1349,6 +1418,15 @@ Item {
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
StyledText {
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon
anchors.horizontalCenter: parent.horizontalCenter
text: root.getWorkspaceIndex(modelData, index)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)

View File

@@ -20,6 +20,7 @@ Singleton {
property string matugenScheme: "scheme-tonal-spot"
property bool use24HourClock: true
property bool showSeconds: false
property bool padHours12Hour: false
property bool useFahrenheit: false
property bool nightModeEnabled: false
property string weatherLocation: "New York, NY"
@@ -39,6 +40,7 @@ Singleton {
property string widgetBackgroundColor: "sch"
property string lockDateFormat: ""
property bool lockScreenShowPowerActions: true
property bool lockScreenShowProfileImage: true
property var screenPreferences: ({})
property int animationSpeed: 2
property string wallpaperFillMode: "Fill"
@@ -52,6 +54,7 @@ Singleton {
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
@@ -71,6 +74,7 @@ Singleton {
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
@@ -88,6 +92,14 @@ Singleton {
}
}
function getEffectiveTimeFormat() {
if (use24HourClock)
return showSeconds ? "hh:mm:ss" : "hh:mm";
if (padHours12Hour)
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
}
function getEffectiveLockDateFormat() {
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
}

View File

@@ -23,7 +23,6 @@ Item {
readonly property string xdgDataDirs: Quickshell.env("XDG_DATA_DIRS")
property string screenName: ""
property string randomFact: ""
property string hyprlandCurrentLayout: ""
property string hyprlandKeyboard: ""
property int hyprlandLayoutCount: 0
@@ -31,10 +30,6 @@ Item {
signal launchRequested
function pickRandomFact() {
randomFact = Facts.getRandomFact();
}
property bool weatherInitialized: false
function initWeatherService() {
@@ -58,7 +53,6 @@ Item {
}
Component.onCompleted: {
pickRandomFact();
initWeatherService();
if (isPrimaryScreen)
@@ -223,7 +217,7 @@ Item {
spacing: 0
property string fullTimeStr: {
const format = GreetdSettings.use24HourClock ? (GreetdSettings.showSeconds ? "HH:mm:ss" : "HH:mm") : (GreetdSettings.showSeconds ? "h:mm:ss AP" : "h:mm AP");
const format = GreetdSettings.getEffectiveTimeFormat();
return systemClock.date.toLocaleTimeString(Qt.locale(), format);
}
property var timeParts: fullTimeStr.split(':')
@@ -369,6 +363,7 @@ Item {
return PortalService.profileImage;
}
fallbackIcon: "person"
visible: GreetdSettings.lockScreenShowProfileImage
}
Rectangle {
@@ -961,20 +956,6 @@ Item {
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Theme.spacingL
width: Math.min(parent.width - Theme.spacingXL * 2, implicitWidth)
text: root.randomFact
font.pixelSize: Theme.fontSizeSmall
color: "white"
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.NoWrap
visible: root.randomFact !== ""
}
DankActionButton {
anchors.bottom: parent.bottom
anchors.left: parent.left

View File

@@ -25,7 +25,6 @@ Item {
property string screenName: ""
property bool unlocking: false
property string pamState: ""
property string randomFact: ""
property string hyprlandCurrentLayout: ""
property string hyprlandKeyboard: ""
property int hyprlandLayoutCount: 0
@@ -41,15 +40,7 @@ Item {
pamState = "";
}
function pickRandomFact() {
randomFact = Facts.getRandomFact();
}
Component.onCompleted: {
if (demoMode) {
pickRandomFact();
}
WeatherService.addRef();
UserInfoService.getUserInfo();
@@ -61,11 +52,6 @@ Item {
lockerReadyArmed = true;
}
onDemoModeChanged: {
if (demoMode) {
pickRandomFact();
}
}
Component.onDestruction: {
WeatherService.removeRef();
if (CompositorService.isHyprland) {
@@ -1195,12 +1181,12 @@ Item {
height: 24
color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter
visible: MprisController.activePlayer
visible: MprisController.activePlayer && SettingsData.lockScreenShowMediaPlayer
}
Row {
spacing: Theme.spacingS
visible: MprisController.activePlayer
visible: MprisController.activePlayer && SettingsData.lockScreenShowMediaPlayer
anchors.verticalCenter: parent.verticalCenter
Item {
@@ -1369,7 +1355,7 @@ Item {
height: 24
color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter
visible: MprisController.activePlayer && WeatherService.weather.available
visible: MprisController.activePlayer && SettingsData.lockScreenShowMediaPlayer && WeatherService.weather.available
}
Row {
@@ -1606,20 +1592,6 @@ Item {
}
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Theme.spacingL
width: Math.min(parent.width - Theme.spacingXL * 2, implicitWidth)
text: root.randomFact
font.pixelSize: Theme.fontSizeSmall
color: "white"
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.NoWrap
visible: root.randomFact !== ""
}
}
Pam {

View File

@@ -42,8 +42,6 @@ DankPopout {
if (!shouldBeVisible) {
searchText = "";
expandedPid = "";
if (processesView)
processesView.reset();
}
}
@@ -108,8 +106,11 @@ DankPopout {
Connections {
target: processListPopout
function onShouldBeVisibleChanged() {
if (processListPopout.shouldBeVisible)
if (processListPopout.shouldBeVisible) {
Qt.callLater(() => processListContent.forceActiveFocus());
} else {
processesView.reset();
}
}
}

View File

@@ -76,6 +76,14 @@ Item {
onToggled: checked => SettingsData.set("lockScreenShowPasswordField", checked)
}
SettingsToggleRow {
settingKey: "lockScreenShowMediaPlayer"
tags: ["lock", "screen", "media", "player", "music", "mpris"]
text: I18n.tr("Show Media Player", "Enable media player controls on the lock screen window")
checked: SettingsData.lockScreenShowMediaPlayer
onToggled: checked => SettingsData.set("lockScreenShowMediaPlayer", checked)
}
SettingsDropdownRow {
settingKey: "lockScreenNotificationMode"
tags: ["lock", "screen", "notification", "notifications", "privacy"]

View File

@@ -125,6 +125,19 @@ Item {
return Theme.warning;
}
function formatThemeAutoTime(isoString) {
if (!isoString)
return "";
try {
const date = new Date(isoString);
if (isNaN(date.getTime()))
return "";
return date.toLocaleTimeString(Qt.locale(), "HH:mm");
} catch (e) {
return "";
}
}
Component.onCompleted: {
SettingsData.detectAvailableIconThemes();
SettingsData.detectAvailableCursorThemes();
@@ -152,7 +165,6 @@ Item {
}
themeColorsTab.templateDetection = detection;
} catch (e) {
console.warn("ThemeColorsTab: Failed to parse template check:", e);
}
}
}
@@ -962,6 +974,453 @@ Item {
}
}
SettingsCard {
tab: "theme"
tags: ["automatic", "color", "mode", "schedule", "sunrise", "sunset"]
title: I18n.tr("Automatic Color Mode")
settingKey: "automaticColorMode"
iconName: "schedule"
Column {
width: parent.width
spacing: Theme.spacingM
DankToggle {
id: themeModeAutoToggle
width: parent.width
text: I18n.tr("Enable Automatic Switching")
description: I18n.tr("Automatically switch between light and dark modes based on time or sunrise/sunset")
checked: SessionData.themeModeAutoEnabled
onToggled: checked => {
SessionData.setThemeModeAutoEnabled(checked);
}
Connections {
target: SessionData
function onThemeModeAutoEnabledChanged() {
themeModeAutoToggle.checked = SessionData.themeModeAutoEnabled;
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoEnabled
DankToggle {
width: parent.width
text: I18n.tr("Share Gamma Control Settings")
description: I18n.tr("Use the same time and location settings as gamma control")
checked: SessionData.themeModeShareGammaSettings
onToggled: checked => {
SessionData.setThemeModeShareGammaSettings(checked);
}
}
Item {
width: parent.width
height: 45 + Theme.spacingM
DankTabBar {
id: themeModeTabBar
width: 200
height: 45
anchors.horizontalCenter: parent.horizontalCenter
model: [
{ "text": "Time", "icon": "access_time" },
{ "text": "Location", "icon": "place" }
]
Component.onCompleted: {
currentIndex = SessionData.themeModeAutoMode === "location" ? 1 : 0;
Qt.callLater(updateIndicator);
}
onTabClicked: index => {
SessionData.setThemeModeAutoMode(index === 1 ? "location" : "time");
currentIndex = index;
}
Connections {
target: SessionData
function onThemeModeAutoModeChanged() {
themeModeTabBar.currentIndex = SessionData.themeModeAutoMode === "location" ? 1 : 0;
Qt.callLater(themeModeTabBar.updateIndicator);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoMode === "time" && !SessionData.themeModeShareGammaSettings
Column {
spacing: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingM
StyledText {
text: ""
width: 80
height: 20
}
StyledText {
text: I18n.tr("Hour")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 70
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: I18n.tr("Minute")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 70
horizontalAlignment: Text.AlignHCenter
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Dark Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 80
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeStartHour.toString()
options: {
var hours = [];
for (var i = 0; i < 24; i++) hours.push(i.toString());
return hours;
}
onValueChanged: value => {
SessionData.setThemeModeStartHour(parseInt(value));
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeStartMinute.toString().padStart(2, '0')
options: {
var minutes = [];
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'));
}
return minutes;
}
onValueChanged: value => {
SessionData.setThemeModeStartMinute(parseInt(value));
}
}
}
Row {
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Light Start")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: 80
height: 40
verticalAlignment: Text.AlignVCenter
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeEndHour.toString()
options: {
var hours = [];
for (var i = 0; i < 24; i++) hours.push(i.toString());
return hours;
}
onValueChanged: value => {
SessionData.setThemeModeEndHour(parseInt(value));
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.themeModeEndMinute.toString().padStart(2, '0')
options: {
var minutes = [];
for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0'));
}
return minutes;
}
onValueChanged: value => {
SessionData.setThemeModeEndMinute(parseInt(value));
}
}
}
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light mode will be active from Light Start to Dark Start") : I18n.tr("Dark mode will be active from Dark Start to Light Start")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SessionData.themeModeAutoMode === "location" && !SessionData.themeModeShareGammaSettings
DankToggle {
id: themeModeIpLocationToggle
width: parent.width
text: I18n.tr("Use IP Location")
description: I18n.tr("Automatically detect location based on IP address")
checked: SessionData.nightModeUseIPLocation || false
onToggled: checked => {
SessionData.setNightModeUseIPLocation(checked);
}
Connections {
target: SessionData
function onNightModeUseIPLocationChanged() {
themeModeIpLocationToggle.checked = SessionData.nightModeUseIPLocation;
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: !SessionData.nightModeUseIPLocation
StyledText {
text: I18n.tr("Manual Coordinates")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
Row {
spacing: Theme.spacingL
anchors.horizontalCenter: parent.horizontalCenter
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Latitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.latitude.toString()
placeholderText: "0.0"
onEditingFinished: {
const lat = parseFloat(text);
if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) {
SessionData.setLatitude(lat);
}
}
}
}
Column {
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Longitude")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: 120
height: 40
text: SessionData.longitude.toString()
placeholderText: "0.0"
onEditingFinished: {
const lon = parseFloat(text);
if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) {
SessionData.setLongitude(lon);
}
}
}
}
}
StyledText {
text: I18n.tr("Uses sunrise/sunset times to automatically adjust theme mode based on your location.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light mode will be active from sunrise to sunset") : I18n.tr("Dark mode will be active from sunset to sunrise")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
visible: SessionData.nightModeUseIPLocation || (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0)
}
}
StyledText {
width: parent.width
text: I18n.tr("Using shared settings from Gamma Control")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
visible: SessionData.themeModeShareGammaSettings
}
Rectangle {
width: parent.width
height: statusRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Row {
id: statusRow
anchors.centerIn: parent
spacing: Theme.spacingL
width: parent.width - Theme.spacingM * 2
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: 8
height: 8
radius: 4
color: SessionData.themeModeAutoEnabled ? Theme.success : Theme.error
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Automation")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
}
StyledText {
text: SessionData.themeModeAutoEnabled ? I18n.tr("Enabled") : I18n.tr("Disabled")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingS
DankIcon {
name: SessionData.isLightMode ? "light_mode" : "dark_mode"
size: Theme.iconSizeMedium
color: SessionData.isLightMode ? "#FFA726" : "#7E57C2"
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: SessionData.isLightMode ? I18n.tr("Light Mode") : I18n.tr("Dark Mode")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("Active")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
Column {
spacing: 2
width: (parent.width - Theme.spacingL * 2) / 3
anchors.verticalCenter: parent.verticalCenter
visible: SessionData.themeModeAutoEnabled && SessionData.themeModeNextTransition
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "schedule"
size: Theme.iconSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Next Transition")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: themeColorsTab.formatThemeAutoTime(SessionData.themeModeNextTransition)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
}
}
}
}
}
SettingsCard {
tab: "theme"
tags: ["light", "dark", "mode", "appearance"]

View File

@@ -49,6 +49,17 @@ Item {
checked: SettingsData.showSeconds
onToggled: checked => SettingsData.set("showSeconds", checked)
}
SettingsToggleRow {
tab: "time"
tags: ["time", "12hour", "format", "padding", "leading", "zero"]
settingKey: "padHours12Hour"
text: I18n.tr("Pad Hours")
description: "02:31 PM vs 2:31 PM"
checked: SettingsData.padHours12Hour
onToggled: checked => SettingsData.set("padHours12Hour", checked)
visible: !SettingsData.use24HourClock
}
}
SettingsCard {

View File

@@ -884,4 +884,52 @@ Singleton {
return allItems;
}
function getPluginLauncherCategories(pluginId) {
if (typeof PluginService === "undefined")
return [];
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return [];
if (typeof instance.getCategories !== "function")
return [];
try {
return instance.getCategories() || [];
} catch (e) {
console.warn("AppSearchService: Error getting categories from plugin", pluginId, ":", e);
return [];
}
}
function setPluginLauncherCategory(pluginId, categoryId) {
if (typeof PluginService === "undefined")
return;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return;
if (typeof instance.setCategory !== "function")
return;
try {
instance.setCategory(categoryId);
} catch (e) {
console.warn("AppSearchService: Error setting category on plugin", pluginId, ":", e);
}
}
function pluginHasCategories(pluginId) {
if (typeof PluginService === "undefined")
return false;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return false;
return typeof instance.getCategories === "function";
}
}

View File

@@ -130,7 +130,7 @@ Singleton {
Component.onCompleted: {
root.userPreference = SettingsData.networkPreference;
lastConnectedVpnUuid = SettingsData.vpnLastConnected || "";
lastConnectedVpnUuid = SessionData.vpnLastConnected || "";
if (socketPath && socketPath.length > 0) {
checkDMSCapabilities();
}
@@ -293,7 +293,7 @@ Singleton {
if (vpnConnected && activeUuid) {
lastConnectedVpnUuid = activeUuid;
SettingsData.set("vpnLastConnected", activeUuid);
SessionData.setVpnLastConnected(activeUuid);
}
if (vpnIsBusy) {

View File

@@ -56,6 +56,7 @@ Singleton {
signal wlrOutputStateUpdate(var data)
signal evdevStateUpdate(var data)
signal gammaStateUpdate(var data)
signal themeAutoStateUpdate(var data)
signal openUrlRequested(string url)
signal appPickerRequested(var data)
signal screensaverStateUpdate(var data)
@@ -64,7 +65,7 @@ Singleton {
property bool screensaverInhibited: false
property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus"]
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus"]
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
@@ -304,7 +305,7 @@ Singleton {
excludeServices = [excludeServices];
}
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
const filtered = allServices.filter(s => !excludeServices.includes(s));
subscribe(filtered);
}
@@ -373,6 +374,8 @@ Singleton {
evdevStateUpdate(data);
} else if (service === "gamma") {
gammaStateUpdate(data);
} else if (service === "theme.auto") {
themeAutoStateUpdate(data);
} else if (service === "browser.open_requested") {
if (data.target) {
if (data.requestType === "url" || !data.requestType) {
@@ -737,4 +740,10 @@ Singleton {
if (callback) callback(response);
});
}
function renameWorkspace(name, callback) {
sendRequest("extworkspace.renameWorkspace", {
"name": name
}, callback);
}
}

View File

@@ -308,4 +308,18 @@ decoration {
reloadConfig();
});
}
function renameWorkspace(newName) {
if (!Hyprland.focusedWorkspace)
return;
const wsId = Hyprland.focusedWorkspace.id;
if (!wsId)
return;
const fullName = wsId + " " + newName;
Proc.runCommand("hyprland-rename-ws", ["hyprctl", "dispatch", "renameworkspace", String(wsId), fullName], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("HyprlandService: Failed to rename workspace:", output);
}
});
}
}

View File

@@ -1422,6 +1422,17 @@ Singleton {
return block;
}
function renameWorkspace(name) {
return send({
"Action": {
"SetWorkspaceName": {
"name": name,
"workspace": null
}
}
});
}
IpcHandler {
function screenshot(): string {
if (!CompositorService.isNiri) {

View File

@@ -416,6 +416,17 @@ Singleton {
}
}
function toggleDankLauncherV2WithQuery(query: string) {
if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggleWithQuery(query);
} else if (dankLauncherV2ModalLoader) {
_dankLauncherV2PendingQuery = query;
_dankLauncherV2WantsOpen = true;
_dankLauncherV2WantsToggle = false;
dankLauncherV2ModalLoader.active = true;
}
}
function _onDankLauncherV2ModalLoaded() {
if (_dankLauncherV2WantsOpen) {
_dankLauncherV2WantsOpen = false;

View File

@@ -396,7 +396,6 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentValue = delegateRoot.modelData;
root.valueChanged(delegateRoot.modelData);
dropdownMenu.close();
}

View File

@@ -1,6 +1,6 @@
/**
* @name dms-midnight
* @description midnight-discord, generated by dms
* @name midnight
* @description A dark, rounded discord theme.
* @author refact0r
* @version 1.6.2
* @invite nz87hXyvcy
@@ -17,13 +17,13 @@
/* customize things here */
:root {
/* font, change to 'gg sans' for default discord font*/
--font: 'figtree';
--font: 'gg sans';
/* top left corner text */
--corner-text: 'Midnight';
/* color of status indicators and window controls */
--online-indicator: {{colors.inverse_primary.default.hex}}; /* change to #23a55a for default green */
--online-indicator: {{colors.inverse_primary.default.hex}}; /* change to #23a55a for default green */
--dnd-indicator: {{colors.error.default.hex}}; /* change to #f13f43 for default red */
--idle-indicator: {{colors.tertiary_container.default.hex}}; /* change to #f0b232 for default yellow */
--streaming-indicator: {{colors.on_primary.default.hex}}; /* change to #593695 for default purple */
@@ -34,11 +34,11 @@
--accent-3: {{colors.primary.default.hex}}; /* accent buttons */
--accent-4: {{colors.surface_bright.default.hex}}; /* accent buttons when hovered */
--accent-5: {{colors.primary_fixed_dim.default.hex}}; /* accent buttons when clicked */
--mention: {{colors.background.default.hex}}; /* mentions & mention messages */
--mention: {{colors.surface.default.hex}}; /* mentions & mention messages */
--mention-hover: {{colors.surface_bright.default.hex}}; /* mentions & mention messages when hovered */
/* text colors */
--text-0: {{colors.background.default.hex}}; /* text on colored elements */
--text-0: {{colors.surface.default.hex}}; /* text on colored elements */
--text-1: {{colors.on_surface.default.hex}}; /* other normally white text */
--text-2: {{colors.on_surface.default.hex}}; /* headings and important text */
--text-3: {{colors.on_surface_variant.default.hex}}; /* normal text */
@@ -46,10 +46,10 @@
--text-5: {{colors.outline.default.hex}}; /* muted channels/chats and timestamps */
/* background and dark colors */
--bg-1: {{colors.primary.default.hex}}; /* dark buttons when clicked */
--bg-2: {{colors.surface_container.default.hex}}; /* dark buttons */
--bg-1: {{colors.surface_variant.default.hex}}; /* dark buttons when clicked */
--bg-2: {{colors.surface_container_high.default.hex}}; /* dark buttons */
--bg-3: {{colors.surface_container_low.default.hex}}; /* spacing, secondary elements */
--bg-4: {{colors.background.default.hex}}; /* main background color */
--bg-4: {{colors.surface.default.hex}}; /* main background color */
--hover: {{colors.surface_bright.default.hex}}; /* channels and buttons when hovered */
--active: {{colors.surface_bright.default.hex}}; /* channels and buttons when clicked or selected */
--message-hover: {{colors.surface_bright.default.hex}}; /* messages when hovered */