1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-06 21:48:30 -04:00

Compare commits

...

51 Commits

Author SHA1 Message Date
Huỳnh Thiện Lộc 7e0af52d5b fix(launcher): honor source badge setting (#3002)
(cherry picked from commit 7cb97f4462)
2026-08-06 00:42:38 -04:00
Huỳnh Thiện Lộc e1b6427929 fix(settings): hide removed widget tooltip (#3003)
(cherry picked from commit 82b1b36b1a)
2026-08-06 00:42:38 -04:00
Huỳnh Thiện Lộc 47bf6a8dfd fix(slideout): yield keyboard focus when modal is open on same screen (#2902)
DankSlideout always held WlrKeyboardFocus.OnDemand regardless of modals,
causing Notepad to steal focus back when Launcher was opened.

ModalManager: clone currentModalsByScreen on mutation so QML bindings
re-evaluate when modals open/close.
DankSlideout: set keyboardFocus to None while a modal is active on the
same screen, letting the modal (launcher) claim focus exclusively.

(cherry picked from commit 9981fcf529)
2026-08-06 00:42:38 -04:00
Thomas Kroll 84ac36295f fix(tray): don't dismiss click-opened tray menus from hover controller (#2979)
closeHoverSurfaces() called TrayMenuManager.closeAllMenus(), tearing down
every registered tray menu including ones opened by right-click. With
hoverPopouts enabled, moving the pointer after a right-click re-ran the
hit test and closed the menu before it could be reached.

Tag each menu with how it was opened and add closeHoverMenus(), which
only closes hover-opened menus. The hover controller now uses that.

Click-driven callers pass no argument, so byHover stays undefined and
`byHover === true` evaluates to false.

Fixes #2978

Claude-Session: https://claude.ai/code/session_01DYF9vdsaToU4Usu7kxPCNB

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 34626070af)
2026-08-06 00:42:38 -04:00
jack-mil ca839487de fix(mpris): remove required non-null artist/title from idle condition (#2993)
The current isIdle detection only works if the player has a title _and_ artist metadata,
which is not appropriate for all cases, e.g. MPV playing a video does not set artist.

In the current implementation, after a video stops playing in MPV, and the window closes
(but MPV and the MPRIS plugin server is still running as a background process), the playback controls remain
shown on the bar, and have non-functional controls.

It is sufficient to only check for the "Stopped" playing state. The MPRIS specification
states that Playback_Status==Stopped should be used to indicate that *NO* track is currently
playing. A compliant player should set that, and should also set the CanPlay/CanPause boolean
to false is not track is being played.

See: https://specifications.freedesktop.org/mpris/latest/Player_Interface.html#Enum:Playback_Status
(cherry picked from commit 49f968d26b)
2026-08-06 00:42:38 -04:00
bbedward dabbec1dce keybinds: fix labeling of spawn keybinds
fixes #2957

(cherry picked from commit 5bb884db57)
2026-08-06 00:42:37 -04:00
euletheia bd4d35d215 fix(BatteryService): fix typo in notification check (#2944)
(cherry picked from commit 8594a414ec)
2026-08-06 00:42:37 -04:00
euletheia cfdacd3ea3 fix(BatteryService): fix unresolved icons for notifications and dismiss alerts when plugged in (#2943)
* feat(BatteryService): dismiss sticky critical battery notifications

If critical notifications do not timeout, dismiss the battery-related
ones automatically when battery is plugged in.

* fix(BatteryService): use material icons for battery alerts

Since the previously specified icons are not resolved, instead reuse
icons from DMS first-party plugin for critical battery alerts.

(cherry picked from commit df396bfa43)
2026-08-06 00:42:37 -04:00
bbedward 2e558f2489 display config/niri: fix nested kdl blocks as being attributed to output
state

fixes #2966
port 1.5

(cherry picked from commit cffc33e14a)
2026-08-05 21:03:47 +00:00
bbedward 8535e3a1d7 gamma: validate config before write and publish state every scheduler
wakeup

fixes #2967
port 1.5

(cherry picked from commit 0c811e3417)
2026-08-05 20:57:22 +00:00
bbedward c2a7f767c2 bar: fix shadow from tinting goth corners
fixes #2975
port 1.5

(cherry picked from commit 6ad46cf2c2)
2026-08-05 02:15:57 +00:00
bbedward c9c11f8a27 powermenu: add soft reboot option for systemd systems
fixes #2986
port 1.5

(cherry picked from commit e089948225)
2026-08-04 22:05:11 +00:00
bbedward 8cffdab4d7 dank16: correct DPS lab lightness scale
fixes #2987
port 1.5

(cherry picked from commit 7f2ba56e06)
2026-08-04 21:56:09 +00:00
bbedward 0e8a4c0cc1 launcher: dispatch plugin-category items as plugin, not app
fixes #2988
port 1.5

(cherry picked from commit 365474b0d9)
2026-08-04 00:05:48 +00:00
bbedward 1db6381c78 evdev: resolve capslock state from any available keyboard
fixes #2991
port 1.5

(cherry picked from commit dc8a47644a)
2026-08-04 00:00:15 +00:00
bbedward d5c7efc861 dankinstall/gentoo: perform autounmask on install
fixes #2992
port 1.5

(cherry picked from commit 27483e68dc)
2026-08-03 23:40:50 +00:00
purian23 943ffb432c fix(battery): reliably apply configured power profiles at shell startup
Port 1.5

(cherry picked from commit 400a18a8ed)
2026-08-02 23:02:00 +00:00
purian23 7063448b80 refactor(media OSD): add previous/next UI controls
Port 1.5

(cherry picked from commit 32ddf614c3)
2026-08-02 22:10:33 +00:00
purian23 52afbc9801 refactor(mpris): consolidate equivalent-player resolution & update track matching
Port 1.5

(cherry picked from commit 19d919ed5c)
2026-08-02 22:09:37 +00:00
Rubén García 6edb985847 refactor(mpris): enrich metadata across equivalent players (#2928)
* fix(mpris): enrich metadata across equivalent players

* fix(mpris): address review feedback on source stealing and title stripping

- _bestPlayingPlayer: when the active player is playing but not
  controllable, only hand ownership to a controllable peer that passes
  isSameTrack; unrelated players can no longer steal the active source.
- displayTrackTitle: keep the full trackTitle for display. Suffix
  stripping now only applies to the isSameTrack matching key, and only
  for a known app-name allowlist (YouTube, SoundCloud, browsers, etc.)
  so legitimate titles containing ' | ' are never mangled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Port 1.5

(cherry picked from commit 594a2cde19)
2026-08-02 21:34:49 +00:00
bbedward 41efe6ad15 tailscale: add missing ID
related #2969
port 1.5

(cherry picked from commit ef191babb7)
2026-07-31 13:37:43 +00:00
bbedward 4fb6a17e1b launcher: fix regex escaping launch args
fixes #2961
port 1.5

(cherry picked from commit a710d6d7cc)
2026-07-30 18:35:04 +00:00
bbedward e771e3b675 greeter: lua for hypr fallback 2026-07-30 14:31:39 -04:00
bbedward b1b7aa7aa2 Revert "dock: use required properties in dock item delegate, fixes context injection on Qt 6.8 (#2926)"
This reverts commit b8e2ce1da8.
2026-07-30 14:30:51 -04:00
bbedward c128793239 qs/socket: improve resilience of socket detection and connection
related #2369
port 1.5

(cherry picked from commit 64461c534f)
2026-07-27 17:41:17 +00:00
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
92 changed files with 2493 additions and 454 deletions
+22 -7
View File
@@ -313,6 +313,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
fg := HexToRGB(hexColor) fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B} cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
Lf, af, bf := cf.Lab() Lf, af, bf := cf.Lab()
Lf *= 100.0
dir := 1.0 dir := 1.0
if isLightMode { if isLightMode {
@@ -341,6 +342,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
fg := HexToRGB(hexColor) fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B} cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
origL, af, bf := cf.Lab() origL, af, bf := cf.Lab()
origL *= 100.0
var darkerResult, lighterResult string var darkerResult, lighterResult string
darkerL, lighterL := origL, origL darkerL, lighterL := origL, origL
@@ -420,6 +422,24 @@ func blendHue(base, target, factor float64) float64 {
return result return result
} }
// color8 sits a fixed L* offset from the background so it keeps its dim role
// regardless of primary brightness (conventional palettes put ANSI bright
// black ~2-2.5:1 from the background, e.g. catppuccin-mocha #585b70 on #1e1e2e)
func DeriveDim(bgHex string, hue, sat float64, isLight bool) string {
offset := 22.0
if isLight {
offset = -offset
}
bgL := getLstar(bgHex)
targetL := math.Max(0, math.Min(100, bgL+offset))
tint := HSVToRGB(HSV{H: hue, S: sat, V: 0.5})
c := colorful.Color{R: tint.R, G: tint.G, B: tint.B}
_, af, bf := c.Lab()
return labToHex(targetL, af, bf)
}
func DeriveContainer(primary string, isLight bool) string { func DeriveContainer(primary string, isLight bool) string {
rgb := HexToRGB(primary) rgb := HexToRGB(primary)
hsv := RGBToHSV(rgb) hsv := RGBToHSV(rgb)
@@ -500,10 +520,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
gray7V := baseVal * 0.28 gray7V := baseVal * 0.28
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts)) palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.05 palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.05, true))
gray8V := baseVal * 0.85
dimTarget := secondaryTarget * 0.5
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
brightRedS := math.Min(baseSat*1.0, 1.0) brightRedS := math.Min(baseSat*1.0, 1.0)
brightRedV := math.Min(baseVal*1.2, 1.0) brightRedV := math.Min(baseVal*1.2, 1.0)
@@ -559,9 +576,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
gray7V := math.Min(baseVal*1.05, 1.0) gray7V := math.Min(baseVal*1.05, 1.0)
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts)) palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.15 palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.15, false))
gray8V := baseVal * 0.65
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
brightRedS := math.Min(baseSat*0.75, 1.0) brightRedS := math.Min(baseSat*0.75, 1.0)
brightRedV := math.Min(baseVal*1.35, 1.0) brightRedV := math.Min(baseVal*1.35, 1.0)
+70
View File
@@ -679,3 +679,73 @@ func TestContrastAlgorithmComparison(t *testing.T) {
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount) t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
} }
func TestEnsureContrastDPSLightModeStaysLight(t *testing.T) {
tests := []struct {
name string
result string
bg string
minLstar float64
}{
{
name: "bidirectional adjustment",
result: EnsureContrastDPSBidirectional("#d0ccc6", "#f8f8f8", 17.5, true),
bg: "#f8f8f8",
minLstar: 20.0,
},
{
name: "lstar adjustment",
result: EnsureContrastDPSLstar("#c0c0c0", "#f8f8f8", 30.0, true),
bg: "#f8f8f8",
minLstar: 20.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lstar := getLstar(tt.result)
if lstar < tt.minLstar {
t.Errorf("result %s has L* %.2f on light bg %s, expected >= %.2f (collapsed to near-black)",
tt.result, lstar, tt.bg, tt.minLstar)
}
})
}
}
func TestGeneratePaletteColor8Dim(t *testing.T) {
hues := []string{"#e91e63", "#f59e0b", "#22c55e", "#06b6d4", "#8b5cf6", "#ef4444"}
for _, base := range hues {
t.Run(base, func(t *testing.T) {
palette := GeneratePalette(base, PaletteOptions{IsLight: false, UseDPS: true})
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
if bgRatio < 1.5 || bgRatio > 3.0 {
t.Errorf("Color8 %s vs bg %s ratio %.2f, expected 1.5-3.0 (bright black stays near bg)",
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
sepRatio := ContrastRatio(palette.Color4.Hex, palette.Color8.Hex)
if sepRatio < 2.0 {
t.Errorf("Color4 %s vs Color8 %s ratio %.2f, expected >= 2.0 (blue must not collide with bright black)",
palette.Color4.Hex, palette.Color8.Hex, sepRatio)
}
})
}
}
func TestGeneratePaletteLightColor8StaysLight(t *testing.T) {
palette := GeneratePalette("#f59e0b", PaletteOptions{IsLight: true, UseDPS: true})
lstar := getLstar(palette.Color8.Hex)
if lstar < 60.0 {
t.Errorf("light mode Color8 %s has L* %.2f, expected >= 60 (dim grey, not near-black)",
palette.Color8.Hex, lstar)
}
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
if bgRatio > 3.0 {
t.Errorf("light mode Color8 %s vs bg %s ratio %.2f, expected <= 3.0",
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
}
+9 -7
View File
@@ -53,6 +53,11 @@ func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDi
} }
} }
func emergeInstallArgs(packages []string) []string {
args := []string{"emerge", "--ask=n", "--quiet", "--autounmask-continue=y", "--autounmask-keep-keywords=y", "--autounmask-keep-masks=y"}
return append(args, packages...)
}
func (g *GentooDistribution) getArchKeyword() string { func (g *GentooDistribution) getArchKeyword() string {
arch := runtime.GOARCH arch := runtime.GOARCH
switch arch { switch arch {
@@ -319,6 +324,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
g.log("Portage tree synced successfully") g.log("Portage tree synced successfully")
args := emergeInstallArgs(missingPkgs)
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", "))) g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites, Phase: PhasePrerequisites,
@@ -326,12 +332,10 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)), Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
} }
args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, missingPkgs...)
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -521,8 +525,7 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
} }
} }
args := []string{"emerge", "--ask=n", "--quiet"} args := emergeInstallArgs(packageNames)
args = append(args, packageNames...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -713,8 +716,7 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
guruPackages[i] = pkg + "::guru" guruPackages[i] = pkg + "::guru"
} }
args := []string{"emerge", "--ask=n", "--quiet"} args := emergeInstallArgs(guruPackages)
args = append(args, guruPackages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
+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
+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{}
+25 -21
View File
@@ -48,7 +48,7 @@ func NewManager() (*Manager, error) {
return nil, fmt.Errorf("failed to find keyboards: %w", err) return nil, fmt.Errorf("failed to find keyboards: %w", err)
} }
initialCapsLock := readInitialCapsLockState(devices[0]) initialCapsLock, _ := capsLockFromDevices(devices)
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
@@ -85,14 +85,21 @@ func NewManager() (*Manager, error) {
return m, nil return m, nil
} }
func readInitialCapsLockState(device EvdevDevice) bool { func capsLockFromDevices(devices []EvdevDevice) (bool, bool) {
ledStates, err := device.State(evLedType) for _, device := range devices {
if err != nil { if device == nil {
log.Debugf("Could not read LED state: %v", err) continue
return false }
ledStates, err := device.State(evLedType)
if err != nil || len(ledStates) == 0 {
continue
}
return ledStates[ledCapslockKey], true
} }
return ledStates[ledCapslockKey] return false, false
} }
func findKeyboards() ([]EvdevDevice, error) { func findKeyboards() ([]EvdevDevice, error) {
@@ -297,25 +304,22 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
m.devicesMutex.RUnlock() m.devicesMutex.RUnlock()
return return
} }
device := m.devices[deviceIndex] ordered := make([]EvdevDevice, 0, len(m.devices))
ordered = append(ordered, m.devices[deviceIndex])
for i, device := range m.devices {
if i == deviceIndex {
continue
}
ordered = append(ordered, device)
}
m.devicesMutex.RUnlock() m.devicesMutex.RUnlock()
ledStates, err := device.State(evLedType) capsLockState, ok := capsLockFromDevices(ordered)
if err != nil { if !ok {
log.Warnf("Failed to read LED state: %v", err) log.Debug("No LED-capable device available for caps lock state")
return return
} }
if len(ledStates) == 0 {
log.Debug("No LED state available (empty map)")
// This means the device either:
// - doesn't support LED reporting at all, or
// - the kernel returned an empty state
return
}
capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState) m.updateCapsLockStateDirect(capsLockState)
} }
+22 -4
View File
@@ -306,7 +306,7 @@ func TestNotifySubscribers(t *testing.T) {
m.Close() m.Close()
} }
func TestReadInitialCapsLockState(t *testing.T) { func TestCapsLockFromDevices(t *testing.T) {
t.Run("caps lock is on", func(t *testing.T) { t.Run("caps lock is on", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t) mockDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{ ledState := evdev.StateMap{
@@ -314,7 +314,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
} }
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.True(t, ok)
assert.True(t, result) assert.True(t, result)
}) })
@@ -325,7 +326,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
} }
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.True(t, ok)
assert.False(t, result) assert.False(t, result)
}) })
@@ -333,9 +335,25 @@ func TestReadInitialCapsLockState(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t) mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.False(t, ok)
assert.False(t, result) assert.False(t, result)
}) })
t.Run("falls back past device without LED state", func(t *testing.T) {
noLedDevice := mocks.NewMockEvdevDevice(t)
noLedDevice.EXPECT().State(evdev.EvType(evLedType)).Return(evdev.StateMap{}, nil).Once()
ledDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{
ledCapslockKey: true,
}
ledDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result, ok := capsLockFromDevices([]EvdevDevice{noLedDevice, nil, ledDevice})
assert.True(t, ok)
assert.True(t, result)
})
} }
func TestHasInputGroupAccess(t *testing.T) { func TestHasInputGroupAccess(t *testing.T) {
+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])
}
}
}
+31 -20
View File
@@ -760,6 +760,9 @@ func (m *Manager) schedulerLoop() {
now := time.Now() now := time.Now()
m.recalcSchedule(now) m.recalcSchedule(now)
// publish independent of output readiness so night status never
// presents a stale schedule while applies are blocked (#2967)
m.updateStateFromSchedule()
var waitDur time.Duration var waitDur time.Duration
if enabled { if enabled {
@@ -1120,13 +1123,15 @@ func (m *Manager) SetTemperature(low, high int) error {
m.configMutex.Unlock() m.configMutex.Unlock()
return nil return nil
} }
m.config.LowTemp = low updated := m.config
m.config.HighTemp = high updated.LowTemp = low
err := m.config.Validate() updated.HighTemp = high
m.configMutex.Unlock() if err := updated.Validate(); err != nil {
if err != nil { m.configMutex.Unlock()
return err return err
} }
m.config = updated
m.configMutex.Unlock()
m.triggerUpdate() m.triggerUpdate()
return nil return nil
} }
@@ -1138,14 +1143,16 @@ func (m *Manager) SetLocation(lat, lon float64) error {
m.configMutex.Unlock() m.configMutex.Unlock()
return nil return nil
} }
m.config.Latitude = &lat updated := m.config
m.config.Longitude = &lon updated.Latitude = &lat
m.config.UseIPLocation = false updated.Longitude = &lon
err := m.config.Validate() updated.UseIPLocation = false
m.configMutex.Unlock() if err := updated.Validate(); err != nil {
if err != nil { m.configMutex.Unlock()
return err return err
} }
m.config = updated
m.configMutex.Unlock()
m.triggerUpdate() m.triggerUpdate()
return nil return nil
} }
@@ -1180,13 +1187,15 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
m.configMutex.Unlock() m.configMutex.Unlock()
return nil return nil
} }
m.config.ManualSunrise = &sunrise updated := m.config
m.config.ManualSunset = &sunset updated.ManualSunrise = &sunrise
err := m.config.Validate() updated.ManualSunset = &sunset
m.configMutex.Unlock() if err := updated.Validate(); err != nil {
if err != nil { m.configMutex.Unlock()
return err return err
} }
m.config = updated
m.configMutex.Unlock()
m.triggerUpdate() m.triggerUpdate()
return nil return nil
} }
@@ -1209,12 +1218,14 @@ func (m *Manager) SetGamma(gamma float64) error {
m.configMutex.Unlock() m.configMutex.Unlock()
return nil return nil
} }
m.config.Gamma = gamma updated := m.config
err := m.config.Validate() updated.Gamma = gamma
m.configMutex.Unlock() if err := updated.Validate(); err != nil {
if err != nil { m.configMutex.Unlock()
return err return err
} }
m.config = updated
m.configMutex.Unlock()
m.triggerUpdate() m.triggerUpdate()
return nil return nil
} }
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
) )
func TestManager_ActorSerializesOutputStateAccess(t *testing.T) { func TestManager_ActorSerializesOutputStateAccess(t *testing.T) {
@@ -412,3 +413,75 @@ func TestNewManager_InvalidConfig(t *testing.T) {
_, err := NewManager(mockDisplay, config) _, err := NewManager(mockDisplay, config)
assert.Error(t, err) assert.Error(t, err)
} }
func TestSetters_RejectedValuesLeaveConfigUntouched(t *testing.T) {
newManager := func() *Manager {
return &Manager{
config: DefaultConfig(),
updateTrigger: make(chan struct{}, 1),
}
}
t.Run("SetTemperature", func(t *testing.T) {
m := newManager()
before := m.config
err := m.SetTemperature(3200, 2500)
assert.Error(t, err)
assert.Equal(t, before, m.config)
assert.Empty(t, m.updateTrigger)
})
t.Run("SetLocation", func(t *testing.T) {
m := newManager()
before := m.config
err := m.SetLocation(120.0, 10.0)
assert.Error(t, err)
assert.Equal(t, before, m.config)
assert.Empty(t, m.updateTrigger)
})
t.Run("SetGamma", func(t *testing.T) {
m := newManager()
before := m.config
err := m.SetGamma(-1.0)
assert.Error(t, err)
assert.Equal(t, before, m.config)
assert.Empty(t, m.updateTrigger)
})
}
func TestSetters_ValidValuesCommitAndTrigger(t *testing.T) {
m := &Manager{
config: DefaultConfig(),
updateTrigger: make(chan struct{}, 1),
}
err := m.SetTemperature(3000, 6000)
assert.NoError(t, err)
assert.Equal(t, 3000, m.config.LowTemp)
assert.Equal(t, 6000, m.config.HighTemp)
assert.Len(t, m.updateTrigger, 1)
}
func TestApplyGamma_SkipsUnchangedTempAndGamma(t *testing.T) {
m := &Manager{config: DefaultConfig()}
m.controlsInitialized = true
out := &outputState{
id: 1,
rampSize: 256,
gammaControl: &wlr_gamma_control.ZwlrGammaControlV1{},
lastTemp: 5000,
lastGamma: m.config.Gamma,
}
m.outputs.Store(out.id, out)
m.applyGamma(5000)
assert.False(t, out.failed, "unchanged temp must not reach the compositor write path")
assert.Equal(t, 5000, out.lastTemp)
assert.Equal(t, uint32(256), out.rampSize)
}
@@ -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();
+11 -2
View File
@@ -15,7 +15,11 @@ Singleton {
function openModal(modal) { function openModal(modal) {
PopoutManager.screenshotActive = false; PopoutManager.screenshotActive = false;
const screenName = modal.effectiveScreen?.name ?? "unknown"; const screenName = modal.effectiveScreen?.name ?? "unknown";
currentModalsByScreen[screenName] = modal; var next = {};
for (var k in currentModalsByScreen)
next[k] = currentModalsByScreen[k];
next[screenName] = modal;
currentModalsByScreen = next;
modalChanged(); modalChanged();
Qt.callLater(() => { Qt.callLater(() => {
if (!modal.allowStacking) if (!modal.allowStacking)
@@ -34,7 +38,12 @@ Singleton {
function closeModal(modal) { function closeModal(modal) {
const screenName = modal.effectiveScreen?.name ?? "unknown"; const screenName = modal.effectiveScreen?.name ?? "unknown";
if (currentModalsByScreen[screenName] === modal) { if (currentModalsByScreen[screenName] === modal) {
delete currentModalsByScreen[screenName]; var next = {};
for (var k in currentModalsByScreen) {
if (k !== screenName)
next[k] = currentModalsByScreen[k];
}
currentModalsByScreen = next;
modalChanged(); modalChanged();
} }
} }
+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;
+12
View File
@@ -22,6 +22,18 @@ Singleton {
activeTrayMenus = newMenus activeTrayMenus = newMenus
} }
function closeHoverMenus() {
for (const screenName in activeTrayMenus) {
const menu = activeTrayMenus[screenName]
if (!menu || menu.openedByHover !== true) continue
if (typeof menu.close === "function") {
menu.close()
} else if (menu.showMenu !== undefined) {
menu.showMenu = false
}
}
}
function closeAllMenus() { function closeAllMenus() {
for (const screenName in activeTrayMenus) { for (const screenName in activeTrayMenus) {
const menu = activeTrayMenus[screenName] const menu = activeTrayMenus[screenName]
@@ -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;
} }
+3
View File
@@ -982,6 +982,9 @@ Item {
case "reboot": case "reboot":
SessionService.reboot(); SessionService.reboot();
break; break;
case "softreboot":
SessionService.softReboot();
break;
case "poweroff": case "poweroff":
SessionService.poweroff(); SessionService.poweroff();
break; break;
+25 -12
View File
@@ -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");
@@ -814,16 +819,24 @@ Item {
} }
if (isCategoryFiltered) { if (isCategoryFiltered) {
var rawApps = AppSearchService.getAppsInCategory(appCategory); var categoryPluginId = AppSearchService.getPluginIdForCategory(appCategory);
for (var i = 0; i < rawApps.length; i++) { if (categoryPluginId) {
allItems.push(getOrTransformApp(rawApps[i])); var pluginCategoryItems = getPluginItems(categoryPluginId, "");
} for (var i = 0; i < pluginCategoryItems.length; i++) {
// Also include core apps (DMS Settings etc.) that match this category allItems.push(pluginCategoryItems[i]);
var allCoreApps = AppSearchService.getCoreApps(""); }
for (var i = 0; i < allCoreApps.length; i++) { } else {
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]); var rawApps = AppSearchService.getAppsInCategory(appCategory);
if (coreAppCats.indexOf(appCategory) !== -1) for (var i = 0; i < rawApps.length; i++) {
allItems.push(transformCoreApp(allCoreApps[i])); allItems.push(getOrTransformApp(rawApps[i]));
}
// Also include core apps (DMS Settings etc.) that match this category
var allCoreApps = AppSearchService.getCoreApps("");
for (var i = 0; i < allCoreApps.length; i++) {
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
if (coreAppCats.indexOf(appCategory) !== -1)
allItems.push(transformCoreApp(allCoreApps[i]));
}
} }
} else { } else {
var apps = searchApps(searchQuery); var apps = searchApps(searchQuery);
@@ -834,7 +847,7 @@ Item {
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500); var newSections = Scorer.groupBySection(scoredItems, buildDynamicSectionDefs(allItems), sortAlpha, searchQuery ? 50 : 500);
for (var sid in collapsedSections) { for (var sid in collapsedSections) {
for (var i = 0; i < newSections.length; i++) { for (var i = 0; i < newSections.length; i++) {
@@ -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);
} }
@@ -7,6 +7,7 @@ Item {
property string source: "" property string source: ""
property int glyphSize: 14 property int glyphSize: 14
property bool badgeVisible: true
readonly property var sourceAsset: ({ readonly property var sourceAsset: ({
"flatpak": "../../assets/package-sources/flatpak.svg", "flatpak": "../../assets/package-sources/flatpak.svg",
@@ -17,7 +18,7 @@ Item {
readonly property string assetPath: sourceAsset[source] || "" readonly property string assetPath: sourceAsset[source] || ""
visible: SettingsData.dankLauncherV2ShowSourceBadges && assetPath.length > 0 visible: badgeVisible && SettingsData.dankLauncherV2ShowSourceBadges && assetPath.length > 0
implicitWidth: glyphSize implicitWidth: glyphSize
implicitHeight: glyphSize implicitHeight: glyphSize
@@ -178,7 +178,7 @@ Rectangle {
anchors.margins: Theme.spacingXS anchors.margins: Theme.spacingXS
source: root.item?.type === "app" ? (root.item.source || "") : "" source: root.item?.type === "app" ? (root.item.source || "") : ""
glyphSize: 16 glyphSize: 16
visible: !root.isSelected && !!source badgeVisible: !root.isSelected
} }
} }
} }
+26 -6
View File
@@ -175,6 +175,8 @@ DankModal {
visibleActions = allActions.filter(action => { visibleActions = allActions.filter(action => {
if (action === "hibernate" && !SessionService.hibernateSupported) if (action === "hibernate" && !SessionService.hibernateSupported)
return false; return false;
if (action === "softreboot" && !SessionService.softRebootSupported)
return false;
return true; return true;
}); });
@@ -223,6 +225,12 @@ DankModal {
"label": I18n.tr("Reboot"), "label": I18n.tr("Reboot"),
"key": "R" "key": "R"
}; };
case "softreboot":
return {
"icon": "autorenew",
"label": I18n.tr("Soft Reboot"),
"key": "B"
};
case "logout": case "logout":
return { return {
"icon": "logout", "icon": "logout",
@@ -370,7 +378,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_B || 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))) {
cancelHold(); cancelHold();
event.accepted = true; event.accepted = true;
} }
@@ -429,6 +437,12 @@ DankModal {
event.accepted = true; event.accepted = true;
} }
break; break;
case Qt.Key_B:
if (visibleActions.includes("softreboot")) {
startHold("softreboot", visibleActions.indexOf("softreboot"));
event.accepted = true;
}
break;
case Qt.Key_X: case Qt.Key_X:
if (visibleActions.includes("logout")) { if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout")); startHold("logout", visibleActions.indexOf("logout"));
@@ -464,7 +478,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_B || 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))) {
cancelHold(); cancelHold();
event.accepted = true; event.accepted = true;
} }
@@ -539,6 +553,12 @@ DankModal {
event.accepted = true; event.accepted = true;
} }
break; break;
case Qt.Key_B:
if (visibleActions.includes("softreboot")) {
startHold("softreboot", visibleActions.indexOf("softreboot"));
event.accepted = true;
}
break;
case Qt.Key_X: case Qt.Key_X:
if (visibleActions.includes("logout")) { if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout")); startHold("logout", visibleActions.indexOf("logout"));
@@ -597,7 +617,7 @@ DankModal {
readonly property var actionData: root.getActionData(modelData) readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff" readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
@@ -627,7 +647,7 @@ DankModal {
color: { color: {
if (gridButtonRect.modelData === "poweroff") if (gridButtonRect.modelData === "poweroff")
return Theme.errorSelected; return Theme.errorSelected;
if (gridButtonRect.modelData === "reboot") if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot")
return Theme.withAlpha(Theme.warning, 0.3); return Theme.withAlpha(Theme.warning, 0.3);
return Theme.primarySelected; return Theme.primarySelected;
} }
@@ -722,7 +742,7 @@ DankModal {
readonly property var actionData: root.getActionData(modelData) readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff" readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: parent.width width: parent.width
@@ -752,7 +772,7 @@ DankModal {
color: { color: {
if (listButtonRect.modelData === "poweroff") if (listButtonRect.modelData === "poweroff")
return Theme.errorSelected; return Theme.errorSelected;
if (listButtonRect.modelData === "reboot") if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot")
return Theme.withAlpha(Theme.warning, 0.3); return Theme.withAlpha(Theme.warning, 0.3);
return Theme.primarySelected; return Theme.primarySelected;
} }
+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
@@ -469,6 +469,8 @@ PluginComponent {
} }
MouseArea { MouseArea {
id: peerMouseArea
z: -1 z: -1
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
@@ -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);
+10 -1
View File
@@ -201,7 +201,16 @@ Item {
direction: root.effectiveShadowDirection direction: root.effectiveShadowDirection
fallbackOffset: 4 fallbackOffset: 4
targetRadius: root.rt targetRadius: root.rt
targetColor: barWindow._bgColor // wing-side body corners are square where the gothic fillets attach;
// rounding the shadow there leaves a shadow-filled notch under the
// translucent bar fill (#2975)
topLeftRadius: root.gothEnabled && (root.isBottom || root.isRight) ? 0 : root.rt
topRightRadius: root.gothEnabled && (root.isBottom || root.isLeft) ? 0 : root.rt
bottomLeftRadius: root.gothEnabled && (root.isTop || root.isRight) ? 0 : root.rt
bottomRightRadius: root.gothEnabled && (root.isTop || root.isLeft) ? 0 : root.rt
// barShape below is the sole painter of the bar; a fill here doubles
// the alpha of translucent bars while the wings stay single-painted
targetColor: "transparent"
shadowBlurPx: root.shadowBlurPx shadowBlurPx: root.shadowBlurPx
shadowOffsetX: root.shadowOffsetX shadowOffsetX: root.shadowOffsetX
@@ -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() {
@@ -609,7 +609,7 @@ Item {
_closeHoverNotepad(); _closeHoverNotepad();
activeHoverTrigger = ""; activeHoverTrigger = "";
PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen); PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen);
TrayMenuManager.closeAllMenus(); TrayMenuManager.closeHoverMenus();
} }
function _beginSupersededCloseForActive() { function _beginSupersededCloseForActive() {
+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
}; };
} }
@@ -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 {
@@ -1484,6 +1484,7 @@ BasePill {
property bool isVertical: false property bool isVertical: false
property var axis: null property var axis: null
property bool showMenu: false property bool showMenu: false
property bool openedByHover: false
property var menuHandle: null property var menuHandle: null
ListModel { ListModel {
@@ -1493,7 +1494,8 @@ BasePill {
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null; return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
} }
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj, byHover) {
openedByHover = byHover === true;
trayItem = item; trayItem = item;
anchorItem = anchor; anchorItem = anchor;
parentScreen = screen; parentScreen = screen;
@@ -2084,7 +2086,7 @@ BasePill {
} }
} }
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj, byHover) {
if (!screen) if (!screen)
return; return;
if (currentTrayMenu) { if (currentTrayMenu) {
@@ -2099,7 +2101,7 @@ BasePill {
currentTrayMenu = trayMenuComponent.createObject(null); currentTrayMenu = trayMenuComponent.createObject(null);
if (!currentTrayMenu) if (!currentTrayMenu)
return; return;
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj); currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj, byHover === true);
} }
function _trayLayoutRoot() { function _trayLayoutRoot() {
@@ -2147,7 +2149,7 @@ BasePill {
if (!hit?.trayItem?.hasMenu) if (!hit?.trayItem?.hasMenu)
return false; return false;
const anchor = hit.children?.length > 0 ? hit.children[0] : hit; const anchor = hit.children?.length > 0 ? hit.children[0] : hit;
showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis); showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis, true);
return true; return true;
} }
} }
@@ -490,7 +490,7 @@ Item {
} }
StyledText { StyledText {
text: activePlayer?.trackAlbum || "" text: MprisController.stableAlbum
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextSecondary color: Theme.surfaceTextSecondary
width: parent.width width: parent.width
+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 {
@@ -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
+12 -8
View File
@@ -386,16 +386,20 @@ end)
HYPRLAND_LUA_EOF HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp --suffix=.lua)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
env = DMS_RUN_GREETER,1 hl.env("DMS_RUN_GREETER", "1")
misc { hl.config({
disable_hyprland_logo = true misc = {
} disable_hyprland_logo = true,
},
})
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit" hl.on("hyprland.start", function()
HYPRLAND_EOF hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
else else
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
+5 -1
View File
@@ -10,6 +10,7 @@ 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
@@ -36,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();
} }
} }
@@ -59,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();
@@ -74,6 +77,7 @@ PanelWindow {
fadeSeq.stop(); fadeSeq.stop();
fadeOverlay.opacity = 0.0; fadeOverlay.opacity = 0.0;
active = false; active = false;
_completed = false;
} }
Connections { Connections {
@@ -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 {
+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()
@@ -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"
+89 -23
View File
@@ -156,9 +156,10 @@ DankOSD {
if (MprisController.isFirefoxYoutubeHoverPreview(player)) if (MprisController.isFirefoxYoutubeHoverPreview(player))
return; return;
const newTitle = player.trackTitle || ""; const metaPlayer = MprisController.bestMetadataPlayer(player);
const newArtist = player.trackArtist || ""; const newTitle = MprisController.displayTrackTitle(metaPlayer);
const newAlbum = player.trackAlbum || ""; const newArtist = metaPlayer.trackArtist || "";
const newAlbum = metaPlayer.trackAlbum || "";
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum; const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
root._displayTitle = newTitle; root._displayTitle = newTitle;
@@ -263,37 +264,102 @@ DankOSD {
} }
} }
Rectangle { Row {
width: Theme.iconSize id: transportControls
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap x: parent.gap
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
DankIcon { Rectangle {
anchors.centerIn: parent width: Theme.iconSize - 4
name: root._displayIcon height: Theme.iconSize - 4
size: Theme.iconSize radius: (Theme.iconSize - 4) / 2
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText anchors.verticalCenter: parent.verticalCenter
color: prevButton.containsMouse ? Theme.surfaceTextHover : "transparent"
opacity: (root.player?.canGoPrevious ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: Theme.iconSize - 10
color: prevButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: prevButton
anchors.fill: parent
hoverEnabled: true
enabled: root.player?.canGoPrevious ?? false
cursorShape: Qt.PointingHandCursor
onClicked: {
MprisController.previousOrRewind();
root.hide();
}
}
} }
MouseArea { Rectangle {
id: playPauseButton width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
anchors.verticalCenter: parent.verticalCenter
color: "transparent"
anchors.fill: parent DankIcon {
hoverEnabled: true anchors.centerIn: parent
cursorShape: Qt.PointingHandCursor name: root._displayIcon
onClicked: { size: Theme.iconSize
togglePlaying(); color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
root.hide(); }
MouseArea {
id: playPauseButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
togglePlaying();
root.hide();
}
}
}
Rectangle {
width: Theme.iconSize - 4
height: Theme.iconSize - 4
radius: (Theme.iconSize - 4) / 2
anchors.verticalCenter: parent.verticalCenter
color: nextButton.containsMouse ? Theme.surfaceTextHover : "transparent"
opacity: (root.player?.canGoNext ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: Theme.iconSize - 10
color: nextButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: nextButton
anchors.fill: parent
hoverEnabled: true
enabled: root.player?.canGoNext ?? false
cursorShape: Qt.PointingHandCursor
onClicked: {
MprisController.next();
root.hide();
}
} }
} }
} }
Column { Column {
x: parent.gap * 2 + Theme.iconSize x: parent.gap * 2 + transportControls.width
width: parent.width - Theme.iconSize - parent.gap * 3 width: parent.width - transportControls.width - parent.gap * 3
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS spacing: Theme.spacingXXS
@@ -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;
@@ -1084,28 +1088,50 @@ Singleton {
} }
} }
function extractNiriOutputBlocks(content) {
const blocks = [];
const headerRegex = /output\s+"([^"]+)"\s*\{/g;
let match;
while ((match = headerRegex.exec(content)) !== null) {
const start = headerRegex.lastIndex;
let depth = 1;
let i = start;
while (i < content.length && depth > 0) {
const ch = content[i];
if (ch === '{')
depth++;
else if (ch === '}')
depth--;
i++;
}
blocks.push({
"name": match[1],
"body": content.slice(start, i - 1)
});
headerRegex.lastIndex = i;
}
return blocks;
}
function stripNestedBlocks(body) {
let stripped = body;
let prev;
do {
prev = stripped;
stripped = stripped.replace(/[\w-]+\s*\{[^{}]*\}/g, "");
} while (stripped !== prev)
return stripped;
}
function parseNiriOutputs(content) { function parseNiriOutputs(content) {
const result = {}; const result = {};
const outputRegex = /output\s+"([^"]+)"\s*\{([^}]*)\}/g; for (const block of extractNiriOutputBlocks(content)) {
let match; const name = block.name;
while ((match = outputRegex.exec(content)) !== null) { const body = block.body;
const name = match[1];
const body = match[2];
if (body.trim() === "off") {
result[name] = {
"name": name,
"disabled": true,
"logical": {
"x": 0,
"y": 0,
"scale": 1.0,
"transform": "Normal"
}
};
continue;
}
// off marks the output disabled only at the top level of its block;
// nested sections like hot-corners { off } must not count (#2966)
const disabled = /^\s*off\s*$/m.test(stripNestedBlocks(body));
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 +1141,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 +1805,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 +1849,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 +1864,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 {}
+15 -3
View File
@@ -400,8 +400,8 @@ Item {
settingKey: "powerMenuDefaultAction" settingKey: "powerMenuDefaultAction"
tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"] tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"]
text: I18n.tr("Default selected action") text: I18n.tr("Default selected action")
options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate")] options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate"), I18n.tr("Soft Reboot")]
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"] property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate", "softreboot"]
Component.onCompleted: { Component.onCompleted: {
const currentAction = SettingsData.powerMenuDefaultAction || "logout"; const currentAction = SettingsData.powerMenuDefaultAction || "logout";
@@ -465,6 +465,12 @@ Item {
label: I18n.tr("Show Hibernate"), label: I18n.tr("Show Hibernate"),
desc: I18n.tr("Only visible if hibernate is supported by your system"), desc: I18n.tr("Only visible if hibernate is supported by your system"),
hibernate: true hibernate: true
},
{
key: "softreboot",
label: I18n.tr("Show Soft Reboot"),
desc: I18n.tr("Restart userspace without rebooting the kernel, requires systemd"),
softreboot: true
} }
] ]
@@ -474,7 +480,13 @@ Item {
tags: ["power", "menu", "action", "show", modelData.key] tags: ["power", "menu", "action", "show", modelData.key]
text: modelData.label text: modelData.label
description: modelData.desc || "" description: modelData.desc || ""
visible: !modelData.hibernate || SessionService.hibernateSupported visible: {
if (modelData.hibernate)
return SessionService.hibernateSupported;
if (modelData.softreboot)
return SessionService.softRebootSupported;
return true;
}
checked: SettingsData.powerMenuActions.includes(modelData.key) checked: SettingsData.powerMenuActions.includes(modelData.key)
onToggled: checked => { onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]; let actions = [...SettingsData.powerMenuActions];
@@ -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
@@ -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;
@@ -18,6 +18,8 @@ Column {
id: sharedTooltip id: sharedTooltip
} }
Component.onDestruction: sharedTooltip.hide()
signal itemEnabledChanged(string sectionId, string itemId, bool enabled) signal itemEnabledChanged(string sectionId, string itemId, bool enabled)
signal itemOrderChanged(string sectionId, var orderedIds) signal itemOrderChanged(string sectionId, var orderedIds)
signal addWidget(string sectionId) signal addWidget(string sectionId)
@@ -1215,6 +1217,7 @@ Column {
iconSize: 18 iconSize: 18
iconColor: Theme.error iconColor: Theme.error
onClicked: { onClicked: {
sharedTooltip.hide();
root.removeWidget(root.sectionId, index); root.removeWidget(root.sectionId, index);
} }
onEntered: { onEntered: {
+18 -22
View File
@@ -749,16 +749,24 @@ Singleton {
if (category === I18n.tr("All")) if (category === I18n.tr("All"))
return visibleApps; return visibleApps;
const pluginItems = getPluginItems(category, "");
if (pluginItems.length > 0)
return pluginItems;
return visibleApps.filter(app => { return visibleApps.filter(app => {
const appCategories = getCategoriesForApp(app); const appCategories = getCategoriesForApp(app);
return appCategories.includes(category); return appCategories.includes(category);
}); });
} }
function getPluginIdForCategory(category) {
if (typeof PluginService === "undefined")
return null;
const launchers = PluginService.getLauncherPlugins();
for (const pluginId in launchers) {
if ((launchers[pluginId].name || pluginId) === category)
return pluginId;
}
return null;
}
// Plugin launcher support functions // Plugin launcher support functions
function getPluginCategories() { function getPluginCategories() {
if (typeof PluginService === "undefined") { if (typeof PluginService === "undefined") {
@@ -778,17 +786,11 @@ Singleton {
} }
function getPluginCategoryIcon(category) { function getPluginCategoryIcon(category) {
if (typeof PluginService === "undefined") const pluginId = getPluginIdForCategory(category);
if (!pluginId)
return null; return null;
const launchers = PluginService.getLauncherPlugins(); return PluginService.getLauncherPlugins()[pluginId].icon || "extension";
for (const pluginId in launchers) {
const plugin = launchers[pluginId];
if ((plugin.name || pluginId) === category) {
return plugin.icon || "extension";
}
}
return null;
} }
function getAllPluginItems() { function getAllPluginItems() {
@@ -809,17 +811,11 @@ Singleton {
} }
function getPluginItems(category, query) { function getPluginItems(category, query) {
if (typeof PluginService === "undefined") const pluginId = getPluginIdForCategory(category);
if (!pluginId)
return []; return [];
const launchers = PluginService.getLauncherPlugins(); return getPluginItemsForPlugin(pluginId, query);
for (const pluginId in launchers) {
const plugin = launchers[pluginId];
if ((plugin.name || pluginId) === category) {
return getPluginItemsForPlugin(pluginId, query);
}
}
return [];
} }
function getPluginItemsForPlugin(pluginId, query) { function getPluginItemsForPlugin(pluginId, query) {
+46 -10
View File
@@ -17,7 +17,29 @@ Singleton {
interval: 500 interval: 500
repeat: false repeat: false
running: true running: true
onTriggered: root.suppressSound = false onTriggered: {
root.suppressSound = false;
root.applyPowerProfile();
}
}
Connections {
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
function onHasPerformanceProfileChanged() {
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;
PowerProfileWatcher.applyProfile(targetProfile);
} }
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY") readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
@@ -121,7 +143,7 @@ Singleton {
function sendAlert(title, message, isWarning, category, notificationType) { function sendAlert(title, message, isWarning, category, notificationType) {
if (notificationType === 1) { if (notificationType === 1) {
Quickshell.execDetached(["notify-send", "-u", isWarning ? "critical" : "normal", "-a", "DMS", "-i", isWarning ? "battery-caution" : "battery-charging", title, message]); Quickshell.execDetached(["notify-send", "-u", isWarning ? "critical" : "normal", "-a", "DMS", "-i", isWarning ? "material:battery_alert" : "material:battery_charging_full", title, message]);
} else { } else {
if (isWarning) { if (isWarning) {
ToastService.showWarning(title, message, "", category); ToastService.showWarning(title, message, "", category);
@@ -135,7 +157,7 @@ Singleton {
if (isCharging && batteryLevel >= SettingsData.batteryChargeLimit) { if (isCharging && batteryLevel >= SettingsData.batteryChargeLimit) {
if (!_hasNotifiedChargeLimit && SettingsData.batteryNotifyChargeLimit) { if (!_hasNotifiedChargeLimit && SettingsData.batteryNotifyChargeLimit) {
_hasNotifiedChargeLimit = true; _hasNotifiedChargeLimit = true;
sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%").arg(SettingsData.batteryChargeLimit), false, "battery-charge-limit", SettingsData.batteryChargeLimitNotificationType); sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%").arg(SettingsData.batteryChargeLimit), false, "material:battery_profile", SettingsData.batteryChargeLimitNotificationType);
} }
} else if (!isCharging || batteryLevel < SettingsData.batteryChargeLimit - 2) { } else if (!isCharging || batteryLevel < SettingsData.batteryChargeLimit - 2) {
_hasNotifiedChargeLimit = false; _hasNotifiedChargeLimit = false;
@@ -151,7 +173,7 @@ Singleton {
if (isCriticalBattery) { if (isCriticalBattery) {
if (!_hasNotifiedCriticalBattery && SettingsData.batteryNotifyCritical) { if (!_hasNotifiedCriticalBattery && SettingsData.batteryNotifyCritical) {
_hasNotifiedCriticalBattery = true; _hasNotifiedCriticalBattery = true;
sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!").arg(batteryLevel), true, "battery-critical", SettingsData.batteryCriticalNotificationType); sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!").arg(batteryLevel), true, "material:battery_alert", SettingsData.batteryCriticalNotificationType);
} }
return; return;
} }
@@ -164,7 +186,7 @@ Singleton {
if (isLowBattery) { if (isLowBattery) {
if (!_hasNotifiedLowBattery && SettingsData.batteryNotifyLow) { if (!_hasNotifiedLowBattery && SettingsData.batteryNotifyLow) {
_hasNotifiedLowBattery = true; _hasNotifiedLowBattery = true;
sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon").arg(batteryLevel), true, "battery-low", SettingsData.batteryLowNotificationType); sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon").arg(batteryLevel), true, "material:battery_0_bar", SettingsData.batteryLowNotificationType);
} }
if (SettingsData.batteryAutoPowerSaver && PowerProfileWatcher.available) { if (SettingsData.batteryAutoPowerSaver && PowerProfileWatcher.available) {
@@ -206,12 +228,26 @@ Singleton {
} }
} }
const profileValue = BatteryService.isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName; applyPowerProfile();
if (profileValue !== "") { if (isPluggedIn) {
const targetProfile = parseInt(profileValue); const dismissLow = SettingsData.batteryLowNotificationType === 1 && SettingsData.notificationTimeoutNormal === 0;
if (!isNaN(targetProfile) && PowerProfiles.profile !== targetProfile) { const dismissCritical = SettingsData.batteryCriticalNotificationType === 1 && SettingsData.notificationTimeoutCritical === 0;
PowerProfiles.profile = targetProfile;
if (dismissLow || dismissCritical) {
const lowSummary = I18n.tr("Low Battery");
const criticalSummary = I18n.tr("Critical Battery");
for (const w of NotificationService.visibleNotifications) {
if (!w || !w.notification)
continue;
const summary = w.notification.summary;
if ((dismissLow && summary === lowSummary) || (dismissCritical && summary === criticalSummary)) {
NotificationService.dismissNotification(w);
}
}
} }
} }
+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 -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);
+38 -54
View File
@@ -11,7 +11,7 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("DMSService") readonly property var log: Log.scoped("DMSService")
property bool dmsAvailable: false readonly property bool dmsAvailable: isConnected
property var capabilities: [] property var capabilities: []
property int apiVersion: 0 property int apiVersion: 0
property string cliVersion: "" property string cliVersion: ""
@@ -21,7 +21,7 @@ Singleton {
property var availableThemes: [] property var availableThemes: []
property var installedThemes: [] property var installedThemes: []
property bool isConnected: false property bool isConnected: false
property bool isConnecting: false readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
property bool subscribeConnected: false property bool subscribeConnected: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET") readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -69,12 +69,13 @@ 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)
detectUpdateCommand(); return;
} detectUpdateCommand();
requestSocket.connected = true;
} }
function detectUpdateCommand() { function detectUpdateCommand() {
@@ -82,12 +83,6 @@ Singleton {
checkAurHelper.running = true; checkAurHelper.running = true;
} }
function startSocketConnection() {
if (socketPath && socketPath.length > 0) {
testProcess.running = true;
}
}
Process { Process {
id: checkAurHelper id: checkAurHelper
command: ["sh", "-c", "command -v paru || command -v yay"] command: ["sh", "-c", "command -v paru || command -v yay"]
@@ -105,7 +100,6 @@ Singleton {
} else { } else {
updateCommand = "dms update"; updateCommand = "dms update";
checkingUpdateCommand = false; checkingUpdateCommand = false;
startSocketConnection();
} }
} }
} }
@@ -114,7 +108,6 @@ Singleton {
if (exitCode !== 0) { if (exitCode !== 0) {
updateCommand = "dms update"; updateCommand = "dms update";
checkingUpdateCommand = false; checkingUpdateCommand = false;
startSocketConnection();
} }
} }
} }
@@ -135,7 +128,6 @@ Singleton {
updateCommand = "dms update"; updateCommand = "dms update";
} }
checkingUpdateCommand = false; checkingUpdateCommand = false;
startSocketConnection();
} }
} }
@@ -143,52 +135,27 @@ Singleton {
if (exitCode !== 0) { if (exitCode !== 0) {
updateCommand = "dms update"; updateCommand = "dms update";
checkingUpdateCommand = false; checkingUpdateCommand = false;
startSocketConnection();
} }
} }
} }
Process {
id: testProcess
command: ["test", "-S", root.socketPath]
onExited: exitCode => {
if (exitCode === 0) {
root.dmsAvailable = true;
connectSocket();
} else {
root.dmsAvailable = false;
}
}
}
function connectSocket() {
if (!dmsAvailable || isConnected || isConnecting) {
return;
}
isConnecting = true;
requestSocket.connected = true;
}
DankSocket { DankSocket {
id: requestSocket id: requestSocket
path: root.socketPath path: root.socketPath
connected: false connected: false
onConnectionStateChanged: { onConnectionStateChanged: {
if (connected) { if (linkUp) {
root.isConnected = true; root.isConnected = true;
root.isConnecting = false;
root.connectionStateChanged(); root.connectionStateChanged();
subscribeSocket.connected = true; subscribeSocket.connected = true;
} else { return;
root.isConnected = false;
root.isConnecting = false;
root.apiVersion = 0;
root.capabilities = [];
root.connectionStateChanged();
} }
root.isConnected = false;
root.apiVersion = 0;
root.capabilities = [];
root.failPendingRequests();
root.connectionStateChanged();
} }
parser: SplitParser { parser: SplitParser {
@@ -219,10 +186,10 @@ Singleton {
connected: false connected: false
onConnectionStateChanged: { onConnectionStateChanged: {
root.subscribeConnected = connected; root.subscribeConnected = linkUp;
if (connected) { if (!linkUp)
sendSubscribeRequest(); return;
} sendSubscribeRequest();
} }
parser: SplitParser { parser: SplitParser {
@@ -335,6 +302,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 +314,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") {
@@ -439,10 +413,20 @@ Singleton {
function handleResponse(response) { function handleResponse(response) {
const callback = pendingRequests[response.id]; const callback = pendingRequests[response.id];
if (!callback)
return;
delete pendingRequests[response.id];
callback(response);
}
if (callback) { function failPendingRequests() {
delete pendingRequests[response.id]; const pending = pendingRequests;
callback(response); pendingRequests = {};
clipboardRequestIds = {};
for (const id in pending) {
pending[id]({
"error": "not connected to DMS socket"
});
} }
} }
+5 -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 "";
@@ -1155,6 +1155,8 @@ Singleton {
return "Temperature must be between 2500K and 6000K"; return "Temperature must be between 2500K and 6000K";
const rounded = Math.round(temp / 500) * 500; const rounded = Math.round(temp / 500) * 500;
if (rounded > SessionData.nightModeHighTemperature)
return "Night temperature must not exceed the day temperature (" + SessionData.nightModeHighTemperature + "K)";
SessionData.setNightModeTemperature(rounded); SessionData.setNightModeTemperature(rounded);
if (root.nightModeEnabled) { if (root.nightModeEnabled) {
@@ -1184,6 +1186,8 @@ Singleton {
return "Temperature must be between 2500K and 6500K"; return "Temperature must be between 2500K and 6500K";
const rounded = Math.round(temp / 500) * 500; const rounded = Math.round(temp / 500) * 500;
if (rounded < SessionData.nightModeTemperature)
return "Day temperature must be at least the night temperature (" + SessionData.nightModeTemperature + "K)";
SessionData.setNightModeHighTemperature(rounded); SessionData.setNightModeHighTemperature(rounded);
if (root.nightModeEnabled && SessionData.nightModeAutoEnabled) if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)
+2
View File
@@ -388,6 +388,8 @@ Singleton {
const binds = bindsData[cat]; const binds = bindsData[cat];
for (var i = 0; i < binds.length; i++) { for (var i = 0; i < binds.length; i++) {
const bind = binds[i]; const bind = binds[i];
if (currentProvider === "hyprland" && bind.action && bind.action.startsWith("exec "))
bind.action = "spawn " + bind.action.slice(5);
const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat; const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat;
if (!processed[targetCat]) if (!processed[targetCat])
processed[targetCat] = []; processed[targetCat] = [];
+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;
+139 -9
View File
@@ -47,6 +47,7 @@ Singleton {
// Chromium can report blank metadata between tracks // Chromium can report blank metadata between tracks
property string stableTitle: "" property string stableTitle: ""
property string stableArtist: "" property string stableArtist: ""
property string stableAlbum: ""
Connections { Connections {
target: root.activePlayer target: root.activePlayer
@@ -59,6 +60,9 @@ Singleton {
root._syncStableMeta(); root._syncStableMeta();
root._checkIdle(); root._checkIdle();
} }
function onTrackAlbumChanged() {
root._syncStableMeta();
}
function onLengthChanged() { function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) { if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length; root.activePlayerStableLength = root.activePlayer.length;
@@ -72,8 +76,10 @@ Singleton {
onActivePlayerChanged: { onActivePlayerChanged: {
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0; activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
stableTitle = activePlayer?.trackTitle || ""; stableTitle = "";
stableArtist = activePlayer?.trackArtist || ""; stableArtist = "";
stableAlbum = "";
_syncStableMeta();
_checkIdle(); _checkIdle();
} }
@@ -82,14 +88,24 @@ Singleton {
if (!p) { if (!p) {
stableTitle = ""; stableTitle = "";
stableArtist = ""; stableArtist = "";
stableAlbum = "";
return; return;
} }
if (isFirefoxYoutubeHoverPreview(p)) if (isFirefoxYoutubeHoverPreview(p))
return; return;
if (p.trackTitle) const metadataPlayer = bestMetadataPlayer(p);
stableTitle = p.trackTitle; const nextTitle = displayTrackTitle(metadataPlayer);
if (p.trackArtist) const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
stableArtist = p.trackArtist; if (trackChanged) {
stableArtist = "";
stableAlbum = "";
}
if (nextTitle)
stableTitle = nextTitle;
if (metadataPlayer.trackArtist)
stableArtist = metadataPlayer.trackArtist;
if (metadataPlayer.trackAlbum)
stableAlbum = metadataPlayer.trackAlbum;
} }
// Chromium reports stopped media w/blank metadata, resolve by checking idle status // Chromium reports stopped media w/blank metadata, resolve by checking idle status
@@ -101,6 +117,7 @@ Singleton {
return; return;
root.stableTitle = ""; root.stableTitle = "";
root.stableArtist = ""; root.stableArtist = "";
root.stableAlbum = "";
root._resolveActivePlayer(); root._resolveActivePlayer();
} }
} }
@@ -122,20 +139,133 @@ Singleton {
delegate: Connections { delegate: Connections {
required property MprisPlayer modelData required property MprisPlayer modelData
target: modelData target: modelData
ignoreUnknownSignals: true
function onIsPlayingChanged() { function onIsPlayingChanged() {
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackTitleChanged() {
if (modelData.isPlaying) if (modelData.isPlaying)
root._resolveActivePlayer(); root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackArtistChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackAlbumChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onMetadataChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
} }
} }
} }
function isIdle(player: MprisPlayer): bool { function isIdle(player: MprisPlayer): bool {
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist; return player && player.playbackState === MprisPlaybackState.Stopped;
}
// Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
function _stripAppTitleSuffix(title: string): string {
const idx = title.lastIndexOf(" | ");
if (idx <= 0)
return title;
const suffix = title.substring(idx + 3).trim().toLowerCase();
return _appTitleSuffixes.indexOf(suffix) !== -1 ? title.substring(0, idx).trim() : title;
}
function normalizedTrackTitle(player: MprisPlayer): string {
return _stripAppTitleSuffix((player?.trackTitle || "").trim()).toLowerCase();
}
function displayTrackTitle(player: MprisPlayer): string {
return (player?.trackTitle || "").trim();
}
function normalizedTrackArtist(player: MprisPlayer): string {
return (player?.trackArtist || "").trim().toLowerCase();
}
// Artist missing on either side: fall back to URL, then album, before trusting a title-only match
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
const firstTitle = normalizedTrackTitle(first);
if (!firstTitle || firstTitle !== normalizedTrackTitle(second))
return false;
const firstArtist = normalizedTrackArtist(first);
const secondArtist = normalizedTrackArtist(second);
if (firstArtist && secondArtist)
return firstArtist === secondArtist;
const firstUrl = (first?.metadata?.["xesam:url"] || "").toString();
const secondUrl = (second?.metadata?.["xesam:url"] || "").toString();
if (firstUrl && secondUrl)
return firstUrl === secondUrl;
const firstAlbum = (first?.trackAlbum || "").trim().toLowerCase();
const secondAlbum = (second?.trackAlbum || "").trim().toLowerCase();
if (firstAlbum && secondAlbum)
return firstAlbum === secondAlbum;
return true;
}
function metadataQuality(player: MprisPlayer): int {
if (!player)
return -1;
let quality = player.trackArtist ? 100 : 0;
quality += player.trackTitle ? 40 : 0;
quality += player.trackAlbum ? 20 : 0;
quality += player.trackArtUrl || player.metadata?.["mpris:artUrl"] ? 10 : 0;
quality += player.metadata?.["xesam:url"] ? 5 : 0;
return quality;
}
function equivalentPlayers(player: MprisPlayer): var {
if (!player)
return [];
return availablePlayers.filter(candidate => {
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
});
}
function bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
const equivalents = equivalentPlayers(player);
if (equivalents.length === 0)
return player;
return equivalents.reduce((best, candidate) => {
return metadataQuality(candidate) > metadataQuality(best) ? candidate : best;
}, player);
}
function _bestPlayingPlayer(): MprisPlayer {
const playing = availablePlayers.filter(player => player.isPlaying);
if (playing.length === 0)
return null;
const controllable = playing.filter(player => player.canControl);
if (activePlayer?.isPlaying) {
if (activePlayer.canControl || controllable.length === 0)
return activePlayer;
// Playing but not controllable: only a same-track controllable peer may take over
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
return mirror || activePlayer;
}
if (activePlayer?.canControl && activePlayer.playbackState === MprisPlaybackState.Paused) {
const onlyEquivalentMirrors = playing.every(player => isSameTrack(activePlayer, player));
if (onlyEquivalentMirrors)
return null;
}
return controllable[0] || playing[0];
} }
function _resolveActivePlayer(): void { function _resolveActivePlayer(): void {
// A playing player always wins; otherwise keep the selection stable w/idle const playing = _bestPlayingPlayer();
const playing = availablePlayers.find(p => p.isPlaying);
if (playing) { if (playing) {
if (activePlayer !== playing) { if (activePlayer !== playing) {
activePlayer = playing; activePlayer = playing;
-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) {
+1 -1
View File
@@ -425,7 +425,7 @@ Singleton {
} }
function closeSettings() { function closeSettings() {
settingsModal?.close(); settingsModal?.hide();
} }
function toggleSettings() { function toggleSettings() {
+64 -2
View File
@@ -17,6 +17,7 @@ Singleton {
property bool loginctlCommandAvailable: false property bool loginctlCommandAvailable: false
property bool systemctlCommandAvailable: false property bool systemctlCommandAvailable: false
property bool hibernateSupported: false property bool hibernateSupported: false
readonly property bool softRebootSupported: systemctlCommandAvailable
property bool inhibitorAvailable: true property bool inhibitorAvailable: true
property bool idleInhibited: false property bool idleInhibited: false
property string inhibitReason: "Keep system awake" property string inhibitReason: "Keep system awake"
@@ -217,6 +218,64 @@ Singleton {
return envObj; return envObj;
} }
function splitShellArgs(str) {
const args = [];
let current = "";
let hasToken = false;
let quote = "";
let escaped = false;
for (const ch of str) {
if (escaped) {
current += ch;
escaped = false;
continue;
}
switch (quote) {
case "'":
if (ch === "'") {
quote = "";
continue;
}
current += ch;
continue;
case "\"":
switch (ch) {
case "\"":
quote = "";
continue;
case "\\":
escaped = true;
continue;
}
current += ch;
continue;
}
switch (ch) {
case "\\":
escaped = true;
continue;
case "'":
case "\"":
quote = ch;
hasToken = true;
continue;
case " ":
case "\t":
case "\n":
if (!hasToken && current.length === 0)
continue;
args.push(current);
current = "";
hasToken = false;
continue;
}
current += ch;
}
if (current.length > 0 || hasToken)
args.push(current);
return args;
}
function launchDesktopEntry(desktopEntry, useNvidia) { function launchDesktopEntry(desktopEntry, useNvidia) {
if (!desktopEntry || !desktopEntry.command) if (!desktopEntry || !desktopEntry.command)
return; return;
@@ -230,8 +289,7 @@ Singleton {
cmd = [nvidiaCommand].concat(cmd); cmd = [nvidiaCommand].concat(cmd);
if (override?.extraFlags) { if (override?.extraFlags) {
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0); cmd = cmd.concat(splitShellArgs(override.extraFlags));
cmd = cmd.concat(extraArgs);
} }
const userPrefix = SettingsData.launchPrefix?.trim() || ""; const userPrefix = SettingsData.launchPrefix?.trim() || "";
@@ -395,6 +453,10 @@ Singleton {
} }
} }
function softReboot() {
Quickshell.execDetached(["systemctl", "soft-reboot"]);
}
function poweroff() { function poweroff() {
if (SettingsData.customPowerActionPowerOff.length === 0) { if (SettingsData.customPowerActionPowerOff.length === 0) {
Quickshell.execDetached(powerManagerCommand("poweroff")); Quickshell.execDetached(powerManagerCommand("poweroff"));
+33 -8
View File
@@ -26,7 +26,7 @@ Singleton {
return hash.toString(16).padStart(8, '0'); return hash.toString(16).padStart(8, '0');
} }
function getArtworkUrl(player) { function _directArtworkUrl(player) {
if (!player) return ""; if (!player) return "";
let artUrl = player.trackArtUrl || ""; let artUrl = player.trackArtUrl || "";
@@ -54,6 +54,17 @@ Singleton {
return ""; return "";
} }
function getArtworkUrl(player) {
const directUrl = _directArtworkUrl(player);
if (directUrl !== "")
return directUrl;
const equivalent = MprisController.equivalentPlayers(player).find(candidate => {
return candidate !== player && _directArtworkUrl(candidate) !== "";
});
return _directArtworkUrl(equivalent);
}
function _commit(u, artKey, srcUrl) { function _commit(u, artKey, srcUrl) {
resolvedArtUrl = u; resolvedArtUrl = u;
_committedArtKey = u !== "" ? artKey : ""; _committedArtKey = u !== "" ? artKey : "";
@@ -171,11 +182,25 @@ Singleton {
onActivePlayerChanged: _updateArtUrl() onActivePlayerChanged: _updateArtUrl()
Connections { Connections {
target: root.activePlayer target: MprisController
ignoreUnknownSignals: true function onAvailablePlayersChanged() {
function onTrackTitleChanged() { root._updateArtUrl(); } root._updateArtUrl();
function onTrackArtUrlChanged() { root._updateArtUrl(); } }
function onMetadataChanged() { root._updateArtUrl(); } }
Instantiator {
model: MprisController.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
ignoreUnknownSignals: true
function onIsPlayingChanged() { root._updateArtUrl(); }
function onTrackTitleChanged() { root._updateArtUrl(); }
function onTrackArtistChanged() { root._updateArtUrl(); }
function onTrackAlbumChanged() { root._updateArtUrl(); }
function onTrackArtUrlChanged() { root._updateArtUrl(); }
function onMetadataChanged() { root._updateArtUrl(); }
}
} }
function _trackKey() { function _trackKey() {
@@ -201,8 +226,8 @@ Singleton {
} }
_pendingArtKey = key; _pendingArtKey = key;
const url = getArtworkUrl(activePlayer); const url = getArtworkUrl(activePlayer);
// Ignore Chrome's same-track thumbnail size updates. // Ignore duplicate notifications, but let a richer peer replace same-track art
if (key !== "" && key === _committedArtKey) if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
return; return;
if (key !== "" && url !== "" && url === _committedSrcUrl) { if (key !== "" && url !== "" && url === _committedSrcUrl) {
// Chrome can publish track metadata before its new artwork URL. // Chrome can publish track metadata before its new artwork URL.
+1 -1
View File
@@ -1 +1 @@
v1.5.2 v1.5.3
+3 -1
View File
@@ -105,9 +105,11 @@ PanelWindow {
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
readonly property string _slideoutScreenName: modelData?.name ?? ""
WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: 0 WlrLayershell.exclusiveZone: 0
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: isVisible && !ModalManager.currentModalsByScreen[_slideoutScreenName] ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
readonly property real dpr: CompositorService.getScreenScale(root.screen) readonly property real dpr: CompositorService.getScreenScale(root.screen)
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr) readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
+1 -1
View File
@@ -869,7 +869,7 @@ Item {
readonly property var tooltipTexts: ({ readonly property var tooltipTexts: ({
"dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"), "dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"),
"compositor": I18n.tr("Niri compositor actions (focus, move, etc.)"), "compositor": I18n.tr("Compositor actions (focus, move, etc.)", "keybind action type tooltip"),
"spawn": I18n.tr("Run a program (e.g., firefox, kitty)"), "spawn": I18n.tr("Run a program (e.g., firefox, kitty)"),
"shell": I18n.tr("Run a shell command (e.g., notify-send)") "shell": I18n.tr("Run a shell command (e.g., notify-send)")
}) })
File diff suppressed because it is too large Load Diff