1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 11:38:30 -04:00

Compare commits

...

42 Commits

Author SHA1 Message Date
dms-ci[bot] 069ddab041 bump VERSION to v1.5.3 2026-07-27 00:17:35 +00:00
bbedward 45cf6ecefd sysupdate: fix gnome-terminal title
(cherry picked from commit c367153bac)
2026-07-26 19:51:46 -04:00
bbedward 7058b00091 cups: fix sub/unsub race
port 1.5

(cherry picked from commit bab2078dfd)
2026-07-26 23:43:53 +00:00
bbedward 6ba4b79039 port: record hand-ported master equivalents
(cherry picked from commit e9bc0169f6)
(cherry picked from commit 9b7d3c64fe)
(cherry picked from commit ea03fb2788)
(cherry picked from commit 0815e48465)
(cherry picked from commit 3938e60ce4)
(cherry picked from commit 3c688cfbd3)
(cherry picked from commit e63d210358)
(cherry picked from commit fa094db127)
2026-07-26 19:32:23 -04:00
bbedward b8e2ce1da8 dock: use required properties in dock item delegate, fixes context injection on Qt 6.8 (#2926)
(cherry picked from commit fa629fbf99)
2026-07-26 19:15:44 -04:00
bbedward c58d55db7d bluetooth: add soft rfkill unblock, wait for adapter to become available
instead of forfeiting subscription

fixes #2922
fixes #1537

(cherry picked from commit 77a357109a)
2026-07-26 19:15:02 -04:00
bbedward e15db00714 cc: migrate some settings to cache
port 1.5

(cherry picked from commit e9bc0169f6)
2026-07-26 21:46:22 +00:00
bbedward 906eba5c17 battery: auto-apply power profile at startup when DMS managed
fixes #2935
port 1.5

(cherry picked from commit 59c83d19e4)
2026-07-26 19:27:56 +00:00
bbedward d2b61b91ef dbar: fix click-through mask of center section
fixes #2938

(cherry picked from commit 9b7d3c64fe)
2026-07-26 15:22:03 -04:00
bbedward 6c27b92445 dbar/notifications: fix right click context menu position and add middle
click to DnD

(cherry picked from commit 1f94c3cbd4)
2026-07-26 14:52:39 -04:00
bbedward a64bd80cc9 window-rules/niri: make floating a tri-state
(cherry picked from commit 814871d982)
2026-07-26 14:34:50 -04:00
feng-yifan 661dc27d26 fix(settings): reset AppBrowserPopup model on close to avoid crash (#2925)
Reopening the autostart "Browse" picker crashed in
QQmlIncubatorPrivate::incubate. Drop the ListView model on hide()
and rebind it on show() so each open starts with a fresh
QQmlDelegateModel instead of reusing a stale incubation queue.

(cherry picked from commit 3182c70857)
2026-07-26 14:34:30 -04:00
bbedward 3d5a6fb9d7 display config: preserve config when turning off on niri
fixes #2939
port 1.5

(cherry picked from commit 24cb3d19a0)
2026-07-26 18:26:18 +00:00
bbedward c2a5397d8a notification: clip region on animation exit
related #2917
port 1.5

(cherry picked from commit f57f254855)
2026-07-24 18:36:07 +00:00
bbedward 9b5538323d lock: guard dpms window 2026-07-24 14:13:43 -04:00
bbedward 8321d28d18 greeter: don't override manually selected session with saved/defaul 2026-07-24 11:47:59 -04:00
bbedward 39278860c8 cups: fix initialization race
port 1.5

(cherry picked from commit ea03fb2788)
2026-07-24 10:58:41 -04:00
bbedward f455456ff1 dock: limit input mask to body, disable hover headroom unless enabled
fixes #2923

(cherry picked from commit 0815e48465)
2026-07-24 10:35:40 -04:00
bbedward 5a80a32951 network: fix networkPreference reverting from eth to wifi
fixes #2919
port 1.5

(cherry picked from commit 42060366cb)
2026-07-24 14:14:08 +00:00
Aldi Prasetyo 3cedd56f23 fix(minimal-install): add hyprland packages to minimal install preference (#2872)
(I guess we'll find out if this breaks any minimal script to desktop runs)

(cherry picked from commit ff71502083)
2026-07-24 09:31:46 -04:00
Legend-017 dc1ad5b329 Fix(spotlight): improve file search query restoration and mode switching (#2887)
* fix(spotlight): fix empty results on restoring last file search query

- When "remember last query" is enabled and the last query is file search, it shows "no results" until the query is manually modified in full launcher mode.

* fix(spotlight): properly restore last selected mode(tab) when enabled in full launcher mode

* fix(spotlight): allow file trigger in plugins mode(tab)

* fix(spotlight): restore previous mode when clearing '/' file search prefix

When the '/' prefix is removed from the query, the controller now correctly reverts to the previous mode instead of remaining stuck in "files" mode.

* fix(spotlight): code review fix

(cherry picked from commit 367ad5f69a)
2026-07-24 09:31:32 -04:00
Thomas Kroll 25c12a14a9 fix(lock): hold sleep inhibitor until compositor confirms lock (#2907)
lock-before-suspend released the logind sleep delay inhibitor as soon as Qt
reported the lock rendered (afterAnimating/afterRendering). That signal fires
before the compositor has committed/presented the ext-session-lock surface, so
the machine could freeze ~1 frame later with the desktop still the last
presented frame. On resume the desktop was briefly visible before the lock
appeared (both suspend and hibernate).

Gate lockerReady on WlSessionLock.secure (the ext-session-lock `locked` event),
so the inhibitor is held until the compositor confirms the session is locked and
the desktop is hidden. LockSurface passes the WlSessionLock down as sessionLock;
the readiness check returns early until secure, and re-fires on secureChanged.
The Go-side fallback timer in loginctl monitor remains as the backstop.

Reproduced on niri / s2idle with 3 outputs: debug log showed lockerReady sent
18 ms before `PM: suspend entry`; with this change the desktop no longer flashes
on resume.

Signed-off-by: Thomas Kroll <99196436+tkroll-ionos@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 237fb57d5e)
2026-07-24 09:31:23 -04:00
bbedward b72b276fb4 lock/pam: attempt to fix fprintd retry spam after resume
(cherry picked from commit 958672badd)
2026-07-24 09:30:57 -04:00
bbedward f89fb67316 settings: fix close IPC
fixes #2912
port 1.5

(cherry picked from commit 5854327c28)
2026-07-23 18:10:19 +00:00
bbedward f457e04cb1 calendar/dcal: fix flatpak socket detection
port 1.5

(cherry picked from commit d818067bee)
2026-07-23 14:14:49 +00:00
bbedward 23daca0b57 location: prevent calls when weather auto-locate is disabled
port 1.5

(cherry picked from commit fe561f2b5d)
2026-07-22 03:36:01 +00:00
bbedward accdd0972c popout: fix lifecycle of blur with various popouts
fixes #2901
port 1.5

(cherry picked from commit 93c3030a1e)
2026-07-21 21:47:06 +00:00
bbedward 00c28f69fd wallpaper: use filter chips for rando transitions 2026-07-21 11:45:18 -04:00
bbedward 2e0337b68c lock: avoid lock surface churn
related #2885
port 1.5

(cherry picked from commit 30d9a34011)
2026-07-21 14:51:07 +00:00
bbedward 4975960ea4 compositor/toplevels: filter on screen for generic compositors, without
workspace distinction

fixes #2890
port 1.5

(cherry picked from commit f0bdaa7f72)
2026-07-21 14:27:43 +00:00
bbedward 0381a112b3 polkit: fix DMS_DISABLE_POLKIT regression from qs 0.2 deprecation commit
fixes #2897
port 1.5

(cherry picked from commit 68e60f036d)
2026-07-20 20:08:20 +00:00
bbedward ed5e2a22c0 audio: revert wpctl workaround
related #2899
port 1.5

(cherry picked from commit 70ceee139b)
2026-07-20 19:19:45 +00:00
bbedward 847718459c lock: fix dpms fade surface getting stuck after unlock
port 1.5

(cherry picked from commit 116cef0b3a)
2026-07-20 14:40:30 +00:00
bbedward 95b120b475 systray: fix menu positioning on vbars
fixes #2818
port 1.5

(cherry picked from commit 1402d2312a)
2026-07-19 20:37:56 +00:00
bbedward 4dd2f1d5be matugen: add config dirs to detection of presence
related #2894
port 1.5

(cherry picked from commit 57fd39d84d)
2026-07-19 16:45:46 +00:00
bbedward bf5faf725b mango: suppress excessive reload messages
fixes #2848
port 1.5

(cherry picked from commit 6239e5a8e4)
2026-07-19 16:45:44 +00:00
bbedward e5eff919b8 matugen: remove padding 0 from gtk light
fixes #2884
port 1.5

(cherry picked from commit 2a8b88c1f0)
2026-07-19 15:38:19 +00:00
bbedward 606115748a theme: put full theme catalog in neovim dropdown
fixes #2892
port 1.5

(cherry picked from commit dbaf2e922c)
2026-07-19 15:21:42 +00:00
bbedward ad936bfdc9 dash: fix color tokens for placeholder in calendar, and weather failed
fixes #2891
port 1.5

(cherry picked from commit eb330f62e1)
2026-07-19 15:18:40 +00:00
bbedward 110396cb2a powermenu: don't require key release for actions other than suspend and
hibernate

port 1.5

(cherry picked from commit 2846363f55)
2026-07-18 19:31:08 +00:00
bbedward 7b08e61863 matugen/neovim: respect terminalsAlwaysDark
port 1.5

(cherry picked from commit 4a90bd3fb1)
2026-07-18 15:52:47 +00:00
bbedward 876ead665b lock: add resilience for when the lock surface is rejected
related #2778
related #2841

port 1.5

(cherry picked from commit 698b66cf91)
2026-07-18 15:51:52 +00:00
89 changed files with 2091 additions and 469 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ type minimalInstallGroup struct {
func shouldPreferMinimalInstall(pkg string) bool { func shouldPreferMinimalInstall(pkg string) bool {
switch pkg { switch pkg {
case "niri", "niri-git": case "niri", "niri-git", "hyprland", "hyprland-git":
return true return true
default: default:
return false return false
+25 -9
View File
@@ -44,6 +44,7 @@ type TemplateDef struct {
ID string ID string
Commands []string Commands []string
Flatpaks []string Flatpaks []string
ConfigDirs []string
ConfigFile string ConfigFile string
Kind TemplateKind Kind TemplateKind
RunUnconditionally bool RunUnconditionally bool
@@ -60,9 +61,9 @@ var templateRegistry = []TemplateDef{
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"}, {ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"}, {ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"}, {ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"}, {ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigDirs: []string{"vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigFile: "vencord.toml"}, {ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigDirs: []string{"Vencord"}, ConfigFile: "vencord.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"}, {ID: "equibop", Commands: []string{"equibop"}, ConfigDirs: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal}, {ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal}, {ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},
{ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal}, {ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal},
@@ -459,9 +460,9 @@ output_path = '%s'
case TemplateKindGTK: case TemplateKindGTK:
switch opts.Mode { switch opts.Mode {
case ColorModeLight: case ColorModeLight:
appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml") appendConfig(opts, cfgFile, nil, nil, nil, "gtk3-light.toml")
default: default:
appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml") appendConfig(opts, cfgFile, nil, nil, nil, "gtk3-dark.toml")
} }
case TemplateKindTerminal: case TemplateKindTerminal:
appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
@@ -474,10 +475,10 @@ output_path = '%s'
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs: case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" { if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
} }
default: default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
} }
} }
@@ -512,13 +513,14 @@ func appendConfig(
cfgFile *os.File, cfgFile *os.File,
checkCmd []string, checkCmd []string,
checkFlatpaks []string, checkFlatpaks []string,
checkConfigDirs []string,
fileName string, fileName string,
) { ) {
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName) configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
return return
} }
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) { if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) && !configDirExists(checkConfigDirs) {
return return
} }
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
@@ -599,6 +601,20 @@ func templateSessionActive(tmpl TemplateDef) bool {
return err == nil return err == nil
} }
func configDirExists(names []string) bool {
configHome := utils.XDGConfigHome()
if configHome == "" {
return false
}
for _, name := range names {
info, err := os.Stat(filepath.Join(configHome, name))
if err == nil && info.IsDir() {
return true
}
}
return false
}
func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool { func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool {
// Both nil is treated as "skip check" / unconditionally run // Both nil is treated as "skip check" / unconditionally run
if checkCmd == nil && checkFlatpaks == nil { if checkCmd == nil && checkFlatpaks == nil {
@@ -1096,7 +1112,7 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
case tmpl.Kind == TemplateKindEmacs: case tmpl.Kind == TemplateKindEmacs:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != "" detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
default: default:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && templateSessionActive(tmpl) detected = (appExists(checker, tmpl.Commands, tmpl.Flatpaks) || configDirExists(tmpl.ConfigDirs)) && templateSessionActive(tmpl)
} }
checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected}) checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected})
+95 -8
View File
@@ -38,7 +38,7 @@ func TestAppendConfigBinaryExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"sh"}, nil, "test.toml") appendConfig(opts, cfgFile, []string{"sh"}, nil, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -82,7 +82,7 @@ func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, "test.toml") appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -122,7 +122,7 @@ func TestAppendConfigFlatpakExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, "test.toml") appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -163,7 +163,7 @@ func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, "test.toml") appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -203,7 +203,7 @@ func TestAppendConfigBothExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, "test.toml") appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -244,7 +244,7 @@ func TestAppendConfigNeitherExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, "test.toml") appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -281,7 +281,7 @@ func TestAppendConfigNoChecks(t *testing.T) {
opts := &Options{ShellDir: shellDir} opts := &Options{ShellDir: shellDir}
appendConfig(opts, cfgFile, nil, nil, "test.toml") appendConfig(opts, cfgFile, nil, nil, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -312,7 +312,7 @@ func TestAppendConfigFileDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir} opts := &Options{ShellDir: shellDir}
appendConfig(opts, cfgFile, nil, nil, "nonexistent.toml") appendConfig(opts, cfgFile, nil, nil, nil, "nonexistent.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -487,3 +487,90 @@ func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) {
} }
assert.NotContains(t, string(output), "[templates.dmsmango]") assert.NotContains(t, string(output), "[templates.dmsmango]")
} }
func TestAppendConfigConfigDirExists(t *testing.T) {
tempDir := t.TempDir()
shellDir := filepath.Join(tempDir, "shell")
configsDir := filepath.Join(shellDir, "matugen", "configs")
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
testConfig := "vencord config content"
if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte(testConfig), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
configHome := filepath.Join(tempDir, "config")
if err := os.MkdirAll(filepath.Join(configHome, "Vencord"), 0o755); err != nil {
t.Fatalf("failed to create Vencord config dir: %v", err)
}
t.Setenv("XDG_CONFIG_HOME", configHome)
outFile := filepath.Join(tempDir, "output.toml")
cfgFile, err := os.Create(outFile)
if err != nil {
t.Fatalf("failed to create output file: %v", err)
}
defer cfgFile.Close()
mockChecker := mocks_utils.NewMockAppChecker(t)
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
assert.Equal(t, testConfig+"\n", string(output))
}
func TestAppendConfigConfigDirDoesNotExist(t *testing.T) {
tempDir := t.TempDir()
shellDir := filepath.Join(tempDir, "shell")
configsDir := filepath.Join(shellDir, "matugen", "configs")
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte("vencord config content"), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
configHome := filepath.Join(tempDir, "config")
if err := os.MkdirAll(configHome, 0o755); err != nil {
t.Fatalf("failed to create config home: %v", err)
}
t.Setenv("XDG_CONFIG_HOME", configHome)
outFile := filepath.Join(tempDir, "output.toml")
cfgFile, err := os.Create(outFile)
if err != nil {
t.Fatalf("failed to create output file: %v", err)
}
defer cfgFile.Close()
mockChecker := mocks_utils.NewMockAppChecker(t)
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
assert.Empty(t, string(output))
}
+11 -6
View File
@@ -43,10 +43,10 @@ func NewManager() (*Manager, error) {
broker := NewSubscriptionBroker(m.broadcastPairingPrompt) broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
m.promptBroker = broker m.promptBroker = broker
adapter, err := m.findAdapter() adapter, err := findAdapter(conn)
if err != nil { if err != nil {
conn.Close() conn.Close()
return nil, fmt.Errorf("no bluetooth adapter found: %w", err) return nil, err
} }
m.adapterPath = adapter m.adapterPath = adapter
@@ -74,12 +74,12 @@ func NewManager() (*Manager, error) {
return m, nil return m, nil
} }
func (m *Manager) findAdapter() (dbus.ObjectPath, error) { func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) {
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/")) obj := conn.Object(bluezService, dbus.ObjectPath("/"))
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil { if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
return "", err return "", fmt.Errorf("%w: %v", ErrNoAdapter, err)
} }
for path, interfaces := range objects { for path, interfaces := range objects {
@@ -89,7 +89,7 @@ func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
} }
} }
return "", fmt.Errorf("no adapter found") return "", ErrNoAdapter
} }
func (m *Manager) initialize() error { func (m *Manager) initialize() error {
@@ -487,6 +487,11 @@ func (m *Manager) StopDiscovery() error {
} }
func (m *Manager) SetPowered(powered bool) error { func (m *Manager) SetPowered(powered bool) error {
if powered {
if err := rfkillUnblockBluetooth(); err != nil {
log.Debugf("[BluezManager] rfkill unblock failed: %v", err)
}
}
obj := m.dbusConn.Object(bluezService, m.adapterPath) obj := m.dbusConn.Object(bluezService, m.adapterPath)
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
} }
@@ -0,0 +1,5 @@
package bluez
func rfkillUnblockBluetooth() error {
return nil
}
@@ -0,0 +1,19 @@
package bluez
import "os"
// linux/rfkill.h: struct rfkill_event { __u32 idx; __u8 type; __u8 op; __u8 soft; __u8 hard; },
// RFKILL_TYPE_BLUETOOTH=2, RFKILL_OP_CHANGE_ALL=3
func rfkillUnblockBluetooth() error {
f, err := os.OpenFile("/dev/rfkill", os.O_WRONLY, 0)
if err != nil {
return err
}
defer f.Close()
var event [8]byte
event[4] = 2
event[5] = 3
_, err = f.Write(event[:])
return err
}
+46
View File
@@ -0,0 +1,46 @@
package bluez
import (
"errors"
"fmt"
"github.com/godbus/dbus/v5"
)
var ErrNoAdapter = errors.New("no bluetooth adapter found")
func WaitForAdapter() error {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return err
}
defer conn.Close()
if err := conn.AddMatchSignal(
dbus.WithMatchInterface(objectMgrIface),
dbus.WithMatchMember("InterfacesAdded"),
); err != nil {
return err
}
signals := make(chan *dbus.Signal, 64)
conn.Signal(signals)
if _, err := findAdapter(conn); err == nil {
return nil
}
for sig := range signals {
if sig == nil || sig.Name != objectMgrIface+".InterfacesAdded" || len(sig.Body) < 2 {
continue
}
ifaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant)
if !ok {
continue
}
if _, ok := ifaces[adapter1Iface]; ok {
return nil
}
}
return fmt.Errorf("dbus signal stream closed")
}
+23 -10
View File
@@ -229,6 +229,9 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState { func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64) ch := make(chan CUPSState, 64)
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
wasEmpty := true wasEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool { m.subscribers.Range(func(key string, ch chan CUPSState) bool {
wasEmpty = false wasEmpty = false
@@ -237,19 +240,25 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
m.subscribers.Store(id, ch) m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != nil { if !wasEmpty || m.subscription == nil {
if err := m.subscription.Start(); err != nil { return ch
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
} else {
m.eventWG.Add(1)
go m.eventHandler()
}
} }
if err := m.subscription.Start(); err != nil {
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
return ch
}
m.eventWG.Add(1)
go m.eventHandler()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
if val, ok := m.subscribers.LoadAndDelete(id); ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val) close(val)
} }
@@ -260,18 +269,22 @@ func (m *Manager) Unsubscribe(id string) {
return false return false
}) })
if isEmpty && m.subscription != nil { if !isEmpty || m.subscription == nil {
m.subscription.Stop() return
m.eventWG.Wait()
} }
m.subscription.Stop()
m.eventWG.Wait()
} }
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.subLifecycleMu.Lock()
if m.subscription != nil { if m.subscription != nil {
m.subscription.Stop() m.subscription.Stop()
} }
m.subLifecycleMu.Unlock()
m.eventWG.Wait() m.eventWG.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
+64
View File
@@ -1,6 +1,9 @@
package cups package cups
import ( import (
"errors"
"fmt"
"sync"
"testing" "testing"
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
@@ -75,6 +78,67 @@ func TestManager_Subscribe(t *testing.T) {
assert.Equal(t, 0, count) assert.Equal(t, 0, count)
} }
// mirrors the real managers: eventChan guarded by mu, conn/running deliberately
// unsynchronized so overlapping Start/Stop trips the race detector
type stubSubscription struct {
mu sync.Mutex
events chan SubscriptionEvent
conn *int
running bool
}
func (s *stubSubscription) Start() error {
if s.running {
return errors.New("already running")
}
s.running = true
s.mu.Lock()
s.events = make(chan SubscriptionEvent)
s.mu.Unlock()
v := 0
s.conn = &v
*s.conn++
return nil
}
func (s *stubSubscription) Stop() {
if !s.running {
return
}
s.running = false
s.conn = nil
s.mu.Lock()
close(s.events)
s.mu.Unlock()
}
func (s *stubSubscription) Events() <-chan SubscriptionEvent {
s.mu.Lock()
defer s.mu.Unlock()
return s.events
}
func TestManager_SubscribeUnsubscribeRace(t *testing.T) {
m := NewTestManager(mocks_cups.NewMockCUPSClientInterface(t), nil)
m.subscription = &stubSubscription{}
var wg sync.WaitGroup
for i := range 8 {
wg.Go(func() {
id := fmt.Sprintf("client-%d", i)
for range 50 {
m.Subscribe(id)
m.Unsubscribe(id)
}
})
}
wg.Wait()
}
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
+1
View File
@@ -79,6 +79,7 @@ type Manager struct {
client CUPSClientInterface client CUPSClientInterface
pkHelper PkHelper pkHelper PkHelper
subscription SubscriptionManagerInterface subscription SubscriptionManagerInterface
subLifecycleMu sync.Mutex
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan CUPSState] subscribers syncmap.Map[string, chan CUPSState]
stopChan chan struct{} stopChan chan struct{}
+3 -2
View File
@@ -117,11 +117,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
} }
if strings.HasPrefix(req.Method, "cups.") { if strings.HasPrefix(req.Method, "cups.") {
if cupsManager == nil { mgr, err := ensureCupsManager()
if err != nil {
models.RespondError(conn, req.ID, "CUPS manager not initialized") models.RespondError(conn, req.ID, "CUPS manager not initialized")
return return
} }
cups.HandleRequest(conn, req, cupsManager) cups.HandleRequest(conn, req, mgr)
return return
} }
+80 -36
View File
@@ -12,7 +12,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@@ -85,8 +84,8 @@ var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
var capabilitySubscribers syncmap.Map[string, chan ServerInfo] var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
var cupsSubscribers syncmap.Map[string, bool] var cupsMu sync.Mutex
var cupsSubscriberCount atomic.Int32 var cupsSubscriberCount int
func getSocketDir() string { func getSocketDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
@@ -243,17 +242,55 @@ func InitializeAppPickerManager() error {
return nil return nil
} }
func InitializeCupsManager() error { func initializeCupsManagerLocked() (bool, error) {
if cupsManager != nil {
return false, nil
}
manager, err := cups.NewManager() manager, err := cups.NewManager()
if err != nil { if err != nil {
log.Warnf("Failed to initialize cups manager: %v", err) log.Warnf("Failed to initialize cups manager: %v", err)
return err return false, err
} }
cupsManager = manager cupsManager = manager
log.Info("CUPS manager initialized") log.Info("CUPS manager initialized")
return nil return true, nil
}
func ensureCupsManager() (*cups.Manager, error) {
cupsMu.Lock()
created, err := initializeCupsManagerLocked()
mgr := cupsManager
cupsMu.Unlock()
if err != nil {
return nil, err
}
if created {
notifyCapabilityChange()
}
return mgr, nil
}
func cupsAvailable() bool {
cupsMu.Lock()
defer cupsMu.Unlock()
return cupsManager != nil
}
func releaseCupsSubscriber() {
cupsMu.Lock()
cupsSubscriberCount--
var mgr *cups.Manager
if cupsSubscriberCount == 0 && cupsManager != nil {
mgr = cupsManager
cupsManager = nil
}
cupsMu.Unlock()
if mgr == nil {
return
}
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
mgr.Close()
notifyCapabilityChange()
} }
func InitializeBrightnessManager() error { func InitializeBrightnessManager() error {
@@ -463,7 +500,7 @@ func getCapabilities() Capabilities {
caps = append(caps, "browser") caps = append(caps, "browser")
} }
if cupsManager != nil { if cupsAvailable() {
caps = append(caps, "cups") caps = append(caps, "cups")
} }
@@ -533,7 +570,7 @@ func getServerInfo() ServerInfo {
caps = append(caps, "browser") caps = append(caps, "browser")
} }
if cupsManager != nil { if cupsAvailable() {
caps = append(caps, "cups") caps = append(caps, "cups")
} }
@@ -997,38 +1034,31 @@ func handleSubscribe(conn net.Conn, req models.Request) {
} }
if shouldSubscribe("cups") { if shouldSubscribe("cups") {
cupsSubscribers.Store(clientID+"-cups", true) cupsMu.Lock()
count := cupsSubscriberCount.Add(1) cupsSubscriberCount++
created, err := initializeCupsManagerLocked()
mgr := cupsManager
cupsMu.Unlock()
if count == 1 { if err != nil {
if err := InitializeCupsManager(); err != nil { log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err) } else if created {
} else { notifyCapabilityChange()
notifyCapabilityChange()
}
} }
if cupsManager != nil { if mgr == nil {
releaseCupsSubscriber()
} else {
wg.Add(1) wg.Add(1)
cupsChan := cupsManager.Subscribe(clientID + "-cups") cupsChan := mgr.Subscribe(clientID + "-cups")
go func() { go func() {
defer wg.Done() defer wg.Done()
defer func() { defer func() {
cupsManager.Unsubscribe(clientID + "-cups") mgr.Unsubscribe(clientID + "-cups")
cupsSubscribers.Delete(clientID + "-cups") releaseCupsSubscriber()
count := cupsSubscriberCount.Add(-1)
if count == 0 {
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
if cupsManager != nil {
cupsManager.Close()
cupsManager = nil
notifyCapabilityChange()
}
}
}() }()
initialState := cupsManager.GetState() initialState := mgr.GetState()
select { select {
case eventChan <- ServiceEvent{Service: "cups", Data: initialState}: case eventChan <- ServiceEvent{Service: "cups", Data: initialState}:
case <-stopChan: case <-stopChan:
@@ -1338,9 +1368,12 @@ func cleanupManagers() {
if appPickerManager != nil { if appPickerManager != nil {
appPickerManager.Close() appPickerManager.Close()
} }
cupsMu.Lock()
if cupsManager != nil { if cupsManager != nil {
cupsManager.Close() cupsManager.Close()
cupsManager = nil
} }
cupsMu.Unlock()
if brightnessManager != nil { if brightnessManager != nil {
brightnessManager.Close() brightnessManager.Close()
} }
@@ -1698,10 +1731,21 @@ func Start(printDocs bool) error {
}() }()
go func() { go func() {
if err := InitializeBluezManager(); err != nil { for {
err := InitializeBluezManager()
if err == nil {
notifyCapabilityChange()
return
}
log.Warnf("Bluez manager unavailable: %v", err) log.Warnf("Bluez manager unavailable: %v", err)
} else { if !errors.Is(err, bluez.ErrNoAdapter) {
notifyCapabilityChange() return
}
if err := bluez.WaitForAdapter(); err != nil {
log.Warnf("Bluetooth adapter watch failed: %v", err)
return
}
log.Info("Bluetooth adapter appeared, initializing bluez manager")
} }
}() }()
+2 -1
View File
@@ -143,7 +143,8 @@ func wrapInTerminal(term, title, shellCmd string, extraArgs []string) []string {
case "konsole": case "konsole":
argv = []string{term, "-p", "tabtitle=" + title} argv = []string{term, "-p", "tabtitle=" + title}
case "gnome-terminal": case "gnome-terminal":
argv = []string{term, "--title=" + title} // --wait: the factory process otherwise returns immediately
argv = []string{term, "--wait", "--title=" + title}
execFlag = "--" execFlag = "--"
default: default:
argv = []string{term} argv = []string{term}
+14 -9
View File
@@ -412,25 +412,30 @@ func (m *Manager) runCustomUpgrade(ctx context.Context, opts UpgradeOptions) {
onLine := func(line string) { m.appendLog(line) } onLine := func(line string) { m.appendLog(line) }
argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs) argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs)
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil { if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
code := ErrCodeBackendFailed
switch { switch {
case errors.Is(ctx.Err(), context.DeadlineExceeded): case errors.Is(ctx.Err(), context.DeadlineExceeded):
code = ErrCodeTimeout m.failCustomUpgrade(ErrCodeTimeout, err)
return
case errors.Is(ctx.Err(), context.Canceled): case errors.Is(ctx.Err(), context.Canceled):
code = ErrCodeCancelled m.failCustomUpgrade(ErrCodeCancelled, err)
return
} }
m.mu.Lock() // exit status reflects the trailing `read`, not the update command
m.state.Phase = PhaseError m.appendLog(fmt.Sprintf("Terminal exited early: %v", err))
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
m.mu.Unlock()
m.markDirty()
return
} }
m.finishSuccessfulUpgrade(false) m.finishSuccessfulUpgrade(false)
m.runRefresh(context.Background(), false) m.runRefresh(context.Background(), false)
} }
func (m *Manager) failCustomUpgrade(code ErrorCode, err error) {
m.mu.Lock()
m.state.Phase = PhaseError
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
m.mu.Unlock()
m.markDirty()
}
func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) { func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
m.appendLog("Upgrade complete.") m.appendLog("Upgrade complete.")
@@ -233,3 +233,27 @@ func TestUpgradeBackendsFiltersFlatpakOnly(t *testing.T) {
t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got) t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got)
} }
} }
func TestWrapInTerminal(t *testing.T) {
tests := []struct {
term string
wantPrefix []string
}{
{"kitty", []string{"kitty", "--class", "com.danklinux.dms", "-T", "Title"}},
{"gnome-terminal", []string{"gnome-terminal", "--wait", "--title=Title"}},
{"foot", []string{"foot", "--app-id=com.danklinux.dms", "--title=Title"}},
}
for _, tt := range tests {
got := wrapInTerminal(tt.term, "Title", "echo hi", nil)
if len(got) < len(tt.wantPrefix) || !reflect.DeepEqual(got[:len(tt.wantPrefix)], tt.wantPrefix) {
t.Errorf("wrapInTerminal(%q) = %#v, want prefix %#v", tt.term, got, tt.wantPrefix)
}
tail := got[len(got)-3:]
if tail[0] != "sh" || tail[1] != "-c" {
t.Errorf("wrapInTerminal(%q) tail = %#v, want [sh -c <cmd>]", tt.term, tail)
}
if !strings.Contains(tail[2], "echo hi") {
t.Errorf("wrapInTerminal(%q) command %q does not contain shell command", tt.term, tail[2])
}
}
}
@@ -928,8 +928,8 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
if a.Opacity != nil { if a.Opacity != nil {
lines = append(lines, fmt.Sprintf(" opacity %.2f", *a.Opacity)) lines = append(lines, fmt.Sprintf(" opacity %.2f", *a.Opacity))
} }
if a.OpenFloating != nil && *a.OpenFloating { if a.OpenFloating != nil {
lines = append(lines, " open-floating true") lines = append(lines, fmt.Sprintf(" open-floating %t", *a.OpenFloating))
} }
if a.OpenMaximized != nil && *a.OpenMaximized { if a.OpenMaximized != nil && *a.OpenMaximized {
lines = append(lines, " open-maximized true") lines = append(lines, " open-maximized true")
+64 -6
View File
@@ -11,7 +11,7 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("CacheData") readonly property var log: Log.scoped("CacheData")
readonly property int cacheConfigVersion: 1 readonly property int cacheConfigVersion: 2
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -19,10 +19,21 @@ Singleton {
readonly property string _stateDir: Paths.strip(_stateUrl) readonly property string _stateDir: Paths.strip(_stateUrl)
property bool _loading: false property bool _loading: false
property bool _hasLoaded: false
property int _loadedCacheVersion: 0
readonly property var _pinKeys: ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"]
readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings"].concat(_pinKeys)
property string wallpaperLastPath: "" property string wallpaperLastPath: ""
property string profileLastPath: "" property string profileLastPath: ""
property var brightnessDevicePins: ({})
property var wifiNetworkPins: ({})
property var bluetoothDevicePins: ({})
property var audioInputDevicePins: ({})
property var audioOutputDevicePins: ({})
property var fileBrowserSettings: ({ property var fileBrowserSettings: ({
"wallpaper": { "wallpaper": {
"lastPath": "", "lastPath": "",
@@ -82,8 +93,46 @@ Singleton {
function loadCache() { function loadCache() {
_loading = true; _loading = true;
parseCache(cacheFile.text()); try {
_loading = false; parseCache(cacheFile.text());
} finally {
_loading = false;
_hasLoaded = true;
}
}
function set(key, value) {
if (_dataKeys.indexOf(key) < 0) {
log.warn("Unknown cache key:", key);
return;
}
root[key] = value;
saveCache();
}
function migratePins(pins) {
if (!pins)
return;
if (!_hasLoaded)
loadCache();
if (_loadedCacheVersion >= cacheConfigVersion)
return;
let migrated = false;
for (const key of _pinKeys) {
const legacy = pins[key];
if (!legacy || Object.keys(legacy).length === 0)
continue;
if (Object.keys(root[key] || {}).length > 0)
continue;
root[key] = legacy;
migrated = true;
}
if (!migrated)
return;
log.info("Migrated device pins from settings.json");
saveCache();
} }
function parseCache(content) { function parseCache(content) {
@@ -91,6 +140,7 @@ Singleton {
try { try {
if (content && content.trim()) { if (content && content.trim()) {
const cache = JSON.parse(content); const cache = JSON.parse(content);
_loadedCacheVersion = cache.configVersion || 0;
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""; wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "";
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""; profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : "";
@@ -126,6 +176,10 @@ Singleton {
}; };
} }
for (const key of _pinKeys) {
root[key] = cache[key] !== undefined ? cache[key] : {};
}
if (cache.configVersion === undefined) { if (cache.configVersion === undefined) {
migrateFromUndefinedToV1(cache); migrateFromUndefinedToV1(cache);
cleanupUnusedKeys(); cleanupUnusedKeys();
@@ -142,12 +196,16 @@ Singleton {
function saveCache() { function saveCache() {
if (_loading) if (_loading)
return; return;
cacheFile.setText(JSON.stringify({ const data = {
"wallpaperLastPath": wallpaperLastPath, "wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath, "profileLastPath": profileLastPath,
"fileBrowserSettings": fileBrowserSettings, "fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion "configVersion": cacheConfigVersion
}, null, 2)); };
for (const key of _pinKeys) {
data[key] = root[key];
}
cacheFile.setText(JSON.stringify(data, null, 2));
} }
function migrateFromUndefinedToV1(cache) { function migrateFromUndefinedToV1(cache) {
@@ -155,7 +213,7 @@ Singleton {
} }
function cleanupUnusedKeys() { function cleanupUnusedKeys() {
const validKeys = ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings", "configVersion"]; const validKeys = _dataKeys.concat(["configVersion"]);
try { try {
const content = cacheFile.text(); const content = cacheFile.text();
+21 -2
View File
@@ -65,16 +65,35 @@ Singleton {
function _openPopout(popout) { function _openPopout(popout) {
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing) let flagStayedTrue = popout.dashVisible === true;
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing) {
popout.dashVisible = false; popout.dashVisible = false;
flagStayedTrue = false;
}
popout.dashVisible = true; popout.dashVisible = true;
// Flag already true (e.g. retargeting to another monitor) won't re-fire
// the change handler, so drive the surface open explicitly.
if (flagStayedTrue)
_ensureSurfaceOpen(popout);
return; return;
} }
if (popout.notificationHistoryVisible !== undefined) { if (popout.notificationHistoryVisible !== undefined) {
const flagStayedTrue = popout.notificationHistoryVisible === true;
popout.notificationHistoryVisible = true; popout.notificationHistoryVisible = true;
if (flagStayedTrue)
_ensureSurfaceOpen(popout);
return; return;
} }
popout.open(); _ensureSurfaceOpen(popout);
}
function _ensureSurfaceOpen(popout) {
if (typeof popout.present === "function") {
popout.present();
return;
}
if (typeof popout.open === "function")
popout.open();
} }
function _closePopout(popout) { function _closePopout(popout) {
+4 -6
View File
@@ -15,7 +15,7 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("SettingsData") readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 12 readonly property int settingsConfigVersion: 13
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -773,11 +773,6 @@ Singleton {
property bool fadeToDpmsEnabled: true property bool fadeToDpmsEnabled: true
property int fadeToDpmsGracePeriod: 5 property int fadeToDpmsGracePeriod: 5
property string launchPrefix: "" property string launchPrefix: ""
property var brightnessDevicePins: ({})
property var wifiNetworkPins: ({})
property var bluetoothDevicePins: ({})
property var audioInputDevicePins: ({})
property var audioOutputDevicePins: ({})
property bool gtkThemingEnabled: false property bool gtkThemingEnabled: false
property bool qtThemingEnabled: false property bool qtThemingEnabled: false
@@ -1741,6 +1736,7 @@ Singleton {
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null; let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
const oldVersion = obj?.configVersion ?? 0; const oldVersion = obj?.configVersion ?? 0;
const legacyPins = oldVersion < 13 ? Store.extractPins(obj) : null;
if (oldVersion < settingsConfigVersion) { if (oldVersion < settingsConfigVersion) {
const migrated = Store.migrateToVersion(obj, settingsConfigVersion); const migrated = Store.migrateToVersion(obj, settingsConfigVersion);
if (migrated) { if (migrated) {
@@ -1748,6 +1744,8 @@ Singleton {
obj = migrated; obj = migrated;
} }
} }
if (legacyPins)
Qt.callLater(() => CacheData.migratePins(legacyPins));
if (obj?.lockScreenActiveMonitor !== undefined) { if (obj?.lockScreenActiveMonitor !== undefined) {
var oldVal = obj.lockScreenActiveMonitor; var oldVal = obj.lockScreenActiveMonitor;
@@ -354,11 +354,6 @@ var SPEC = {
fadeToDpmsEnabled: { def: true }, fadeToDpmsEnabled: { def: true },
fadeToDpmsGracePeriod: { def: 5 }, fadeToDpmsGracePeriod: { def: 5 },
launchPrefix: { def: "" }, launchPrefix: { def: "" },
brightnessDevicePins: { def: {} },
wifiNetworkPins: { def: {} },
bluetoothDevicePins: { def: {} },
audioInputDevicePins: { def: {} },
audioOutputDevicePins: { def: {} },
gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" }, gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" },
qtThemingEnabled: { def: false, onChange: "regenSystemThemes" }, qtThemingEnabled: { def: false, onChange: "regenSystemThemes" },
@@ -2,6 +2,21 @@
.import "./SettingsSpec.js" as SpecModule .import "./SettingsSpec.js" as SpecModule
var PIN_KEYS = ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"];
function extractPins(obj) {
if (!obj) return null;
var pins = null;
for (var i = 0; i < PIN_KEYS.length; i++) {
var value = obj[PIN_KEYS[i]];
if (!value || Object.keys(value).length === 0) continue;
if (!pins) pins = {};
pins[PIN_KEYS[i]] = value;
}
return pins;
}
function parse(root, jsonObj) { function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC; var SPEC = SpecModule.SPEC;
@@ -263,6 +278,17 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 12; settings.configVersion = 12;
} }
if (currentVersion < 13) {
console.info("Migrating settings from version", currentVersion, "to version 13");
console.info("Moving device and network pins to cache.json");
for (var p = 0; p < PIN_KEYS.length; p++) {
delete settings[PIN_KEYS[p]];
}
settings.configVersion = 13;
}
return settings; return settings;
} }
+8
View File
@@ -139,6 +139,14 @@ Item {
return; return;
fadeDpmsWindowLoader.loadedWindow.cancelFade(); fadeDpmsWindowLoader.loadedWindow.cancelFade();
} }
function onMonitorsOffChanged() {
if (IdleService.monitorsOff)
return;
if (!fadeDpmsWindowLoader.loadedWindow)
return;
fadeDpmsWindowLoader.loadedWindow.dismiss();
}
} }
} }
} }
@@ -449,7 +449,12 @@ Item {
searchQuery = query; searchQuery = query;
requestSearch(); requestSearch();
if (searchMode !== "plugins" && query.startsWith("/")) { if (autoSwitchedToFiles && !query.startsWith("/")) {
restorePreviousMode();
return;
}
if (query.startsWith("/")) {
var prefix = Utils.parseFileSearchPrefix(query); var prefix = Utils.parseFileSearchPrefix(query);
var explicitType = prefix && prefix.type !== null ? prefix.type : null; var explicitType = prefix && prefix.type !== null ? prefix.type : null;
var targetType = explicitType !== null ? explicitType : (SessionData.launcherLastFileSearchType || "all"); var targetType = explicitType !== null ? explicitType : (SessionData.launcherLastFileSearchType || "all");
@@ -174,9 +174,12 @@ Item {
spotlightContent.controller.selectedFlatIndex = 0; spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null; spotlightContent.controller.selectedItem = null;
spotlightContent.controller.historyIndex = -1; spotlightContent.controller.historyIndex = -1;
spotlightContent.controller.searchQuery = targetQuery; if (targetQuery) {
spotlightContent.controller.setSearchQuery(targetQuery);
spotlightContent.controller.performSearch(); } else {
spotlightContent.controller.searchQuery = "";
spotlightContent.controller.performSearch();
}
} }
if (spotlightContent.resetScroll) { if (spotlightContent.resetScroll) {
spotlightContent.resetScroll(); spotlightContent.resetScroll();
@@ -271,7 +274,7 @@ Item {
target: spotlightContent?.controller ?? null target: spotlightContent?.controller ?? null
function onModeChanged(mode, userInitiated) { function onModeChanged(mode, userInitiated) {
if (!userInitiated || !SettingsData.rememberLastMode || (mode !== "all" && mode !== "apps")) if (!userInitiated || !SettingsData.rememberLastMode)
return; return;
SessionData.setLauncherLastMode(mode); SessionData.setLauncherLastMode(mode);
} }
+61 -28
View File
@@ -27,7 +27,7 @@ DankModal {
property real holdProgress: 0 property real holdProgress: 0
property bool showHoldHint: false property bool showHoldHint: false
property bool holdFromKeyboard: false property bool holdFromKeyboard: false
property string pendingKeyAction: "" property string committedAction: ""
readonly property bool needsConfirmation: SettingsData.powerActionConfirm readonly property bool needsConfirmation: SettingsData.powerActionConfirm
readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000 readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000
@@ -39,10 +39,16 @@ DankModal {
return action !== "lock" && action !== "restart"; return action !== "lock" && action !== "restart";
} }
function actionWakesOnKeyRelease(action) {
return action === "suspend" || action === "hibernate";
}
function startHold(action, actionIndex) { function startHold(action, actionIndex) {
if (committedAction !== "")
return;
if (!needsConfirmation || !actionNeedsConfirm(action)) { if (!needsConfirmation || !actionNeedsConfirm(action)) {
if (holdFromKeyboard) { if (holdFromKeyboard && actionWakesOnKeyRelease(action)) {
pendingKeyAction = action; commitAction(action, actionIndex);
return; return;
} }
executeAction(action); executeAction(action);
@@ -56,7 +62,6 @@ DankModal {
} }
function cancelHold() { function cancelHold() {
pendingKeyAction = "";
if (holdAction === "") if (holdAction === "")
return; return;
const wasHolding = holdProgress > 0; const wasHolding = holdProgress > 0;
@@ -76,8 +81,8 @@ DankModal {
return; return;
} }
holdTimer.stop(); holdTimer.stop();
if (holdFromKeyboard) { if (holdFromKeyboard && actionWakesOnKeyRelease(holdAction)) {
pendingKeyAction = holdAction; commitAction(holdAction, holdActionIndex);
return; return;
} }
const action = holdAction; const action = holdAction;
@@ -87,6 +92,25 @@ DankModal {
executeAction(action); executeAction(action);
} }
function commitAction(action, actionIndex) {
holdTimer.stop();
holdAction = "";
holdActionIndex = actionIndex;
committedAction = action;
commitFallbackTimer.restart();
}
function executeCommittedAction() {
if (committedAction === "")
return;
commitFallbackTimer.stop();
const action = committedAction;
committedAction = "";
holdActionIndex = -1;
holdProgress = 0;
executeAction(action);
}
signal switchUserRequested signal switchUserRequested
function executeAction(action) { function executeAction(action) {
@@ -128,6 +152,12 @@ DankModal {
onTriggered: root.showHoldHint = false onTriggered: root.showHoldHint = false
} }
Timer {
id: commitFallbackTimer
interval: 5000
onTriggered: root.executeCommittedAction()
}
function openCentered() { function openCentered() {
parentBounds = Qt.rect(0, 0, 0, 0); parentBounds = Qt.rect(0, 0, 0, 0);
parentScreen = null; parentScreen = null;
@@ -286,7 +316,8 @@ DankModal {
holdProgress = 0; holdProgress = 0;
showHoldHint = false; showHoldHint = false;
holdFromKeyboard = false; holdFromKeyboard = false;
pendingKeyAction = ""; committedAction = "";
commitFallbackTimer.stop();
updateVisibleActions(); updateVisibleActions();
const defaultIndex = getDefaultActionIndex(); const defaultIndex = getDefaultActionIndex();
selectedIndex = defaultIndex; selectedIndex = defaultIndex;
@@ -309,6 +340,10 @@ DankModal {
event.accepted = true; event.accepted = true;
return; return;
} }
if (committedAction !== "") {
event.accepted = true;
return;
}
holdFromKeyboard = true; holdFromKeyboard = true;
if (SettingsData.powerMenuGridLayout) { if (SettingsData.powerMenuGridLayout) {
handleGridNavigation(event, true); handleGridNavigation(event, true);
@@ -321,6 +356,11 @@ DankModal {
event.accepted = true; event.accepted = true;
return; return;
} }
if (committedAction !== "") {
event.accepted = true;
executeCommittedAction();
return;
}
if (SettingsData.powerMenuGridLayout) { if (SettingsData.powerMenuGridLayout) {
handleGridNavigation(event, false); handleGridNavigation(event, false);
} else { } else {
@@ -331,16 +371,7 @@ DankModal {
function handleListNavigation(event, isPressed) { function handleListNavigation(event, isPressed) {
if (!isPressed) { if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
if (pendingKeyAction !== "") { cancelHold();
const action = pendingKeyAction;
pendingKeyAction = "";
holdAction = "";
holdActionIndex = -1;
holdProgress = 0;
executeAction(action);
} else {
cancelHold();
}
event.accepted = true; event.accepted = true;
} }
return; return;
@@ -434,16 +465,7 @@ DankModal {
function handleGridNavigation(event, isPressed) { function handleGridNavigation(event, isPressed) {
if (!isPressed) { if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
if (pendingKeyAction !== "") { cancelHold();
const action = pendingKeyAction;
pendingKeyAction = "";
holdAction = "";
holdActionIndex = -1;
holdProgress = 0;
executeAction(action);
} else {
cancelHold();
}
event.accepted = true; event.accepted = true;
} }
return; return;
@@ -812,6 +834,7 @@ DankModal {
Row { Row {
id: hintRow id: hintRow
readonly property bool selectedNeedsHold: root.actionNeedsConfirm(root.getActionAtIndex(root.selectedIndex))
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: Theme.spacingS
@@ -826,7 +849,13 @@ DankModal {
} }
DankIcon { DankIcon {
name: root.showHoldHint ? "warning" : "touch_app" name: {
if (root.showHoldHint)
return "warning";
if (!hintRow.selectedNeedsHold)
return "bolt";
return "touch_app";
}
size: Theme.fontSizeSmall size: Theme.fontSizeSmall
color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -836,8 +865,12 @@ DankModal {
readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000 readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress)) readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
text: { text: {
if (root.committedAction !== "")
return I18n.tr("Release to confirm");
if (root.showHoldHint) if (root.showHoldHint)
return I18n.tr("Hold longer to confirm"); return I18n.tr("Hold longer to confirm");
if (!hintRow.selectedNeedsHold)
return I18n.tr("Activates immediately");
if (root.holdProgress > 0) { if (root.holdProgress > 0) {
if (totalMs < 1000) if (totalMs < 1000)
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs); return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
+11 -5
View File
@@ -41,6 +41,9 @@ FloatingWindow {
signal closingModal signal closingModal
function show() { function show() {
if (visible && !backingWindowVisible) {
visible = false;
}
visible = true; visible = true;
} }
@@ -49,7 +52,11 @@ FloatingWindow {
} }
function toggle() { function toggle() {
visible = !visible; if (visible && backingWindowVisible) {
hide();
return;
}
show();
} }
function setTabIndex(tabIndex: int) { function setTabIndex(tabIndex: int) {
@@ -61,13 +68,12 @@ FloatingWindow {
function showWithTab(tabIndex: int) { function showWithTab(tabIndex: int) {
setTabIndex(tabIndex); setTabIndex(tabIndex);
visible = true; show();
} }
function showWithTabName(tabName: string) { function showWithTabName(tabName: string) {
var idx = sidebar.resolveTabIndex(tabName); setTabIndex(sidebar.resolveTabIndex(tabName));
setTabIndex(idx); show();
visible = true;
} }
function resolveTabIndex(tabName: string): int { function resolveTabIndex(tabName: string): int {
+17 -1
View File
@@ -52,6 +52,7 @@ FloatingWindow {
condInitialised.triState = 0; condInitialised.triState = 0;
opacityEnabled.checked = false; opacityEnabled.checked = false;
opacitySlider.value = 100; opacitySlider.value = 100;
floatingCond.triState = 0;
floatingToggle.checked = false; floatingToggle.checked = false;
maximizedToggle.checked = false; maximizedToggle.checked = false;
maximizedToEdgesToggle.checked = false; maximizedToEdgesToggle.checked = false;
@@ -164,6 +165,7 @@ FloatingWindow {
opacityEnabled.checked = hasOpacity; opacityEnabled.checked = hasOpacity;
opacitySlider.value = hasOpacity ? Math.round(actions.opacity * 100) : 100; opacitySlider.value = hasOpacity ? Math.round(actions.opacity * 100) : 100;
floatingCond.triState = triFromBool(actions.openFloating);
floatingToggle.checked = actions.openFloating || false; floatingToggle.checked = actions.openFloating || false;
maximizedToggle.checked = actions.openMaximized || false; maximizedToggle.checked = actions.openMaximized || false;
maximizedToEdgesToggle.checked = actions.openMaximizedToEdges || false; maximizedToEdgesToggle.checked = actions.openMaximizedToEdges || false;
@@ -318,7 +320,9 @@ FloatingWindow {
if (opacityEnabled.checked) if (opacityEnabled.checked)
actions.opacity = opacitySlider.value / 100; actions.opacity = opacitySlider.value / 100;
if (floatingToggle.checked) if (isNiri)
applyCond(actions, "openFloating", floatingCond.triState);
else if (floatingToggle.checked)
actions.openFloating = true; actions.openFloating = true;
if (maximizedToggle.checked) if (maximizedToggle.checked)
actions.openMaximized = true; actions.openMaximized = true;
@@ -943,6 +947,17 @@ FloatingWindow {
title: I18n.tr("Window Opening") title: I18n.tr("Window Opening")
} }
Flow {
width: parent.width
spacing: Theme.spacingS
visible: isNiri
MatchCond {
id: floatingCond
label: I18n.tr("Float")
}
}
Flow { Flow {
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
@@ -950,6 +965,7 @@ FloatingWindow {
CheckboxRow { CheckboxRow {
id: floatingToggle id: floatingToggle
label: I18n.tr("Float") label: I18n.tr("Float")
visible: !isNiri
} }
CheckboxRow { CheckboxRow {
id: maximizedToggle id: maximizedToggle
@@ -77,6 +77,12 @@ DankPopout {
readonly property color _containerBg: Theme.nestedSurface readonly property color _containerBg: Theme.nestedSurface
// Defer open one tick so screen-change geometry settles before the surface
// maps; a synchronous open churns the surface and loses the blur on a switch.
function present() {
Qt.callLater(open);
}
function openWithSection(section) { function openWithSection(section) {
StateUtils.openWithSection(root, section); StateUtils.openWithSection(root, section);
} }
@@ -159,7 +159,7 @@ Rectangle {
} }
function getPinnedInputs() { function getPinnedInputs() {
const pins = SettingsData.audioInputDevicePins || {}; const pins = CacheData.audioInputDevicePins || {};
return normalizePinList(pins["preferredInput"]); return normalizePinList(pins["preferredInput"]);
} }
@@ -315,7 +315,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.audioInputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredInput"]); let pinnedList = audioContent.normalizePinList(pins["preferredInput"]);
const pinIndex = pinnedList.indexOf(modelData.name); const pinIndex = pinnedList.indexOf(modelData.name);
@@ -332,7 +332,7 @@ Rectangle {
else else
delete pins["preferredInput"]; delete pins["preferredInput"];
SettingsData.set("audioInputDevicePins", pins); CacheData.set("audioInputDevicePins", pins);
} }
} }
} }
@@ -169,7 +169,7 @@ Rectangle {
} }
function getPinnedOutputs() { function getPinnedOutputs() {
const pins = SettingsData.audioOutputDevicePins || {}; const pins = CacheData.audioOutputDevicePins || {};
return normalizePinList(pins["preferredOutput"]); return normalizePinList(pins["preferredOutput"]);
} }
@@ -324,7 +324,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.audioOutputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]); let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]);
const pinIndex = pinnedList.indexOf(modelData.name); const pinIndex = pinnedList.indexOf(modelData.name);
@@ -341,7 +341,7 @@ Rectangle {
else else
delete pins["preferredOutput"]; delete pins["preferredOutput"];
SettingsData.set("audioOutputDevicePins", pins); CacheData.set("audioOutputDevicePins", pins);
} }
} }
} }
@@ -112,7 +112,7 @@ Rectangle {
} }
function getPinnedDevices() { function getPinnedDevices() {
const pins = SettingsData.bluetoothDevicePins || {}; const pins = CacheData.bluetoothDevicePins || {};
return normalizePinList(pins["preferredDevice"]); return normalizePinList(pins["preferredDevice"]);
} }
@@ -400,7 +400,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.bluetoothDevicePins || {}));
let pinnedList = root.normalizePinList(pins["preferredDevice"]); let pinnedList = root.normalizePinList(pins["preferredDevice"]);
const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address); const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address);
@@ -418,7 +418,7 @@ Rectangle {
delete pins["preferredDevice"]; delete pins["preferredDevice"];
} }
SettingsData.set("bluetoothDevicePins", pins); CacheData.set("bluetoothDevicePins", pins);
} }
} }
} }
@@ -37,7 +37,7 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (pinKey.length > 0) { if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey]; const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = devices.find(d => d.name === pinnedDevice); const found = devices.find(d => d.name === pinnedDevice);
@@ -85,12 +85,12 @@ Rectangle {
} }
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (pinKey.length > 0) { if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const existing = pins[pinKey]; const existing = pins[pinKey];
if (existing && existing !== deviceName) { if (existing && existing !== deviceName) {
const next = JSON.parse(JSON.stringify(pins)); const next = JSON.parse(JSON.stringify(pins));
delete next[pinKey]; delete next[pinKey];
SettingsData.set("brightnessDevicePins", next); CacheData.set("brightnessDevicePins", next);
} }
} }
root.currentDeviceName = deviceName; root.currentDeviceName = deviceName;
@@ -106,7 +106,7 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey || !deviceName) if (!pinKey || !deviceName)
return false; return false;
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
return pins[pinKey] === deviceName; return pins[pinKey] === deviceName;
} }
@@ -114,13 +114,13 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey || !deviceName) if (!pinKey || !deviceName)
return; return;
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {}));
if (pins[pinKey] === deviceName) { if (pins[pinKey] === deviceName) {
delete pins[pinKey]; delete pins[pinKey];
} else { } else {
pins[pinKey] = deviceName; pins[pinKey] = deviceName;
} }
SettingsData.set("brightnessDevicePins", pins); CacheData.set("brightnessDevicePins", pins);
} }
implicitHeight: { implicitHeight: {
@@ -269,7 +269,7 @@ Rectangle {
readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName) readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName)
readonly property bool devicePinnedHere: { readonly property bool devicePinnedHere: {
SettingsData.brightnessDevicePins; CacheData.brightnessDevicePins;
return root.isDevicePinnedToScreen(modelData ? modelData.name : ""); return root.isDevicePinnedToScreen(modelData ? modelData.name : "");
} }
@@ -49,7 +49,7 @@ Rectangle {
} }
function getPinnedNetworks() { function getPinnedNetworks() {
const pins = SettingsData.wifiNetworkPins || {}; const pins = CacheData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]); return normalizePinList(pins["preferredWifi"]);
} }
@@ -710,7 +710,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
let pinnedList = root.normalizePinList(pins["preferredWifi"]); let pinnedList = root.normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(modelData.ssid); const pinIndex = pinnedList.indexOf(modelData.ssid);
@@ -727,7 +727,7 @@ Rectangle {
else else
delete pins["preferredWifi"]; delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins); CacheData.set("wifiNetworkPins", pins);
} }
} }
} }
@@ -34,7 +34,7 @@ Row {
if (screenName && screenName.length > 0) { if (screenName && screenName.length > 0) {
const screen = Quickshell.screens.find(s => s.name === screenName); const screen = Quickshell.screens.find(s => s.name === screenName);
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName; const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey]; const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice); const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
@@ -29,6 +29,8 @@ Item {
property var centerWidgets: [] property var centerWidgets: []
property int totalWidgets: 0 property int totalWidgets: 0
property real totalSize: 0 property real totalSize: 0
property real contentStart: 0
property real contentSize: 0
function updateLayout() { function updateLayout() {
if (SettingsData.centeringMode === "geometric") { if (SettingsData.centeringMode === "geometric") {
@@ -36,6 +38,25 @@ Item {
} else { } else {
applyIndexLayout(); applyIndexLayout();
} }
updateContentExtent();
}
function updateContentExtent() {
if (centerWidgets.length === 0) {
contentStart = 0;
contentSize = 0;
return;
}
let start = Infinity;
let end = -Infinity;
for (const widget of centerWidgets) {
const pos = isVertical ? widget.y : widget.x;
const size = isVertical ? widget.height : widget.width;
start = Math.min(start, pos);
end = Math.max(end, pos + size);
}
contentStart = start;
contentSize = end - start;
} }
function applyGeometricLayout() { function applyGeometricLayout() {
+18 -4
View File
@@ -833,16 +833,30 @@ PanelWindow {
const pos = section.mapToItem(barWindow.contentItem, 0, 0); const pos = section.mapToItem(barWindow.contentItem, 0, 0);
const implW = section.implicitWidth || 0; const implW = section.implicitWidth || 0;
const implH = section.implicitHeight || 0; const implH = section.implicitHeight || 0;
const contentSize = isCenter ? (section.contentSize || 0) : 0;
const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0; let offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0;
const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0); let offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0);
let w = implW;
let h = implH;
// index centering lays content out asymmetrically; use the real extent
if (contentSize > 0) {
if (barWindow.isVertical) {
offsetY = section.contentStart;
h = contentSize;
} else {
offsetX = section.contentStart;
w = contentSize;
}
}
const edgePad = 2; const edgePad = 2;
return { return {
"x": pos.x + offsetX - edgePad, "x": pos.x + offsetX - edgePad,
"y": pos.y + offsetY - edgePad, "y": pos.y + offsetY - edgePad,
"w": implW + edgePad * 2, "w": w + edgePad * 2,
"h": implH + edgePad * 2 "h": h + edgePad * 2
}; };
} }
@@ -63,8 +63,8 @@ BasePill {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => root.triggerRipple(this, mouse.x, mouse.y) onPressed: function (mouse) {
onClicked: function (mouse) { root.triggerRipple(this, mouse.x, mouse.y);
switch (mouse.button) { switch (mouse.button) {
case Qt.RightButton: case Qt.RightButton:
openContextMenu(); openContextMenu();
@@ -203,7 +203,7 @@ BasePill {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey) if (!pinKey)
return ""; return "";
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
return pins[pinKey] || ""; return pins[pinKey] || "";
} }
@@ -35,44 +35,42 @@ BasePill {
} }
} }
onRightClicked: (rx, ry) => { onRightClicked: {
const screen = root.parentScreen || Screen; const screen = root.parentScreen || Screen;
if (!screen) if (!screen)
return; return;
const globalPos = root.visualContent.mapToItem(null, 0, 0);
const isVertical = root.axis?.isVertical ?? false; const isVertical = root.axis?.isVertical ?? false;
const edge = root.axis?.edge ?? "top"; const edge = root.axis?.edge ?? "top";
const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS); const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS);
const barOffset = root.barThickness + root.barSpacing + gap; const barOffset = root.barThickness + root.barSpacing + gap;
const localPos = root.visualContent.mapToItem(null, root.visualContent.width / 2, root.visualContent.height / 2);
let anchorX; let anchorX;
let anchorY; let anchorY;
let anchorEdge;
if (isVertical) { if (isVertical) {
anchorY = globalPos.y - (screen.y || 0) + root.visualContent.height / 2; anchorX = edge === "left" ? barOffset : screen.width - barOffset;
if (edge === "left") { anchorY = localPos.y;
anchorX = barOffset;
anchorEdge = "top";
} else {
anchorX = screen.width - barOffset;
anchorEdge = "top";
}
} else { } else {
anchorX = globalPos.x - (screen.x || 0) + root.visualContent.width / 2; anchorX = localPos.x;
if (edge === "bottom") { anchorY = edge === "bottom" ? screen.height - barOffset : barOffset;
anchorY = screen.height - barOffset;
anchorEdge = "bottom";
} else {
anchorY = barOffset;
anchorEdge = "top";
}
} }
dndPopupLoader.active = true; dndPopupLoader.active = true;
const popup = dndPopupLoader.item; const popup = dndPopupLoader.item;
if (!popup) if (!popup)
return; return;
popup.showAt(anchorX, anchorY, screen, anchorEdge);
popup.showAt(anchorX, anchorY, isVertical, edge, screen);
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.MiddleButton
onPressed: mouse => {
root.triggerRipple(this, mouse.x, mouse.y);
SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
}
} }
Loader { Loader {
@@ -1250,13 +1250,8 @@ BasePill {
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const edge = root.axis?.edge; const edge = root.axis?.edge;
if (edge === "left") { const targetX = edge === "left" ? overflowMenu.anchorPos.x : overflowMenu.anchorPos.x - alignedWidth;
const targetX = overflowMenu.anchorPos.x; return Math.max(10, Math.min(overflowMenu.width - alignedWidth - 10, targetX));
return Math.max(left, Math.min(right, targetX));
} else {
const targetX = overflowMenu.anchorPos.x - alignedWidth;
return Math.max(left, Math.min(right, targetX));
}
} else { } else {
const want = overflowMenu.anchorPos.x - alignedWidth / 2; const want = overflowMenu.anchorPos.x - alignedWidth / 2;
return Math.max(left, Math.min(right, want)); return Math.max(left, Math.min(right, want));
@@ -1271,13 +1266,8 @@ BasePill {
const want = overflowMenu.anchorPos.y - alignedHeight / 2; const want = overflowMenu.anchorPos.y - alignedHeight / 2;
return Math.max(top, Math.min(bottom, want)); return Math.max(top, Math.min(bottom, want));
} else { } else {
if (root.isAtBottom) { const targetY = root.isAtBottom ? overflowMenu.anchorPos.y - alignedHeight : overflowMenu.anchorPos.y;
const targetY = overflowMenu.anchorPos.y - alignedHeight; return Math.max(10, Math.min(overflowMenu.height - alignedHeight - 10, targetY));
return Math.max(top, Math.min(bottom, targetY));
} else {
const targetY = overflowMenu.anchorPos.y;
return Math.max(top, Math.min(bottom, targetY));
}
} }
})(), overflowMenu.dpr) })(), overflowMenu.dpr)
@@ -1770,13 +1760,8 @@ BasePill {
if (menuRoot.isVertical) { if (menuRoot.isVertical) {
const edge = menuRoot.axis?.edge; const edge = menuRoot.axis?.edge;
if (edge === "left") { const targetX = edge === "left" ? menuWindow.anchorPos.x : menuWindow.anchorPos.x - alignedWidth;
const targetX = menuWindow.anchorPos.x; return Math.max(10, Math.min(menuWindow.width - alignedWidth - 10, targetX));
return Math.max(left, Math.min(right, targetX));
} else {
const targetX = menuWindow.anchorPos.x - alignedWidth;
return Math.max(left, Math.min(right, targetX));
}
} else { } else {
const want = menuWindow.anchorPos.x - alignedWidth / 2; const want = menuWindow.anchorPos.x - alignedWidth / 2;
return Math.max(left, Math.min(right, want)); return Math.max(left, Math.min(right, want));
@@ -1791,13 +1776,8 @@ BasePill {
const want = menuWindow.anchorPos.y - alignedHeight / 2; const want = menuWindow.anchorPos.y - alignedHeight / 2;
return Math.max(top, Math.min(bottom, want)); return Math.max(top, Math.min(bottom, want));
} else { } else {
if (menuRoot.isAtBottom) { const targetY = menuRoot.isAtBottom ? menuWindow.anchorPos.y - alignedHeight : menuWindow.anchorPos.y;
const targetY = menuWindow.anchorPos.y - alignedHeight; return Math.max(10, Math.min(menuWindow.height - alignedHeight - 10, targetY));
return Math.max(top, Math.min(bottom, targetY));
} else {
const targetY = menuWindow.anchorPos.y;
return Math.max(top, Math.min(bottom, targetY));
}
} }
})(), menuWindow.dpr) })(), menuWindow.dpr)
@@ -1079,7 +1079,7 @@ Rectangle {
Text { Text {
text: I18n.tr("Add a task...", "placeholder in the new-task input field") text: I18n.tr("Add a task...", "placeholder in the new-task input field")
color: Theme.onSurface_38 color: Theme.outlineButton
visible: taskInput.text.length === 0 visible: taskInput.text.length === 0
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -19,6 +18,7 @@ Card {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
visible: !WeatherService.weather.available visible: !WeatherService.weather.available
z: 1
DankSpinner { DankSpinner {
size: 24 size: 24
@@ -42,9 +42,9 @@ Card {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
Button { DankButton {
text: I18n.tr("Refresh") text: I18n.tr("Refresh")
flat: true buttonHeight: 32
visible: !WeatherService.weather.loading visible: !WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onClicked: WeatherService.forceRefresh() onClicked: WeatherService.forceRefresh()
+28 -93
View File
@@ -141,52 +141,27 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
DankIcon { DankActionButton {
id: refreshButtonTwo id: refreshButtonTwo
name: "refresh"
size: Theme.iconSize - 4
color: Theme.withAlpha(Theme.surfaceText, 0.4)
anchors.top: parent.top
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
smoothTransform: isRefreshing iconName: isRefreshing ? "" : "refresh"
iconColor: Theme.withAlpha(Theme.surfaceText, 0.4)
property bool isRefreshing: false tooltipText: I18n.tr("Refresh Weather")
tooltipSide: "left"
enabled: !isRefreshing enabled: !isRefreshing
MouseArea { property bool isRefreshing: false
id: refreshButtonMouseAreaTwo
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
enabled: parent.enabled
Timer { onClicked: {
id: hoverDelayTwo isRefreshing = true;
interval: 300 WeatherService.forceRefresh();
repeat: false refreshTimerTwo.restart();
onTriggered: {
refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), refreshButtonTwo, 0, 0, "left");
}
}
onEntered: {
hoverDelayTwo.restart();
}
onExited: {
hoverDelayTwo.stop();
refreshButtonTooltipTwo.hide();
}
onClicked: {
refreshButtonTwo.isRefreshing = true;
WeatherService.forceRefresh();
refreshTimerTwo.restart();
}
} }
DankTooltipV2 { DankSpinner {
id: refreshButtonTooltipTwo anchors.centerIn: parent
size: refreshButtonTwo.iconSize
visible: refreshButtonTwo.isRefreshing
} }
Timer { Timer {
@@ -194,14 +169,6 @@ Item {
interval: 2000 interval: 2000
onTriggered: refreshButtonTwo.isRefreshing = false onTriggered: refreshButtonTwo.isRefreshing = false
} }
RotationAnimator on rotation {
running: refreshButtonTwo.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
} }
} }
} }
@@ -864,52 +831,28 @@ Item {
onClicked: SessionData.setWeatherHourlyDetailed(!SessionData.weatherHourlyDetailed) onClicked: SessionData.setWeatherHourlyDetailed(!SessionData.weatherHourlyDetailed)
} }
DankIcon { DankActionButton {
id: refreshButton id: refreshButton
name: "refresh"
size: Theme.iconSize - 4
color: Theme.withAlpha(Theme.surfaceText, 0.4)
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
smoothTransform: isRefreshing iconName: isRefreshing ? "" : "refresh"
iconColor: Theme.withAlpha(Theme.surfaceText, 0.4)
property bool isRefreshing: false tooltipText: I18n.tr("Refresh Weather")
tooltipSide: "left"
enabled: !isRefreshing enabled: !isRefreshing
MouseArea { property bool isRefreshing: false
id: refreshButtonMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
enabled: parent.enabled
Timer { onClicked: {
id: hoverDelay isRefreshing = true;
interval: 300 WeatherService.forceRefresh();
repeat: false refreshTimer.restart();
onTriggered: {
refreshButtonTooltip.show(I18n.tr("Refresh Weather"), refreshButton, 0, 0, "left");
}
}
onEntered: {
hoverDelay.restart();
}
onExited: {
hoverDelay.stop();
refreshButtonTooltip.hide();
}
onClicked: {
refreshButton.isRefreshing = true;
WeatherService.forceRefresh();
refreshTimer.restart();
}
} }
DankTooltipV2 { DankSpinner {
id: refreshButtonTooltip anchors.centerIn: parent
size: refreshButton.iconSize
visible: refreshButton.isRefreshing
} }
Timer { Timer {
@@ -917,14 +860,6 @@ Item {
interval: 2000 interval: 2000
onTriggered: refreshButton.isRefreshing = false onTriggered: refreshButton.isRefreshing = false
} }
RotationAnimator on rotation {
running: refreshButton.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
} }
} }
+45 -10
View File
@@ -438,20 +438,55 @@ Variants {
parent: dock.contentItem parent: dock.contentItem
visible: false visible: false
readonly property bool expanded: dock.reveal readonly property bool expanded: dock.reveal
readonly property bool chrome: dock.usesConnectedFrameChrome
readonly property bool atEndEdge: SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right
readonly property real innerReach: borderThickness + (SettingsData.appsDockEnlargeOnHover ? animationHeadroom : 0)
readonly property real bodyX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
readonly property real bodyY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
x: { x: {
const baseX = dockCore.x + dockMouseArea.x; if (chrome) {
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) const baseX = dockCore.x + dockMouseArea.x;
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0); if (isVertical && atEndEdge)
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0); return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
}
if (!isVertical)
return dockCore.x + dockMouseArea.x - (expanded ? borderThickness : 0);
if (!atEndEdge)
return 0;
return expanded ? Math.min(bodyX - innerReach, dock.width - 1) : dock.width - 1;
} }
y: { y: {
const baseY = dockCore.y + dockMouseArea.y; if (chrome) {
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) const baseY = dockCore.y + dockMouseArea.y;
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0); if (!isVertical && atEndEdge)
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0); return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
}
if (isVertical)
return dockCore.y + dockMouseArea.y - (expanded ? borderThickness : 0);
if (!atEndEdge)
return 0;
return expanded ? Math.min(bodyY - innerReach, dock.height - 1) : dock.height - 1;
}
width: {
if (chrome)
return dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0);
if (!isVertical)
return dockMouseArea.width + (expanded ? borderThickness * 2 : 0);
if (!expanded)
return 1;
return atEndEdge ? dock.width - x : Math.max(bodyX + dockBackground.width + innerReach, 1);
}
height: {
if (chrome)
return dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0);
if (isVertical)
return dockMouseArea.height + (expanded ? borderThickness * 2 : 0);
if (!expanded)
return 1;
return atEndEdge ? dock.height - y : Math.max(bodyY + dockBackground.height + innerReach, 1);
} }
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
} }
mask: Region { mask: Region {
+4 -2
View File
@@ -479,6 +479,8 @@ Item {
delegate: Item { delegate: Item {
id: delegateItem id: delegateItem
required property var modelData
required property int index
property var dockButton: { property var dockButton: {
switch (itemData.type) { switch (itemData.type) {
@@ -599,7 +601,7 @@ Item {
height: delegateItem.height height: delegateItem.height
actualIconSize: root.iconSize actualIconSize: root.iconSize
dockApps: root dockApps: root
index: model.index index: delegateItem.index
} }
DockTrashButton { DockTrashButton {
@@ -624,7 +626,7 @@ Item {
appData: itemData appData: itemData
contextMenu: root.contextMenu contextMenu: root.contextMenu
dockApps: root dockApps: root
index: model.index index: delegateItem.index
parentDockScreen: root.dockScreen parentDockScreen: root.dockScreen
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped" showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
windowTitle: { windowTitle: {
@@ -1773,6 +1773,7 @@ Item {
const idx = GreeterState.sessionList.indexOf(value); const idx = GreeterState.sessionList.indexOf(value);
if (idx < 0) if (idx < 0)
return; return;
GreeterState.sessionManuallySelected = true;
GreeterState.currentSessionIndex = idx; GreeterState.currentSessionIndex = idx;
GreeterState.selectedSession = GreeterState.sessionExecs[idx]; GreeterState.selectedSession = GreeterState.sessionExecs[idx];
GreeterState.selectedSessionPath = GreeterState.sessionPaths[idx]; GreeterState.selectedSessionPath = GreeterState.sessionPaths[idx];
@@ -1785,6 +1786,8 @@ Item {
property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || "" property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || ""
function finalizeSessionSelection() { function finalizeSessionSelection() {
if (GreeterState.sessionManuallySelected)
return;
if (GreeterState.sessionList.length === 0) if (GreeterState.sessionList.length === 0)
return; return;
if (!GreetdMemory.memoryReady) if (!GreetdMemory.memoryReady)
@@ -21,6 +21,7 @@ Singleton {
property var sessionPaths: [] property var sessionPaths: []
property var sessionDesktopIds: [] property var sessionDesktopIds: []
property int currentSessionIndex: 0 property int currentSessionIndex: 0
property bool sessionManuallySelected: false
property var availableUsers: [] property var availableUsers: []
property int selectedUserIndex: -1 property int selectedUserIndex: -1
+20 -2
View File
@@ -4,11 +4,13 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services
PanelWindow { PanelWindow {
id: root id: root
property bool active: false property bool active: false
property bool _completed: false
signal fadeCompleted signal fadeCompleted
signal fadeCancelled signal fadeCancelled
@@ -35,7 +37,8 @@ PanelWindow {
opacity: 0 opacity: 0
onOpacityChanged: { onOpacityChanged: {
if (opacity >= 0.99 && root.active) { if (opacity >= 0.99 && root.active && !root._completed) {
root._completed = true;
root.fadeCompleted(); root.fadeCompleted();
} }
} }
@@ -58,6 +61,7 @@ PanelWindow {
function startFade() { function startFade() {
if (!SettingsData.fadeToDpmsEnabled) if (!SettingsData.fadeToDpmsEnabled)
return; return;
_completed = false;
active = true; active = true;
fadeOverlay.opacity = 0.0; fadeOverlay.opacity = 0.0;
fadeSeq.stop(); fadeSeq.stop();
@@ -65,10 +69,24 @@ PanelWindow {
} }
function cancelFade() { function cancelFade() {
dismiss();
fadeCancelled();
}
function dismiss() {
fadeSeq.stop(); fadeSeq.stop();
fadeOverlay.opacity = 0.0; fadeOverlay.opacity = 0.0;
active = false; active = false;
fadeCancelled(); _completed = false;
}
Connections {
target: IdleService
function onIsShellLockedChanged() {
if (IdleService.isShellLocked)
return;
root.dismiss();
}
} }
MouseArea { MouseArea {
@@ -27,11 +27,14 @@ Item {
function hide() { function hide() {
if (isKeyboardActive && keyboard !== null) { if (isKeyboardActive && keyboard !== null) {
keyboard.destroy(); keyboard.destroy();
keyboard = null;
isKeyboardActive = false; isKeyboardActive = false;
} else } else
log.debug("The keyboard is already hidden"); log.debug("The keyboard is already hidden");
} }
Component.onDestruction: hide()
// private // private
property Item keyboard: null property Item keyboard: null
Component { Component {
+81 -31
View File
@@ -10,6 +10,8 @@ import qs.Services
Scope { Scope {
id: root id: root
readonly property var log: Log.scoped("Lock")
property string sharedPasswordBuffer: "" property string sharedPasswordBuffer: ""
property bool shouldLock: false property bool shouldLock: false
@@ -21,26 +23,13 @@ Scope {
onShouldLockChanged: { onShouldLockChanged: {
IdleService.isShellLocked = shouldLock; IdleService.isShellLocked = shouldLock;
if (shouldLock && lockPowerOffArmed) {
lockStateCheck.restart();
}
}
Timer {
id: lockStateCheck
interval: 100
repeat: false
onTriggered: {
if (sessionLock.locked && lockPowerOffArmed) {
pendingLock = false;
lockPowerOffArmed = false;
beginPowerOff();
}
}
} }
property bool lockInitiatedLocally: false property bool lockInitiatedLocally: false
property bool pendingLock: false property bool pendingLock: false
readonly property int maxLockRetries: 3
property int lockRetryAttempts: 0
property bool lockRetryPending: false
property bool lockPowerOffArmed: false property bool lockPowerOffArmed: false
property bool lockWakeAllowed: false property bool lockWakeAllowed: false
property bool customLockerSpawned: false property bool customLockerSpawned: false
@@ -116,6 +105,24 @@ Scope {
return true; return true;
} }
function resetLockRetry() {
lockRetryAttempts = 0;
lockRetryPending = false;
lockRetryTimer.stop();
}
function handleLockLost() {
if (lockRetryAttempts >= maxLockRetries) {
log.error("Compositor refused session lock", maxLockRetries, "times - resetting lock state");
forceReset();
return;
}
lockRetryAttempts++;
lockRetryPending = true;
lockRetryTimer.restart();
log.warn("Session lock lost while lock requested - retry", lockRetryAttempts, "/", maxLockRetries);
}
function lock() { function lock() {
if (SettingsData.customPowerActionLock?.length > 0) { if (SettingsData.customPowerActionLock?.length > 0) {
spawnCustomLocker(); spawnCustomLocker();
@@ -124,6 +131,7 @@ Scope {
if (shouldLock || pendingLock) if (shouldLock || pendingLock)
return; return;
resetLockRetry();
lockInitiatedLocally = true; lockInitiatedLocally = true;
lockPowerOffArmed = powerOffOnLock; lockPowerOffArmed = powerOffOnLock;
@@ -139,7 +147,7 @@ Scope {
function lockAndOutputsOff() { function lockAndOutputsOff() {
IdleService.lockPowerOffRequested = true; IdleService.lockPowerOffRequested = true;
if (sessionLock.locked) { if (sessionLock.secure) {
beginPowerOff(); beginPowerOff();
return; return;
} }
@@ -150,6 +158,7 @@ Scope {
function unlock() { function unlock() {
if (!shouldLock) if (!shouldLock)
return; return;
resetLockRetry();
lockInitiatedLocally = false; lockInitiatedLocally = false;
notifyLoginctl(false); notifyLoginctl(false);
shouldLock = false; shouldLock = false;
@@ -160,6 +169,7 @@ Scope {
pendingLock = false; pendingLock = false;
shouldLock = false; shouldLock = false;
customLockerSpawned = false; customLockerSpawned = false;
resetLockRetry();
resetPowerOffFade(); resetPowerOffFade();
IdleService.lockPowerOffRequested = false; IdleService.lockPowerOffRequested = false;
} }
@@ -198,6 +208,12 @@ Scope {
shouldLock = false; shouldLock = false;
} }
function onSessionResumed() {
if (!shouldLock || sessionLock.locked)
return;
resumeRelockTimer.restart();
}
function onLoginctlStateChanged() { function onLoginctlStateChanged() {
if (SessionService.active && pendingLock) { if (SessionService.active && pendingLock) {
pendingLock = false; pendingLock = false;
@@ -239,7 +255,7 @@ Scope {
WlSessionLock { WlSessionLock {
id: sessionLock id: sessionLock
locked: shouldLock locked: shouldLock && !lockRetryPending
WlSessionLockSurface { WlSessionLockSurface {
id: lockSurface id: lockSurface
@@ -312,16 +328,23 @@ Scope {
Connections { Connections {
target: sessionLock target: sessionLock
function onLockedChanged() { function onSecureChanged() {
notifyLockedHint(sessionLock.locked); notifyLockedHint(sessionLock.secure);
if (sessionLock.locked) { if (!sessionLock.secure)
pendingLock = false;
if (lockPowerOffArmed && powerOffOnLock)
beginPowerOff();
lockPowerOffArmed = false;
return; return;
} IdleService.dismissFadeToLock();
lockRetryAttempts = 0;
pendingLock = false;
if (lockPowerOffArmed && powerOffOnLock)
beginPowerOff();
lockPowerOffArmed = false;
}
function onLockedChanged() {
if (sessionLock.locked)
return;
if (shouldLock && (IdleService.monitorsOff || lockPowerOffArmed || IdleService.lockPowerOffRequested))
return;
lockWakeAllowed = false; lockWakeAllowed = false;
resetPowerOffFade(); resetPowerOffFade();
if (IdleService.monitorsOff && powerOffOnLock) { if (IdleService.monitorsOff && powerOffOnLock) {
@@ -329,6 +352,8 @@ Scope {
CompositorService.powerOnMonitors(); CompositorService.powerOnMonitors();
} }
IdleService.lockPowerOffRequested = false; IdleService.lockPowerOffRequested = false;
if (shouldLock && !lockRetryPending)
handleLockLost();
} }
} }
@@ -360,13 +385,15 @@ Scope {
} }
function isLocked(): bool { function isLocked(): bool {
return sessionLock.locked; return sessionLock.secure;
} }
function status(): string { function status(): string {
return JSON.stringify({ return JSON.stringify({
shouldLock: root.shouldLock, shouldLock: root.shouldLock,
sessionLockLocked: sessionLock.locked, sessionLockLocked: sessionLock.locked,
sessionLockSecure: sessionLock.secure,
lockRetryAttempts: root.lockRetryAttempts,
lockInitiatedLocally: root.lockInitiatedLocally, lockInitiatedLocally: root.lockInitiatedLocally,
pendingLock: root.pendingLock, pendingLock: root.pendingLock,
loginctlLocked: SessionService.locked, loginctlLocked: SessionService.locked,
@@ -395,6 +422,29 @@ Scope {
} }
} }
Timer {
id: lockRetryTimer
interval: 1000
repeat: false
onTriggered: root.lockRetryPending = false
}
Timer {
id: resumeRelockTimer
interval: 1000
repeat: false
onTriggered: {
if (!root.shouldLock || sessionLock.locked)
return;
if (IdleService.monitorsOff || root.lockPowerOffArmed || IdleService.lockPowerOffRequested)
return;
root.log.warn("Session lock dead after resume - re-locking");
root.resetLockRetry();
root.lockRetryPending = true;
root.lockRetryPending = false;
}
}
Timer { Timer {
id: dpmsReapplyTimer id: dpmsReapplyTimer
interval: 100 interval: 100
@@ -407,7 +457,7 @@ Scope {
interval: 200 interval: 200
repeat: false repeat: false
onTriggered: { onTriggered: {
if (!sessionLock.locked) if (!sessionLock.secure)
return; return;
if (!powerOffOnLock) if (!powerOffOnLock)
return; return;
@@ -426,7 +476,7 @@ Scope {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: sessionLock.locked enabled: sessionLock.secure
hoverEnabled: enabled hoverEnabled: enabled
onPressed: lockWakeDebounce.restart() onPressed: lockWakeDebounce.restart()
onPositionChanged: lockWakeDebounce.restart() onPositionChanged: lockWakeDebounce.restart()
@@ -435,10 +485,10 @@ Scope {
FocusScope { FocusScope {
anchors.fill: parent anchors.fill: parent
focus: sessionLock.locked focus: sessionLock.secure
Keys.onPressed: event => { Keys.onPressed: event => {
if (!sessionLock.locked) if (!sessionLock.secure)
return; return;
lockWakeDebounce.restart(); lockWakeDebounce.restart();
} }
+19 -1
View File
@@ -35,6 +35,7 @@ Item {
property int hyprlandLayoutCount: 0 property int hyprlandLayoutCount: 0
property bool lockerReadySent: false property bool lockerReadySent: false
property bool lockerReadyArmed: false property bool lockerReadyArmed: false
property var sessionLock: null
readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== "" readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== ""
readonly property string lockFontFamily: SettingsData.lockScreenFontFamily readonly property string lockFontFamily: SettingsData.lockScreenFontFamily
@@ -111,6 +112,8 @@ Item {
} }
function sendLockerReadyOnce() { function sendLockerReadyOnce() {
if (root.demoMode)
return;
if (lockerReadySent) if (lockerReadySent)
return; return;
if (root.unlocking) if (root.unlocking)
@@ -133,12 +136,27 @@ Item {
return; return;
if (!root.visible || root.opacity <= 0) if (!root.visible || root.opacity <= 0)
return; return;
// Don't report ready until the compositor has confirmed the session is
// locked (ext-session-lock `locked` event). Qt's afterRendering fires
// before the lock surface is committed/presented, so releasing the sleep
// inhibitor on it lets the machine freeze with the desktop still on screen,
// which then flashes on resume. secure=true guarantees the desktop is hidden.
if (root.sessionLock && !root.sessionLock.secure)
return;
Qt.callLater(() => { Qt.callLater(() => {
if (root.visible && root.opacity > 0 && !root.unlocking) if (root.visible && root.opacity > 0 && !root.unlocking)
sendLockerReadyOnce(); sendLockerReadyOnce();
}); });
} }
Connections {
target: root.sessionLock
enabled: target !== null
function onSecureChanged() {
root.maybeSend();
}
}
Connections { Connections {
target: root.Window.window target: root.Window.window
enabled: target !== null enabled: target !== null
@@ -778,7 +796,7 @@ Item {
return "passkey"; return "passkey";
if (pam.fprint.tries >= SettingsData.maxFprintTries) if (pam.fprint.tries >= SettingsData.maxFprintTries)
return "fingerprint_off"; return "fingerprint_off";
if (pam.fprint.active) if (pam.fprint.active || pam.fprint.retrying)
return "fingerprint"; return "fingerprint";
if (pam.u2f.active) if (pam.u2f.active)
return "passkey"; return "passkey";
+1
View File
@@ -33,6 +33,7 @@ FocusScope {
anchors.fill: parent anchors.fill: parent
demoMode: false demoMode: false
sessionLock: root.lock
pam: root.pam pam: root.pam
passwordBuffer: root.sharedPasswordBuffer passwordBuffer: root.sharedPasswordBuffer
screenName: root.screenName screenName: root.screenName
+8 -1
View File
@@ -262,9 +262,11 @@ Scope {
property bool available: SettingsData.lockFingerprintReady property bool available: SettingsData.lockFingerprintReady
property int tries property int tries
property int errorTries property int errorTries
property bool retrying: false
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) { if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) {
retrying = false;
abort(); abort();
return; return;
} }
@@ -273,6 +275,7 @@ Scope {
tries = 0; tries = 0;
errorTries = 0; errorTries = 0;
retrying = false;
start(); start();
} }
@@ -285,6 +288,7 @@ Scope {
switch (res) { switch (res) {
case PamResult.Success: case PamResult.Success:
retrying = false;
if (!root.unlockInProgress) { if (!root.unlockInProgress) {
passwd.abort(); passwd.abort();
root.proceedAfterPrimaryAuth(); root.proceedAfterPrimaryAuth();
@@ -293,13 +297,16 @@ Scope {
case PamResult.Error: case PamResult.Error:
errorTries++; errorTries++;
if (errorTries < 200) { if (errorTries < 200) {
retrying = true;
abort(); abort();
errorRetry.restart(); errorRetry.restart();
return; return;
} }
retrying = false;
abort(); abort();
return; return;
case PamResult.MaxTries: case PamResult.MaxTries:
retrying = false;
tries++; tries++;
if (tries < SettingsData.maxFprintTries) { if (tries < SettingsData.maxFprintTries) {
root.fprintState = "fail"; root.fprintState = "fail";
@@ -418,7 +425,7 @@ Scope {
Timer { Timer {
id: errorRetry id: errorRetry
interval: 1500 interval: Math.min(1500 * Math.pow(2, Math.max(0, fprint.errorTries - 1)), 30000)
onTriggered: fprint.start() onTriggered: fprint.start()
} }
@@ -31,14 +31,16 @@ PanelWindow {
} }
property point anchorPos: Qt.point(0, 0) property point anchorPos: Qt.point(0, 0)
property string anchorEdge: "top" property bool isVertical: false
property string edge: "top"
visible: false visible: false
function showAt(x, y, targetScreen, edge) { function showAt(x, y, vertical, barEdge, targetScreen) {
if (targetScreen) if (targetScreen)
root.screen = targetScreen; root.screen = targetScreen;
anchorPos = Qt.point(x, y); anchorPos = Qt.point(x, y);
anchorEdge = edge || "top"; isVertical = vertical ?? false;
edge = barEdge ?? "top";
visible = true; visible = true;
} }
@@ -66,21 +68,19 @@ PanelWindow {
visible: root.visible visible: root.visible
x: { x: {
const left = 10; if (root.isVertical) {
const right = root.width - width - 10; if (root.edge === "left")
const want = root.anchorPos.x - width / 2; return Math.min(root.width - width - 10, root.anchorPos.x);
return Math.max(left, Math.min(right, want)); return Math.max(10, root.anchorPos.x - width);
}
return Math.max(10, Math.min(root.width - width - 10, root.anchorPos.x - width / 2));
} }
y: { y: {
switch (root.anchorEdge) { if (root.isVertical)
case "bottom":
return Math.max(10, root.anchorPos.y - height);
case "left":
case "right":
return Math.max(10, Math.min(root.height - height - 10, root.anchorPos.y - height / 2)); return Math.max(10, Math.min(root.height - height - 10, root.anchorPos.y - height / 2));
default: if (root.edge === "bottom")
return Math.min(root.height - height - 10, root.anchorPos.y); return Math.max(10, root.anchorPos.y - height);
} return Math.min(root.height - height - 10, root.anchorPos.y);
} }
onDismissed: root.closeMenu() onDismissed: root.closeMenu()
@@ -61,6 +61,11 @@ DankPopout {
notificationHistoryVisible = !notificationHistoryVisible; notificationHistoryVisible = !notificationHistoryVisible;
} }
// Re-open without toggling the flag (used when retargeting to another monitor).
function present() {
openSized();
}
function openSized() { function openSized() {
if (!notificationHistoryVisible) if (!notificationHistoryVisible)
return; return;
@@ -45,6 +45,14 @@ PanelWindow {
blurWidth: !win._finalized && !win.connectedFrameMode ? innerW * s : 0 blurWidth: !win._finalized && !win.connectedFrameMode ? innerW * s : 0
blurHeight: !win._finalized && !win.connectedFrameMode ? innerH * s : 0 blurHeight: !win._finalized && !win.connectedFrameMode ? innerH * s : 0
blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
// Slide-out translates the card past the surface edge; intersecting with the
// resting bounds keeps the committed region on the visible content instead of
// leaving a stray blur sliver at the edge (#2917).
clipEnabled: true
clipX: content.x + content.cardInset
clipY: content.y + content.cardInset
clipWidth: innerW
clipHeight: innerH
} }
WlrLayershell.namespace: "dms:notification-popup" WlrLayershell.namespace: "dms:notification-popup"
@@ -375,6 +375,8 @@ Item {
visible: DesktopService.autostartAvailable visible: DesktopService.autostartAvailable
SettingsCard { SettingsCard {
settingKey: "autostartAddEntry"
tags: ["autostart", "add", "entry", "command", "startup"]
width: parent.width width: parent.width
iconName: "add_circle" iconName: "add_circle"
title: I18n.tr("Add Entry") title: I18n.tr("Add Entry")
@@ -764,6 +766,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "autostartTrayIconFix"
tags: ["tray", "icons", "fix", "systemd"]
width: parent.width width: parent.width
iconName: "handyman" iconName: "handyman"
title: I18n.tr("Tray Icon Fix") title: I18n.tr("Tray Icon Fix")
@@ -463,6 +463,7 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "barEnable"
iconName: selectedBarConfig?.enabled ? "visibility" : "visibility_off" iconName: selectedBarConfig?.enabled ? "visibility" : "visibility_off"
title: I18n.tr("Enable Bar") title: I18n.tr("Enable Bar")
visible: !dankBarTab.appearanceOnly && selectedBarId !== "default" visible: !dankBarTab.appearanceOnly && selectedBarId !== "default"
@@ -651,6 +652,8 @@ Item {
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
SettingsToggleRow { SettingsToggleRow {
settingKey: "barAutoHide"
tags: ["autohide", "auto-hide", "reveal", "intellihide"]
text: I18n.tr("Auto-hide") text: I18n.tr("Auto-hide")
description: I18n.tr("Automatically hide the bar when the pointer moves away") description: I18n.tr("Automatically hide the bar when the pointer moves away")
checked: selectedBarConfig?.autoHide ?? false checked: selectedBarConfig?.autoHide ?? false
@@ -700,6 +703,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barAutoHideStrict"
tags: ["autohide", "strict", "popout"]
width: parent.width - parent.leftPadding width: parent.width - parent.leftPadding
text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open") text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open")
description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open") description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open")
@@ -713,6 +718,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barHideWhenWindowsOpen"
tags: ["hide", "windows", "empty", "workspace"]
width: parent.width - parent.leftPadding width: parent.width - parent.leftPadding
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
text: I18n.tr("Hide When Windows Open") text: I18n.tr("Hide When Windows Open")
@@ -734,6 +741,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barManualVisibility"
tags: ["manual", "show", "hide", "ipc", "toggle"]
text: I18n.tr("Manual Show/Hide") text: I18n.tr("Manual Show/Hide")
description: I18n.tr("Toggle bar visibility manually via IPC") description: I18n.tr("Toggle bar visibility manually via IPC")
checked: selectedBarConfig?.visible ?? true checked: selectedBarConfig?.visible ?? true
@@ -753,6 +762,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barClickThrough"
tags: ["clickthrough", "click", "through", "mouse", "input", "mask", "passthrough"]
text: I18n.tr("Click Through") text: I18n.tr("Click Through")
description: I18n.tr("Mouse clicks pass through the bar to windows behind it") description: I18n.tr("Mouse clicks pass through the bar to windows behind it")
checked: selectedBarConfig?.clickThrough ?? false checked: selectedBarConfig?.clickThrough ?? false
@@ -910,6 +921,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: exclusiveZoneSlider id: exclusiveZoneSlider
settingKey: "barExclusiveZone"
tags: ["exclusive", "zone", "reserved", "offset"]
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
text: I18n.tr("Exclusive Zone Offset") text: I18n.tr("Exclusive Zone Offset")
description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge") description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge")
@@ -933,6 +946,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: sizeSlider id: sizeSlider
settingKey: "barSize"
tags: ["size", "thickness", "height", "inner"]
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
text: I18n.tr("Size") text: I18n.tr("Size")
description: I18n.tr("Adjust the bar height via inner padding") description: I18n.tr("Adjust the bar height via inner padding")
@@ -1154,6 +1169,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barSquareCorners"
tags: ["square", "corners", "rounding"]
text: I18n.tr("Square Corners") text: I18n.tr("Square Corners")
description: I18n.tr("Remove corner rounding from the bar") description: I18n.tr("Remove corner rounding from the bar")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
@@ -1164,6 +1181,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barNoBackground"
tags: ["transparent", "background", "invisible"]
text: I18n.tr("No Background") text: I18n.tr("No Background")
description: I18n.tr("Make the bar background fully transparent") description: I18n.tr("Make the bar background fully transparent")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
@@ -1174,6 +1193,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barMaximizeWidgetIcons"
tags: ["maximize", "icons", "stretch"]
text: I18n.tr("Maximize Widget Icons") text: I18n.tr("Maximize Widget Icons")
description: I18n.tr("Stretch widget icons to fill the available bar height") description: I18n.tr("Stretch widget icons to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetIcons ?? false checked: selectedBarConfig?.maximizeWidgetIcons ?? false
@@ -1183,6 +1204,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barMaximizeWidgetText"
tags: ["maximize", "text", "stretch"]
text: I18n.tr("Maximize Widget Text") text: I18n.tr("Maximize Widget Text")
description: I18n.tr("Stretch widget text to fill the available bar height") description: I18n.tr("Stretch widget text to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetText ?? false checked: selectedBarConfig?.maximizeWidgetText ?? false
@@ -1192,6 +1215,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barRemoveWidgetPadding"
tags: ["padding", "compact", "widgets"]
text: I18n.tr("Remove Widget Padding") text: I18n.tr("Remove Widget Padding")
description: I18n.tr("Remove inner padding from all widgets") description: I18n.tr("Remove inner padding from all widgets")
checked: selectedBarConfig?.removeWidgetPadding ?? false checked: selectedBarConfig?.removeWidgetPadding ?? false
@@ -1208,6 +1233,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barGothCorners"
tags: ["goth", "corners", "concave", "cutout"]
text: I18n.tr("Goth Corners") text: I18n.tr("Goth Corners")
description: I18n.tr("Apply inverse concave corner cutouts to the bar") description: I18n.tr("Apply inverse concave corner cutouts to the bar")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
@@ -1302,6 +1329,8 @@ Item {
} }
SettingsToggleCard { SettingsToggleCard {
settingKey: "barMaximizeDetection"
tags: ["maximize", "gaps", "border", "fullscreen"]
iconName: "fit_screen" iconName: "fit_screen"
title: I18n.tr("Maximize Detection") title: I18n.tr("Maximize Detection")
description: I18n.tr("Remove gaps and border when windows are maximized") description: I18n.tr("Remove gaps and border when windows are maximized")
@@ -1847,6 +1876,8 @@ Item {
SettingsToggleCard { SettingsToggleCard {
iconName: "mouse" iconName: "mouse"
settingKey: "barScrollWheel"
tags: ["scroll", "wheel", "workspace", "axis"]
title: I18n.tr("Scroll Wheel") title: I18n.tr("Scroll Wheel")
description: I18n.tr("Control workspaces and columns by scrolling on the bar") description: I18n.tr("Control workspaces and columns by scrolling on the bar")
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
@@ -267,6 +267,8 @@ Item {
spacing: Theme.spacingXL spacing: Theme.spacingXL
SettingsCard { SettingsCard {
settingKey: "defaultAppsInternet"
tags: ["browser", "mail", "email", "web"]
title: I18n.tr("Internet", "Internet") title: I18n.tr("Internet", "Internet")
iconName: "public" iconName: "public"
@@ -293,6 +295,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "defaultAppsUtilities"
tags: ["file", "manager", "terminal", "editor"]
title: I18n.tr("Utilities", "Utilities") title: I18n.tr("Utilities", "Utilities")
iconName: "terminal" iconName: "terminal"
@@ -317,6 +321,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "defaultAppsDocuments"
tags: ["pdf", "text", "reader", "office"]
title: I18n.tr("Documents", "Documents") title: I18n.tr("Documents", "Documents")
iconName: "edit_document" iconName: "edit_document"
@@ -335,6 +341,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "defaultAppsMultimedia"
tags: ["image", "video", "music", "viewer", "player"]
title: I18n.tr("Multimedia", "Multimedia") title: I18n.tr("Multimedia", "Multimedia")
iconName: "movie" iconName: "movie"
AppSelector { AppSelector {
@@ -166,6 +166,8 @@ Item {
spacing: Theme.spacingXL spacing: Theme.spacingXL
SettingsCard { SettingsCard {
settingKey: "desktopWidgetsManage"
tags: ["desktop", "widgets", "clock", "conky"]
width: parent.width width: parent.width
iconName: "widgets" iconName: "widgets"
title: I18n.tr("Desktop Widgets") title: I18n.tr("Desktop Widgets")
@@ -203,6 +205,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "desktopWidgetGroups"
tags: ["groups", "profiles", "layouts"]
width: parent.width width: parent.width
iconName: "folder" iconName: "folder"
title: I18n.tr("Groups") title: I18n.tr("Groups")
@@ -17,6 +17,7 @@ Singleton {
readonly property var wlrOutputs: WlrOutputService.outputs readonly property var wlrOutputs: WlrOutputService.outputs
property var outputs: ({}) property var outputs: ({})
property var savedOutputs: ({}) property var savedOutputs: ({})
property var savedParsedOutputs: ({})
property var allOutputs: buildAllOutputsMap() property var allOutputs: buildAllOutputsMap()
property var includeStatus: ({ property var includeStatus: ({
@@ -911,15 +912,18 @@ Singleton {
const paths = getConfigPaths(); const paths = getConfigPaths();
if (!paths) { if (!paths) {
savedOutputs = {}; savedOutputs = {};
savedParsedOutputs = {};
return; return;
} }
Proc.runCommand("load-saved-outputs", ["cat", paths.outputsFile], (content, exitCode) => { Proc.runCommand("load-saved-outputs", ["cat", paths.outputsFile], (content, exitCode) => {
if (exitCode !== 0 || !content.trim()) { if (exitCode !== 0 || !content.trim()) {
savedOutputs = {}; savedOutputs = {};
savedParsedOutputs = {};
return; return;
} }
const parsed = parseOutputsConfig(content); const parsed = parseOutputsConfig(content);
savedParsedOutputs = parsed;
const filtered = filterDisconnectedOnly(parsed); const filtered = filterDisconnectedOnly(parsed);
savedOutputs = filtered; savedOutputs = filtered;
@@ -1092,20 +1096,7 @@ Singleton {
const name = match[1]; const name = match[1];
const body = match[2]; const body = match[2];
if (body.trim() === "off") { const disabled = /^\s*off\s*$/m.test(body);
result[name] = {
"name": name,
"disabled": true,
"logical": {
"x": 0,
"y": 0,
"scale": 1.0,
"transform": "Normal"
}
};
continue;
}
const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/); const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/);
const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/); const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/);
const scaleMatch = body.match(/scale\s+([\d.]+)/); const scaleMatch = body.match(/scale\s+([\d.]+)/);
@@ -1115,6 +1106,7 @@ Singleton {
result[name] = { result[name] = {
"name": name, "name": name,
"disabled": disabled,
"logical": { "logical": {
"x": posMatch ? parseInt(posMatch[1]) : 0, "x": posMatch ? parseInt(posMatch[1]) : 0,
"y": posMatch ? parseInt(posMatch[2]) : 0, "y": posMatch ? parseInt(posMatch[2]) : 0,
@@ -1778,6 +1770,41 @@ Singleton {
return normalized; return normalized;
} }
function findSavedParsedOutput(outputName) {
const output = outputs[outputName];
const candidates = [outputName];
if (output?.make && output?.model) {
candidates.push(output.make + " " + output.model + " " + (output.serial || "Unknown"));
candidates.push(output.make + " " + output.model);
}
for (const savedName in savedParsedOutputs) {
if (candidates.includes(savedName.trim()))
return savedParsedOutputs[savedName];
}
return null;
}
// A disabled wlr head reports no current mode, scale 0 and position 0,0
// overlay the last written config so rewrites don't discard real settings.
function overlaySavedDisabledOutput(entry, outputName) {
const saved = findSavedParsedOutput(outputName);
if (!saved)
return;
const savedMode = saved.modes?.[saved.current_mode ?? 0];
if (savedMode && !entry.configured_mode)
entry.configured_mode = savedMode.width + "x" + savedMode.height + "@" + (savedMode.refresh_rate / 1000).toFixed(3);
if (saved.vrr_enabled !== undefined)
entry.vrr_enabled = saved.vrr_enabled;
if (!saved.logical || !entry.logical)
return;
entry.logical.x = saved.logical.x;
entry.logical.y = saved.logical.y;
entry.logical.scale = saved.logical.scale;
entry.logical.transform = saved.logical.transform;
}
function buildOutputsWithPendingChanges() { function buildOutputsWithPendingChanges() {
const result = {}; const result = {};
@@ -1787,7 +1814,10 @@ Singleton {
} }
for (const outputName in outputs) { for (const outputName in outputs) {
result[outputName] = JSON.parse(JSON.stringify(outputs[outputName])); const entry = JSON.parse(JSON.stringify(outputs[outputName]));
if (entry.enabled === false)
overlaySavedDisabledOutput(entry, outputName);
result[outputName] = entry;
} }
for (const outputName in pendingChanges) { for (const outputName in pendingChanges) {
@@ -1799,6 +1829,8 @@ Singleton {
result[outputName].logical.y = changes.position.y; result[outputName].logical.y = changes.position.y;
} }
if (changes.mode !== undefined && result[outputName].modes) { if (changes.mode !== undefined && result[outputName].modes) {
if (result[outputName].configured_mode)
result[outputName].configured_mode = changes.mode;
for (var i = 0; i < result[outputName].modes.length; i++) { for (var i = 0; i < result[outputName].modes.length; i++) {
if (formatMode(result[outputName].modes[i]) === changes.mode) { if (formatMode(result[outputName].modes[i]) === changes.mode) {
result[outputName].current_mode = i; result[outputName].current_mode = i;
+2
View File
@@ -618,6 +618,8 @@ Item {
} }
SettingsSliderRow { SettingsSliderRow {
settingKey: "dockExclusiveZone"
tags: ["exclusive", "zone", "reserved", "offset"]
text: I18n.tr("Exclusive Zone Offset") text: I18n.tr("Exclusive Zone Offset")
visible: !root.connectedFrameModeActive || root.connectedPersistentDockActive visible: !root.connectedFrameModeActive || root.connectedPersistentDockActive
value: SettingsData.dockBottomGap value: SettingsData.dockBottomGap
@@ -38,6 +38,8 @@ Item {
settingKey: "mediaPlayer" settingKey: "mediaPlayer"
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaWaveProgress"
tags: ["wave", "progress", "animated"]
text: I18n.tr("Wave Progress Bars") text: I18n.tr("Wave Progress Bars")
description: I18n.tr("Use animated wave progress bars for media playback") description: I18n.tr("Use animated wave progress bars for media playback")
checked: SettingsData.waveProgressEnabled checked: SettingsData.waveProgressEnabled
@@ -45,6 +47,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaScrollTitle"
tags: ["scroll", "title", "marquee"]
text: I18n.tr("Scroll song title") text: I18n.tr("Scroll song title")
description: I18n.tr("Scroll title if it doesn't fit in widget") description: I18n.tr("Scroll title if it doesn't fit in widget")
checked: SettingsData.scrollTitleEnabled checked: SettingsData.scrollTitleEnabled
@@ -52,6 +56,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaVisualizer"
tags: ["visualizer", "cava", "spectrum"]
text: I18n.tr("Audio Visualizer") text: I18n.tr("Audio Visualizer")
description: I18n.tr("Show cava audio visualizer in media widget") description: I18n.tr("Show cava audio visualizer in media widget")
checked: SettingsData.audioVisualizerEnabled checked: SettingsData.audioVisualizerEnabled
@@ -59,6 +65,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaAdaptiveWidth"
tags: ["adaptive", "width", "shrink"]
text: I18n.tr("Adaptive Media Width") text: I18n.tr("Adaptive Media Width")
description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size") description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size")
checked: SettingsData.mediaAdaptiveWidthEnabled checked: SettingsData.mediaAdaptiveWidthEnabled
@@ -66,6 +74,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaAlbumArtAccent"
tags: ["album", "art", "accent", "colors"]
text: I18n.tr("Use album art accent") text: I18n.tr("Use album art accent")
description: I18n.tr("Use colors extracted from album art instead of system theme colors") description: I18n.tr("Use colors extracted from album art instead of system theme colors")
checked: SettingsData.mediaUseAlbumArtAccent checked: SettingsData.mediaUseAlbumArtAccent
@@ -136,6 +146,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaDeviceScrollVolume"
tags: ["device", "scroll", "volume"]
text: I18n.tr("Device list scroll volume") text: I18n.tr("Device list scroll volume")
description: I18n.tr("Allow adjusting device volume by scrolling on the right half of items in the device list") description: I18n.tr("Allow adjusting device volume by scrolling on the right half of items in the device list")
checked: SettingsData.audioDeviceScrollVolumeEnabled checked: SettingsData.audioDeviceScrollVolumeEnabled
+1
View File
@@ -81,6 +81,7 @@ Item {
SettingsCard { SettingsCard {
tab: "mux" tab: "mux"
tags: ["mux", "session", "filter", "exclude", "hide"] tags: ["mux", "session", "filter", "exclude", "hide"]
settingKey: "muxSessionFilter"
title: I18n.tr("Session Filter") title: I18n.tr("Session Filter")
iconName: "filter_list" iconName: "filter_list"
@@ -55,12 +55,12 @@ Item {
} }
function getPinnedWifiNetworks() { function getPinnedWifiNetworks() {
const pins = SettingsData.wifiNetworkPins || {}; const pins = CacheData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]); return normalizePinList(pins["preferredWifi"]);
} }
function toggleWifiPin(ssid) { function toggleWifiPin(ssid) {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
let pinnedList = normalizePinList(pins["preferredWifi"]); let pinnedList = normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(ssid); const pinIndex = pinnedList.indexOf(ssid);
@@ -77,7 +77,7 @@ Item {
else else
delete pins["preferredWifi"]; delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins); CacheData.set("wifiNetworkPins", pins);
} }
property var forgetNetworkConfirm: ConfirmModal {} property var forgetNetworkConfirm: ConfirmModal {}
@@ -89,6 +89,8 @@ Item {
} }
SettingsDropdownRow { SettingsDropdownRow {
settingKey: "systemUpdaterCheckInterval"
tags: ["interval", "poll", "frequency"]
text: I18n.tr("Check interval") text: I18n.tr("Check interval")
description: I18n.tr("How often the server polls for new updates.") description: I18n.tr("How often the server polls for new updates.")
options: root.intervalOptions.map(o => o.label).concat([root.customIntervalLabel]) options: root.intervalOptions.map(o => o.label).concat([root.customIntervalLabel])
@@ -157,6 +159,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "systemUpdaterCheckOnStart"
tags: ["startup", "check", "boot"]
text: I18n.tr("Check on startup") text: I18n.tr("Check on startup")
description: I18n.tr("When enabled, checks updates on startup. When disabled, only the interval above or a manual refresh runs a check.") description: I18n.tr("When enabled, checks updates on startup. When disabled, only the interval above or a manual refresh runs a check.")
checked: SettingsData.updaterCheckOnStart checked: SettingsData.updaterCheckOnStart
@@ -164,6 +168,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "systemUpdaterFlatpak"
tags: ["flatpak", "include"]
text: I18n.tr("Include Flatpak updates") text: I18n.tr("Include Flatpak updates")
description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.") description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.")
visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak") visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak")
@@ -172,6 +178,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "systemUpdaterAUR"
tags: ["aur", "paru", "yay"]
text: I18n.tr("Include AUR updates") text: I18n.tr("Include AUR updates")
description: I18n.tr("Run paru/yay with AUR enabled when 'Update All' is clicked.") description: I18n.tr("Run paru/yay with AUR enabled when 'Update All' is clicked.")
visible: (SystemUpdateService.backends || []).some(b => b.id === "paru" || b.id === "yay") visible: (SystemUpdateService.backends || []).some(b => b.id === "paru" || b.id === "yay")
@@ -328,6 +336,8 @@ Item {
settingKey: "systemUpdaterAdvanced" settingKey: "systemUpdaterAdvanced"
SettingsToggleRow { SettingsToggleRow {
settingKey: "systemUpdaterCustomCommand"
tags: ["custom", "command", "terminal"]
text: I18n.tr("Use Custom Command") text: I18n.tr("Use Custom Command")
description: I18n.tr("Open a terminal and run a custom command instead of the in-shell upgrade flow.") description: I18n.tr("Open a terminal and run a custom command instead of the in-shell upgrade flow.")
checked: SettingsData.updaterUseCustomCommand checked: SettingsData.updaterUseCustomCommand
@@ -24,6 +24,8 @@ Item {
property string matugenPreviewRequestKey: "" property string matugenPreviewRequestKey: ""
property var installedRegistryThemes: [] property var installedRegistryThemes: []
property var templateDetection: [] property var templateDetection: []
readonly property var neovimDarkBaseThemes: ["aquarium", "ashes", "aylin", "ayu_dark", "bearded-arc", "carbonfox", "catppuccin", "chadracula", "chadracula-evondev", "chadtain", "chocolate", "darcula-dark", "dark_horizon", "decay", "default-dark", "doomchad", "eldritch", "embark", "everblush", "everforest", "falcon", "flexoki", "flouromachine", "gatekeeper", "github_dark", "gruvbox", "gruvchad", "hiberbee", "horizon", "jabuti", "jellybeans", "kanagawa", "kanagawa-dragon", "material-darker", "material-deep-ocean", "melange", "midnight_breeze", "mito-laser", "monekai", "monochrome", "mountain", "neofusion", "nightfox", "nightlamp", "nightowl", "nord", "obsidian-ember", "oceanic-next", "onedark", "onenord", "oxocarbon", "palenight", "pastelDark", "pastelbeans", "penumbra_dark", "poimandres", "radium", "rosepine", "rxyhn", "scaryforest", "seoul256_dark", "solarized_dark", "solarized_osaka", "starlight", "sweetpastel", "tokyodark", "tokyonight", "tomorrow_night", "tundra", "vesper", "vscode_dark", "wombat", "yoru", "zenburn"]
readonly property var neovimLightBaseThemes: ["ayu_light", "blossom_light", "catppuccin-latte", "default-light", "everforest_light", "flex-light", "flexoki-light", "github_light", "gruvbox_light", "material-lighter", "nano-light", "oceanic-light", "one_light", "onenord_light", "penumbra_light", "rosepine-dawn", "seoul256_light", "solarized_light", "sunrise_breeze", "vscode_light"]
readonly property var matugenSchemeColorMap: { readonly property var matugenSchemeColorMap: {
const map = {}; const map = {};
const mode = SessionData.isLightMode ? "light" : "dark"; const mode = SessionData.isLightMode ? "light" : "dark";
@@ -2666,7 +2668,7 @@ Item {
description: "Base to derive dark theme from" description: "Base to derive dark theme from"
visible: neovimThemeToggle.visible && neovimThemeToggle.checked visible: neovimThemeToggle.visible && neovimThemeToggle.checked
currentValue: SettingsData.matugenTemplateNeovimSettings?.dark?.baseTheme ?? "github_dark" currentValue: SettingsData.matugenTemplateNeovimSettings?.dark?.baseTheme ?? "github_dark"
options: ["aquarium", "ashes", "aylin", "ayu_dark", "bearded-arc", "carbonfox", "catppuccin", "chadracula", "chadracula-evondev", "chadtain", "chocolate", "darcula-dark", "dark_horizon", "decay", "default-dark", "doomchad", "eldritch", "embark", "everblush", "everforest", "falcon", "flexoki", "flouromachine", "gatekeeper", "github_dark", "gruvbox", "gruvchad", "hiberbee", "horizon", "jabuti", "jellybeans", "kanagawa", "kanagawa-dragon", "material-darker", "material-deep-ocean", "melange", "midnight_breeze", "mito-laser", "monekai", "monochrome", "mountain", "neofusion", "nightfox", "nightlamp", "nightowl", "nord", "obsidian-ember", "oceanic-next", "onedark", "onenord", "oxocarbon", "palenight", "pastelDark", "pastelbeans", "penumbra_dark", "poimandres", "radium", "rosepine", "rxyhn", "scaryforest", "seoul256_dark", "solarized_dark", "solarized_osaka", "starlight", "sweetpastel", "tokyodark", "tokyonight", "tomorrow_night", "tundra", "vesper", "vscode_dark", "wombat", "yoru", "zenburn"] options: themeColorsTab.neovimDarkBaseThemes.concat(themeColorsTab.neovimLightBaseThemes)
enableFuzzySearch: true enableFuzzySearch: true
onValueChanged: value => { onValueChanged: value => {
const settings = SettingsData.matugenTemplateNeovimSettings; const settings = SettingsData.matugenTemplateNeovimSettings;
@@ -2683,7 +2685,7 @@ Item {
description: "Base to derive light theme from" description: "Base to derive light theme from"
visible: neovimThemeToggle.visible && neovimThemeToggle.checked visible: neovimThemeToggle.visible && neovimThemeToggle.checked
currentValue: SettingsData.matugenTemplateNeovimSettings?.light?.baseTheme ?? "github_light" currentValue: SettingsData.matugenTemplateNeovimSettings?.light?.baseTheme ?? "github_light"
options: ["ayu_light", "blossom_light", "catppuccin-latte", "default-light", "everforest_light", "flex-light", "flexoki-light", "github_light", "gruvbox_light", "material-lighter", "nano-light", "oceanic-light", "one_light", "onenord_light", "penumbra_light", "rosepine-dawn", "seoul256_light", "solarized_light", "sunrise_breeze", "vscode_light"] options: themeColorsTab.neovimLightBaseThemes.concat(themeColorsTab.neovimDarkBaseThemes)
enableFuzzySearch: true enableFuzzySearch: true
onValueChanged: value => { onValueChanged: value => {
const settings = SettingsData.matugenTemplateNeovimSettings; const settings = SettingsData.matugenTemplateNeovimSettings;
+9 -7
View File
@@ -1199,16 +1199,18 @@ Item {
width: parent.width - Theme.spacingM * 2 width: parent.width - Theme.spacingM * 2
} }
DankButtonGroup { DankFilterChips {
id: transitionGroup id: transitionGroup
width: parent.width - Theme.spacingM * 2 width: parent.width - Theme.spacingM * 2
selectionMode: "multi" multiSelect: true
model: SessionData.availableWallpaperTransitions.filter(t => t !== "none") model: SessionData.availableWallpaperTransitions.filter(t => t !== "none").map(t => ({
initialSelection: SessionData.includedTransitions "value": t,
currentSelection: SessionData.includedTransitions "label": t.replace(/\b\w/g, c => c.toUpperCase())
}))
selectedValues: SessionData.includedTransitions
onSelectionChanged: (index, selected) => { onSelectionToggled: (index, selected) => {
const transition = model[index]; const transition = model[index].value;
let newIncluded = SessionData.includedTransitions.slice(); let newIncluded = SessionData.includedTransitions.slice();
if (selected && !newIncluded.includes(transition)) { if (selected && !newIncluded.includes(transition)) {
@@ -167,7 +167,13 @@ FloatingWindow {
width: parent.width width: parent.width
height: parent.height - searchField.height - Theme.spacingM height: parent.height - searchField.height - Theme.spacingM
spacing: Theme.spacingS spacing: Theme.spacingS
model: root.filteredApps // Start with no model. It is (re)bound in show() so that each open
// gets a fresh QQmlDelegateModel, avoiding a crash where a stale
// incubation queue from a previous open races with a background
// refreshApplications() replacing the underlying DesktopEntries
// QObjectModel (SIGSEGV in QQmlIncubatorPrivate::incubate via
// libQt6QmlModels). See hide()/onVisibleChanged() for the teardown.
model: null
clip: true clip: true
delegate: Rectangle { delegate: Rectangle {
@@ -316,11 +322,21 @@ FloatingWindow {
function show() { function show() {
updateFilteredApps(); updateFilteredApps();
// Rebind the model reactively (search relies on filteredApps changes flowing
// through), and do it after populating so the ListView starts incubating from
// a fully-formed array rather than [].
appList.model = Qt.binding(() => root.filteredApps);
visible = true; visible = true;
Qt.callLater(() => searchField.forceActiveFocus()); Qt.callLater(() => searchField.forceActiveFocus());
} }
function hide() { function hide() {
// Drop the model before clearing visible, so the ListView releases its
// QQmlDelegateModel (and any in-flight incubators) synchronously on this
// frame. This is the crux of the fix: a later show() will allocate a fresh
// DelegateModel instead of reusing one whose incubation queue may hold
// references invalidated by a concurrent refreshApplications().
appList.model = null;
visible = false; visible = false;
searchQuery = ""; searchQuery = "";
filteredApps = []; filteredApps = [];
@@ -330,6 +346,9 @@ FloatingWindow {
onVisibleChanged: { onVisibleChanged: {
if (!visible) { if (!visible) {
// Guard against visibility being cleared without going through hide()
// (e.g. window manager close, FloatingWindow.onClosed -> hide()).
appList.model = null;
searchQuery = ""; searchQuery = "";
filteredApps = []; filteredApps = [];
selectedIndex = -1; selectedIndex = -1;
+7 -5
View File
@@ -847,7 +847,7 @@ EOFCONFIG
const maxVol = root.sinkMaxVolume; const maxVol = root.sinkMaxVolume;
const clampedVolume = Math.max(0, Math.min(maxVol, percentage)); const clampedVolume = Math.max(0, Math.min(maxVol, percentage));
Quickshell.execDetached(["wpctl", "set-volume", "-l", String(maxVol / 100), "@DEFAULT_AUDIO_SINK@", String(clampedVolume / 100)]); root.sink.audio.volume = clampedVolume / 100;
return `Volume set to ${clampedVolume}%`; return `Volume set to ${clampedVolume}%`;
} }
@@ -857,14 +857,16 @@ EOFCONFIG
} }
function adjustDefaultSinkVolume(step, direction) { function adjustDefaultSinkVolume(step, direction) {
const audio = root.sink.audio;
const maxVol = root.sinkMaxVolume; const maxVol = root.sinkMaxVolume;
const stepValue = outputVolumeStep(step); const stepValue = outputVolumeStep(step);
const currentVolume = Math.round(root.sink.audio.volume * 100); const currentVolume = Math.round(audio.volume * 100);
const newVolume = Math.max(0, Math.min(maxVol, currentVolume + direction * stepValue)); const newVolume = Math.max(0, Math.min(maxVol, currentVolume + direction * stepValue));
const suffix = direction > 0 ? "+" : "-";
Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "0"]); if (audio.muted)
Quickshell.execDetached(["wpctl", "set-volume", "-l", String(maxVol / 100), "@DEFAULT_AUDIO_SINK@", `${stepValue}%${suffix}`]); audio.muted = false;
audio.volume = newVolume / 100;
return newVolume; return newVolume;
} }
+17 -9
View File
@@ -17,7 +17,22 @@ Singleton {
interval: 500 interval: 500
repeat: false repeat: false
running: true running: true
onTriggered: root.suppressSound = false onTriggered: {
root.suppressSound = false;
root.applyPowerProfile();
}
}
function applyPowerProfile() {
if (!batteryAvailable)
return;
const profileValue = isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName;
if (profileValue === "")
return;
const targetProfile = parseInt(profileValue);
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
return;
PowerProfiles.profile = targetProfile;
} }
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY") readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
@@ -206,14 +221,7 @@ Singleton {
} }
} }
const profileValue = BatteryService.isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName; applyPowerProfile();
if (profileValue !== "") {
const targetProfile = parseInt(profileValue);
if (!isNaN(targetProfile) && PowerProfiles.profile !== targetProfile) {
PowerProfiles.profile = targetProfile;
}
}
previousPluggedState = isPluggedIn; previousPluggedState = isPluggedIn;
} }
+19 -8
View File
@@ -15,8 +15,10 @@ Item {
property string socketPath: "" property string socketPath: ""
readonly property bool socketFound: socketPath.length > 0 readonly property bool socketFound: socketPath.length > 0
property bool connected: false property bool connected: false
property bool binaryExists: false readonly property bool binaryExists: launchKind.length > 0
property bool binaryChecked: false property bool binaryChecked: false
// "dcal" for a host install, "flatpak" for a sandboxed one, "" for neither.
property string launchKind: ""
property var calendars: [] property var calendars: []
property var events: [] property var events: []
@@ -55,18 +57,20 @@ Item {
Process { Process {
id: binaryCheck id: binaryCheck
command: ["sh", "-c", "command -v dcal"] command: ["sh", "-c", "if command -v dcal >/dev/null 2>&1; then echo dcal; elif command -v flatpak >/dev/null 2>&1 && flatpak info com.danklinux.dankcalendar >/dev/null 2>&1; then echo flatpak; fi"]
running: false running: false
onExited: code => { stdout: StdioCollector {
root.binaryExists = (code === 0); onStreamFinished: {
root.binaryChecked = true; root.launchKind = text.trim();
root.binaryChecked = true;
}
} }
} }
Process { Process {
id: discoverProcess id: discoverProcess
running: false running: false
command: ["sh", "-c", "s=\"${DANKCAL_SOCKET:-}\"; if [ -S \"$s\" ]; then echo \"$s\"; exit 0; fi; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/dankcal-*.sock /tmp/dankcal-*.sock; do [ -S \"$f\" ] || continue; p=$(basename \"$f\" .sock); p=${p#dankcal-}; if kill -0 \"$p\" 2>/dev/null; then echo \"$f\"; exit 0; fi; done"] command: ["sh", "-c", "s=\"${DANKCAL_SOCKET:-}\"; if [ -S \"$s\" ]; then echo \"$s\"; exit 0; fi; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/app/com.danklinux.dankcalendar/dankcal-*.sock; do if [ -S \"$f\" ]; then echo \"$f\"; exit 0; fi; done; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/dankcal-*.sock /tmp/dankcal-*.sock; do [ -S \"$f\" ] || continue; p=$(basename \"$f\" .sock); p=${p#dankcal-}; if kill -0 \"$p\" 2>/dev/null; then echo \"$f\"; exit 0; fi; done"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
@@ -98,9 +102,16 @@ Item {
} }
function launch() { function launch() {
if (!binaryExists) switch (launchKind) {
case "dcal":
Quickshell.execDetached(["dcal", "run", "-d", "--hidden"]);
break;
case "flatpak":
Quickshell.execDetached(["flatpak", "run", "com.danklinux.dankcalendar", "run", "-d", "--hidden"]);
break;
default:
return; return;
Quickshell.execDetached(["dcal", "run", "-d", "--hidden"]); }
if (enabled && !connected) if (enabled && !connected)
discoverProcess.running = true; discoverProcess.running = true;
} }
+1 -1
View File
@@ -511,7 +511,7 @@ Singleton {
return MangoService.filterCurrentDisplay(toplevels, screenName); return MangoService.filterCurrentDisplay(toplevels, screenName);
if (isHyprland) if (isHyprland)
return filterHyprlandCurrentDisplaySafe(toplevels, screenName); return filterHyprlandCurrentDisplaySafe(toplevels, screenName);
return toplevels; return toplevels.filter(t => _toplevelOnScreen(t, screenName));
} }
function _screenName(screenOrName) { function _screenName(screenOrName) {
+1 -3
View File
@@ -56,7 +56,7 @@ Singleton {
return "wifi_1_bar"; return "wifi_1_bar";
} }
property string userPreference: "auto" readonly property string userPreference: SettingsData.networkPreference
property bool isConnecting: false property bool isConnecting: false
property string connectingSSID: "" property string connectingSSID: ""
property string connectionError: "" property string connectionError: ""
@@ -187,7 +187,6 @@ Singleton {
} }
Component.onCompleted: { Component.onCompleted: {
root.userPreference = SettingsData.networkPreference;
lastConnectedVpnUuid = SessionData.vpnLastConnected || ""; lastConnectedVpnUuid = SessionData.vpnLastConnected || "";
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
checkDMSCapabilities(); checkDMSCapabilities();
@@ -672,7 +671,6 @@ Singleton {
function setNetworkPreference(preference) { function setNetworkPreference(preference) {
if (!networkAvailable) if (!networkAvailable)
return; return;
userPreference = preference;
changingPreference = true; changingPreference = true;
targetPreference = preference; targetPreference = preference;
SettingsData.set("networkPreference", preference); SettingsData.set("networkPreference", preference);
+8 -1
View File
@@ -69,7 +69,7 @@ Singleton {
property bool screensaverInhibited: false property bool screensaverInhibited: false
property var screensaverInhibitors: [] property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"] property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -335,6 +335,7 @@ Singleton {
const data = response.result.data; const data = response.result.data;
if (service === "server") { if (service === "server") {
const prevCapabilities = capabilities;
apiVersion = data.apiVersion || 0; apiVersion = data.apiVersion || 0;
cliVersion = data.cliVersion || ""; cliVersion = data.cliVersion || "";
capabilities = data.capabilities || []; capabilities = data.capabilities || [];
@@ -346,6 +347,12 @@ Singleton {
} }
capabilitiesReceived(); capabilitiesReceived();
const capabilitiesChanged = prevCapabilities.length !== capabilities.length || capabilities.some(c => !prevCapabilities.includes(c));
if (prevCapabilities.length > 0 && capabilitiesChanged) {
log.info("Capabilities changed, resubscribing");
subscribe(activeSubscriptions);
}
} else if (service === "network") { } else if (service === "network") {
networkStateUpdate(data); networkStateUpdate(data);
} else if (service === "network.credentials") { } else if (service === "network.credentials") {
+1 -1
View File
@@ -386,7 +386,7 @@ Singleton {
if (!focusedScreen) if (!focusedScreen)
return ""; return "";
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const screenKey = SettingsData.getScreenDisplayName(focusedScreen); const screenKey = SettingsData.getScreenDisplayName(focusedScreen);
if (!screenKey) if (!screenKey)
return ""; return "";
+10
View File
@@ -80,6 +80,8 @@ Singleton {
respectInhibitors: root.respectInhibitors respectInhibitors: root.respectInhibitors
enabled: false enabled: false
onIsIdleChanged: { onIsIdleChanged: {
if (!enabled)
return;
if (isIdle) { if (isIdle) {
if (SettingsData.fadeToDpmsEnabled) { if (SettingsData.fadeToDpmsEnabled) {
root.fadeToDpmsRequested(); root.fadeToDpmsRequested();
@@ -101,6 +103,8 @@ Singleton {
respectInhibitors: root.respectInhibitors respectInhibitors: root.respectInhibitors
enabled: false enabled: false
onIsIdleChanged: { onIsIdleChanged: {
if (!enabled)
return;
if (isIdle) { if (isIdle) {
root.requestMonitorOff(); root.requestMonitorOff();
} else { } else {
@@ -115,6 +119,8 @@ Singleton {
respectInhibitors: root.respectInhibitors respectInhibitors: root.respectInhibitors
enabled: false enabled: false
onIsIdleChanged: { onIsIdleChanged: {
if (!enabled)
return;
if (isIdle) { if (isIdle) {
if (SettingsData.fadeToLockEnabled) { if (SettingsData.fadeToLockEnabled) {
root.fadeToLockRequested(); root.fadeToLockRequested();
@@ -135,6 +141,8 @@ Singleton {
respectInhibitors: root.respectInhibitors respectInhibitors: root.respectInhibitors
enabled: false enabled: false
onIsIdleChanged: { onIsIdleChanged: {
if (!enabled)
return;
if (isIdle) if (isIdle)
root.requestSuspend(); root.requestSuspend();
} }
@@ -149,6 +157,8 @@ Singleton {
respectInhibitors: false respectInhibitors: false
enabled: root.enabled && root.isShellLocked && root.monitorsOff && (SettingsData.lockScreenPowerOffMonitorsOnLock || root.lockPowerOffRequested) enabled: root.enabled && root.isShellLocked && root.monitorsOff && (SettingsData.lockScreenPowerOffMonitorsOnLock || root.lockPowerOffRequested)
onIsIdleChanged: { onIsIdleChanged: {
if (!enabled)
return;
if (!isIdle && root.monitorsOff) if (!isIdle && root.monitorsOff)
root.requestMonitorOn(); root.requestMonitorOn();
} }
+39 -7
View File
@@ -3,10 +3,13 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property bool wantsLocation: SettingsData.weatherEnabled && SettingsData.useAutoLocation
readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location") readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location")
readonly property bool valid: latitude !== 0 || longitude !== 0 readonly property bool valid: latitude !== 0 || longitude !== 0
@@ -15,19 +18,46 @@ Singleton {
signal locationChanged(var data) signal locationChanged(var data)
onLocationAvailableChanged: { onWantsLocationChanged: {
if (locationAvailable && !valid) if (wantsLocation) {
getState(); ensureSubscription();
} else if (DMSService.activeSubscriptions.includes("location")) {
DMSService.removeSubscription("location");
}
} }
onLocationAvailableChanged: ensureSubscription()
Component.onCompleted: ensureSubscription()
Connections { Connections {
target: DMSService target: DMSService
function onLocationStateUpdate(data) { function onConnectionStateChanged() {
if (!locationAvailable) if (DMSService.isConnected)
return; root.ensureSubscription();
handleStateUpdate(data);
} }
function onLocationStateUpdate(data) {
if (!root.wantsLocation)
return;
root.handleStateUpdate(data);
}
}
function ensureSubscription() {
if (!wantsLocation)
return;
if (!locationAvailable)
return;
if (DMSService.activeSubscriptions.includes("location"))
return;
if (DMSService.activeSubscriptions.includes("all"))
return;
DMSService.addSubscription("location");
if (!valid)
getState();
} }
function handleStateUpdate(data) { function handleStateUpdate(data) {
@@ -42,6 +72,8 @@ Singleton {
} }
function getState() { function getState() {
if (!wantsLocation)
return;
if (!locationAvailable) if (!locationAvailable)
return; return;
+1 -1
View File
@@ -467,7 +467,7 @@ Singleton {
if (now - root._lastWatchedReloadAt < 700) if (now - root._lastWatchedReloadAt < 700)
return; return;
root._lastWatchedReloadAt = now; root._lastWatchedReloadAt = now;
root.reloadConfig(true, false); root.reloadConfig(false, false);
} }
function reloadConfig(showToast, suppressWatch) { function reloadConfig(showToast, suppressWatch) {
-2
View File
@@ -1487,8 +1487,6 @@ window-rule {
if (outputSettings.disabled) { if (outputSettings.disabled) {
kdlContent += ` off\n`; kdlContent += ` off\n`;
kdlContent += `}\n\n`;
continue;
} }
if (output.configured_mode) { if (output.configured_mode) {
@@ -0,0 +1,3 @@
import Quickshell.Services.Polkit
PolkitAgent {}
+6 -4
View File
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Polkit
Singleton { Singleton {
id: root id: root
@@ -12,10 +11,13 @@ Singleton {
readonly property bool disablePolkitIntegration: Quickshell.env("DMS_DISABLE_POLKIT") === "1" readonly property bool disablePolkitIntegration: Quickshell.env("DMS_DISABLE_POLKIT") === "1"
readonly property bool polkitAvailable: !disablePolkitIntegration readonly property bool polkitAvailable: !disablePolkitIntegration
readonly property alias agent: polkitAgentInstance readonly property var agent: polkitAgentLoader.item
PolkitAgent { Loader {
id: polkitAgentInstance id: polkitAgentLoader
active: root.polkitAvailable
asynchronous: false
source: "PolkitAgentInstance.qml"
} }
Component.onCompleted: { Component.onCompleted: {
+1 -1
View File
@@ -425,7 +425,7 @@ Singleton {
} }
function closeSettings() { function closeSettings() {
settingsModal?.close(); settingsModal?.hide();
} }
function toggleSettings() { function toggleSettings() {
+1 -1
View File
@@ -1 +1 @@
v1.5.2 v1.5.3
+9 -1
View File
@@ -7,12 +7,15 @@ Flow {
property var model: [] property var model: []
property int currentIndex: 0 property int currentIndex: 0
property bool multiSelect: false
property var selectedValues: []
property int chipHeight: 32 property int chipHeight: 32
property int chipPadding: Theme.spacingM property int chipPadding: Theme.spacingM
property bool showCheck: true property bool showCheck: true
property bool showCounts: true property bool showCounts: true
signal selectionChanged(int index) signal selectionChanged(int index)
signal selectionToggled(int index, bool selected)
spacing: Theme.spacingS spacing: Theme.spacingS
width: parent ? parent.width : 400 width: parent ? parent.width : 400
@@ -25,7 +28,8 @@ Flow {
required property var modelData required property var modelData
required property int index required property int index
property bool selected: index === root.currentIndex property var value: typeof modelData === "string" ? modelData : (modelData.value !== undefined ? modelData.value : (modelData.label || ""))
property bool selected: root.multiSelect ? root.selectedValues.includes(value) : (index === root.currentIndex)
property bool hovered: mouseArea.containsMouse property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed property bool pressed: mouseArea.pressed
property string label: typeof modelData === "string" ? modelData : (modelData.label || "") property string label: typeof modelData === "string" ? modelData : (modelData.label || "")
@@ -99,6 +103,10 @@ Flow {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => chipRipple.trigger(mouse.x, mouse.y) onPressed: mouse => chipRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
if (root.multiSelect) {
root.selectionToggled(chip.index, !chip.selected);
return;
}
root.currentIndex = chip.index; root.currentIndex = chip.index;
root.selectionChanged(chip.index); root.selectionChanged(chip.index);
} }
+9 -8
View File
@@ -79,6 +79,9 @@ Item {
"rightBar": 0 "rightBar": 0
}) })
property var screen: null property var screen: null
// Output the surface binds to; retargeted only in open() while hidden. A live
// screen change on a mapped surface loses the popup blur on niri.
property var surfaceScreen: null
readonly property bool frameGapStandaloneActive: CompositorService.frameConfiguredForScreen(screen) && !CompositorService.usesConnectedFrameChromeForScreen(screen) readonly property bool frameGapStandaloneActive: CompositorService.frameConfiguredForScreen(screen) && !CompositorService.usesConnectedFrameChromeForScreen(screen)
readonly property bool fluidStandaloneActive: Theme.isDirectionalEffect readonly property bool fluidStandaloneActive: Theme.isDirectionalEffect
readonly property bool backgroundDismissWindowRequired: backgroundInteractive readonly property bool backgroundDismissWindowRequired: backgroundInteractive
@@ -133,8 +136,6 @@ Item {
signal popoutClosed signal popoutClosed
signal backgroundClicked signal backgroundClicked
property var _lastOpenedScreen: null
property int effectiveBarPosition: 0 property int effectiveBarPosition: 0
property real effectiveBarBottomGap: 0 property real effectiveBarBottomGap: 0
readonly property string autoBarShadowDirection: { readonly property string autoBarShadowDirection: {
@@ -338,14 +339,14 @@ Item {
_frozenMaskWidth = maskWidth; _frozenMaskWidth = maskWidth;
_frozenMaskHeight = maskHeight; _frozenMaskHeight = maskHeight;
const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen; const screenChanged = surfaceScreen !== null && surfaceScreen !== screen;
if (screenChanged) { if (screenChanged) {
// Hide on this tick so Qt actually tears down the wl_surface; the show // Unmap before retargeting so the surface is destroyed and recreated
// gets deferred below so the unmap is processed before the remap. // on the new output rather than live-migrated.
contentWindow.visible = false; contentWindow.visible = false;
backgroundWindow.visible = false; backgroundWindow.visible = false;
} }
_lastOpenedScreen = screen; surfaceScreen = screen;
if (contentContainer && !shouldBeVisible) { if (contentContainer && !shouldBeVisible) {
// Snap morph closed only on a fresh open; on screen-change re-open we stay at 1 // Snap morph closed only on a fresh open; on screen-change re-open we stay at 1
@@ -559,7 +560,7 @@ Item {
PanelWindow { PanelWindow {
id: backgroundWindow id: backgroundWindow
screen: root.screen screen: root.surfaceScreen
visible: false visible: false
color: "transparent" color: "transparent"
// Skip buffer updates when there's nothing to render. Briefly flipped // Skip buffer updates when there's nothing to render. Briefly flipped
@@ -625,7 +626,7 @@ Item {
PanelWindow { PanelWindow {
id: contentWindow id: contentWindow
screen: root.screen screen: root.surfaceScreen
visible: false visible: false
color: "transparent" color: "transparent"
readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing
@@ -131,7 +131,6 @@
@define-color content_view_bg @view_bg_color; @define-color content_view_bg @view_bg_color;
@define-color text_view_bg @view_bg_color; @define-color text_view_bg @view_bg_color;
* { * {
padding: 0;
-GtkToolButton-icon-spacing: 4; -GtkToolButton-icon-spacing: 4;
-GtkTextView-error-underline-color: @error_bg_color; -GtkTextView-error-underline-color: @error_bg_color;
-GtkScrolledWindow-scrollbar-spacing: 0; -GtkScrolledWindow-scrollbar-spacing: 0;
@@ -44,6 +44,10 @@ if mode ~= nil then
end end
end end
if settings.terminalsAlwaysDark then
vim.o.background = "dark"
end
local current_file_path = debug.getinfo(1, "S").source:sub(2) local current_file_path = debug.getinfo(1, "S").source:sub(2)
local theme_base = deepGet(settings, { "matugenTemplateNeovimSettings", vim.o.background, "baseTheme" }) local theme_base = deepGet(settings, { "matugenTemplateNeovimSettings", vim.o.background, "baseTheme" })
or ("github_" .. vim.o.background) or ("github_" .. vim.o.background)
File diff suppressed because it is too large Load Diff