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

Compare commits

..

160 Commits

Author SHA1 Message Date
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
bbedward accdd0972c popout: fix lifecycle of blur with various popouts
fixes #2901
port 1.5

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

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

fixes #2890
port 1.5

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

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

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

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

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

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

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

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

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

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

port 1.5

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

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

port 1.5

(cherry picked from commit 698b66cf91)
2026-07-18 15:51:52 +00:00
dms-ci[bot] 74896fb87c bump VERSION to v1.5.2 2026-07-18 02:18:37 +00:00
bbedward f1f528b2cb widgets: fix hidden widgets disrupting index when arranging widgets
fixes #2844
port 1.5

(cherry picked from commit 5dfd875b9e)
2026-07-18 01:10:36 +00:00
purian23 9853e1431b fix(frame): restore frame edge inset states regression in framemode
Port 1.5

(cherry picked from commit 00b22df802)
2026-07-18 00:25:07 +00:00
bbedward e7b8196a4a i18n: preserve terms for 1.5 point releases
port 1.5

(cherry picked from commit 4200270b55)
2026-07-17 22:39:56 +00:00
purian23 85ebb402cb feat(nix): opt-in dankshell-u2f PAM service for lock screen security keys
- Updated instructional pushed:
https://danklinux.com/docs

Related: #2874
Port 1.5

(cherry picked from commit c2c180d304)
2026-07-17 22:07:23 +00:00
purian23 f37f4a1f35 refactor(auth): relay on distro system auth over DMS managed sessions
- Gate greeter external-auth status fprint availability only on confirmed setups
- Reload dankshell-U2F/U2F-Key watchers live

Related: #2874
Port 1.5

(cherry picked from commit 3938e60ce4)
2026-07-17 17:54:36 -04:00
bbedward fc11dfbc57 media: further optimizations for bar widget animations
related #2863
port 1.5

(cherry picked from commit 2986e354e8)
2026-07-17 19:49:25 +00:00
Hec 7806cf4a58 systemlogo: Add Zirconium (#2881)
Signed-off-by: Hec <hec@heccraft.com>
(cherry picked from commit ed44d02202)
2026-07-17 15:41:30 -04:00
bbedward 89895da4fa lock: add lockAndOutputsOff IPC function
(cherry picked from commit bf12665adb)
2026-07-17 15:25:04 -04:00
bbedward 4ff93bf46e screenshot: initial support for scroll capture mode
fixes #2871
port 1.5

(cherry picked from commit f8fe16738e)
2026-07-17 17:32:12 +00:00
purian23 63ffd7caec feat(notifications): introduce notification foreground layers settings override
port 1.5

(cherry picked from commit 3c688cfbd3)
2026-07-17 17:17:34 +00:00
purian23 dac08b34c6 feat(settings): add explicit border toggle and updated layouts
- Defaults On with blur enabled to clean up the border edges
- Darken Modal moves to Launcher settings

Closes #2446
port 1.5

(cherry picked from commit 92ba2bb57d)
2026-07-17 16:55:21 +00:00
bbedward 840a448b4a wallpaper: optimize blurred wallpaper layer
port 1.5

(cherry picked from commit 504c132f78)
2026-07-17 15:41:25 +00:00
bbedward 26d429fa31 fix(network): cancel in-flight connection when setting autoconnect
fixes #2882
port 1.5

(cherry picked from commit bdaa6bcc9d)
2026-07-17 15:01:36 +00:00
bbedward fd20cce9b7 sysupdate: tweak styling of ignored banner
port 1.5

(cherry picked from commit a50bb2cab3)
2026-07-17 14:32:55 +00:00
bbedward ad5a96d95b sysupdate: disallow partial updates on arch, add ignored to popout
port 1.5

(cherry picked from commit b17a90a412)
2026-07-17 14:19:41 +00:00
bbedward 8ba5e1a082 sysupdate: pass custom command to daemon instead of fire and forget from
QML

related #2873

port 1.5

(cherry picked from commit 0ffec5011b)
2026-07-17 13:46:54 +00:00
bbedward 7d6a7bae06 bar: fix bar order-creation regression caused by async loading
port 1.5

(cherry picked from commit 95e1d6b72e)
2026-07-17 13:17:40 +00:00
purian23 986a6e0230 fix(DankSlideout): update textureSize for improved rendering
- Fixes #2880
Port 1.5

(cherry picked from commit 4cb09dcc1b)
2026-07-17 12:24:58 +00:00
bbedward 64ba1fbb1b launcher: fix sticky header appearance without blur
port 1.5

(cherry picked from commit 6c45413d7a)
2026-07-16 14:51:10 +00:00
bbedward ab9df791b3 launcher: transparent sticky header when blur is enabled
port 1.5

(cherry picked from commit 149e1e64b1)
2026-07-16 14:48:27 +00:00
bbedward 65f4a9a044 dash/weather: fix X wheel scroll
port 1.5

(cherry picked from commit 5589cfb783)
2026-07-16 14:41:44 +00:00
bbedward 59164cbd30 popout: guard against bad state and reset input method on close
port 1.5

(cherry picked from commit 3951cd6b18)
2026-07-16 14:22:28 +00:00
bbedward 7ab1f6a956 core: fix log file deadlock
port 1.5

(cherry picked from commit 9686f29246)
2026-07-16 14:09:28 +00:00
bbedward 841167ac44 launcher: fix usage history loading by adding guards
port 1.5

(cherry picked from commit 6d206c9521)
2026-07-16 13:30:36 +00:00
purian23 c2a6a7aebd feat(auth): expand shared PAM support for custom policies
- Add support for DMS and System managed factor policies
- Add validated dedicated security-key PAM / U2F Key sources
- Restore U2F OR/AND flows

Related: #2874
Port 1.5

(cherry picked from commit 48e36ae146)
2026-07-16 13:24:40 +00:00
purian23 b17ef80c91 fix(qml): resolve lint regressions from async shell loading
port 1.5

(cherry picked from commit 7c24e85c19)
2026-07-16 10:11:01 +00:00
purian23 a55177f1d2 fix(media-player): update Mpris stable metadata handling
- Restores Brave browsers
port 1.5

(cherry picked from commit fe1a783ec2)
2026-07-16 00:57:21 +00:00
purian23 3d0ce17a8c refactor: update workfows & issue notes
port 1.5

(cherry picked from commit 358496134c)
2026-07-15 20:18:54 +00:00
dms-ci[bot] 6306bc7aa2 bump VERSION to v1.5.1 2026-07-15 19:32:57 +00:00
purian23 eca92898f9 refactor: unify media control calls to MprisController sync
(cherry picked from commit 6cc574ea5b)
2026-07-15 14:36:39 -04:00
bbedward db9fb0bd90 system tray: fix overflow of menus
port 1.5

(cherry picked from commit fb2dbced08)
2026-07-15 18:20:21 +00:00
bbedward 815820aa27 media: throttle frame rate of media-related animations
port 1.5

related #2869

(cherry picked from commit cdaedad969)
2026-07-15 16:58:04 +00:00
bbedward 01c0ed2778 i18n: cut down terms and sync
port 1.5

(cherry picked from commit bf408f8d00)
2026-07-15 15:05:42 +00:00
purian23 9967db0c5a feat: new PAM auth management & settings in greeter/lockscreen
- Introduce external management of greetd PAM
- New functionality to validate and apply custom PAM service paths in lockscreen

Port 1.5

(cherry picked from commit b169fe0d77)
2026-07-15 14:38:30 +00:00
bbedward 883a787db7 battery: dont scale values and skew the actual battery values
fixes #2867

port 1.5

(cherry picked from commit 9b67cedaa1)
2026-07-15 14:21:38 +00:00
bbedward 3b1a3c1c22 tailscale: fix inconsistency in styling of control center widget
port 1.5

(cherry picked from commit 2edf70a144)
2026-07-15 14:16:03 +00:00
bbedward 970262f276 hyprland: scale overview displays relative to their actual scale
port 1.5

(cherry picked from commit 72a71cacba)
2026-07-15 14:11:01 +00:00
bbedward b708e97ec5 compositor: improve compositor detection
port 1.5

(cherry picked from commit 0440e40a82)
2026-07-15 13:45:49 +00:00
bbedward e94fc7fe99 qs: improve initial load of wallpaper, dock, and desktop plugins
(cherry picked from commit ec27d4643c)
2026-07-15 09:44:04 -04:00
bbedward c4bca6f6cd screenshot: add --json flag to capture metadata
fixes #2852

port 1.5

(cherry picked from commit f18d36f6c2)
2026-07-15 13:18:14 +00:00
purian23 9e7fc833e9 update release & changelog docs formatting
port 1.5

(cherry picked from commit 3254cc6a1e)
2026-07-15 04:57:49 +00:00
bbedward 9e2d493749 network: avoid dropping to legacy service when loading
(cherry picked from commit 4af3225f21)
2026-07-14 23:22:07 -04:00
bbedward 93120fd73b qs: use asynchronous loaders to load shell core
(cherry picked from commit 3c5245914f)
2026-07-14 23:22:07 -04:00
bbedward 536ebf7b0a i18n: sync
(cherry picked from commit 89814a2c65)
2026-07-14 22:34:04 -04:00
bbedward 1105047a80 gamma: fix scheduler losing track during suspend
port 1.5

(cherry picked from commit 5d2093e54a)
2026-07-15 02:27:32 +00:00
purian23 8b46397200 greeter: update success message
(cherry picked from commit 3cd52ca327)
2026-07-14 20:28:39 -04:00
purian23 6b70491ba9 fix(greeter): reimplement regression to hide debug logs during login
- Provide more status feedback
- Allow 2 fprint tries within 10 seconds for DMS-managed PAM
- Updated dms greeter docs

Note: distro-managed PAM takes precedence and may use different limits.
DMS auth changes remain conditional on greetd being installed.

Fixes #2853
Port 1.5

(cherry picked from commit 5b41d699fa)
2026-07-14 23:41:52 +00:00
bbedward deb0138198 media: fix track art flash from chrome changing media art sizes on track
change

port 1.5

(cherry picked from commit 544599bf1c)
2026-07-14 22:45:42 +00:00
bbedward 2e0587ca16 mango: add separate dispatch socket
port 1.5

(cherry picked from commit 296b3a3d7e)
2026-07-14 21:10:01 +00:00
bbedward 45cafc6bdb dbar: missing show on overview from search index
port 1.5

(cherry picked from commit 729a990fa7)
2026-07-14 20:47:23 +00:00
bbedward 82d5081b7d cava: more optimizations for visualizer
related #2863

(cherry picked from commit c3fa7b2e1d)
2026-07-14 14:30:00 -04:00
bbedward b5b083305f cava: optimize CPU burn
related #631

(cherry picked from commit 2c5a1a2804)
2026-07-14 14:29:54 -04:00
Kilian Mio 039d14c790 Feature/split move size hyprland windowrules (#2824)
* windowrules: add split move/size fields for Lua table syntax

* windowrules: remove deprecated Move/Size fields, switch QML to split fields

* fix: use resolved dms binary path in Proc.runCommand

(cherry picked from commit 21eaaef056)
2026-07-14 11:57:56 -04:00
Alexis Corporal ee7ac9d2bd chore: fix indentation in niri media key bindings. (#2858)
(cherry picked from commit 52740290b2)
2026-07-14 11:57:46 -04:00
bbedward bd159be414 plugins: prevent churn of daemon plugins by not re-creating the entire map
fixes #2860

port 1.5

(cherry picked from commit 1973526c4e)
2026-07-14 15:47:14 +00:00
Callum Wong 4dda0e3a5e Smooth battery time remaining estimate (#2854)
* feat(battery): smooth time remaining estimate with moving average

* feat(battery): use a time-weighted EMA for time remaining estimate

port 1.5

(cherry picked from commit bba5502960)
2026-07-14 15:33:07 +00:00
bbedward 724ee9422d plugins: use grid-style plugin browser
port 1.5

(cherry picked from commit 27703575bc)
2026-07-14 15:25:42 +00:00
bbedward c91cb5d89a core/cli: expose QR code CLI
port 1.5

(cherry picked from commit e6504add7b)
2026-07-14 15:25:39 +00:00
Huỳnh Thiện Lộc 660953ebc5 fix(Modal): respect targetScreen property instead of always using focused screen (#2861)
The open() method in both DankModalStandalone and DankModalConnected unconditionally set contentWindow.screen to the focused screen, ignoring the targetScreen property.

Fix: use root.targetScreen ?? CompositorService.getFocusedScreen() so the modal appears on the configured screen when set, and falls back to focused screen otherwise.
(cherry picked from commit f1e9121295)
2026-07-14 12:42:38 +00:00
bbedward bd70a6286e dock: fix dock s howing with no apps
port 1.5

(cherry picked from commit 9ff751b82a)
2026-07-14 02:47:41 +00:00
bbedward d87e2fcb01 clipboard: store text alongside image when both are offered
related #2849
port 1.5

(cherry picked from commit bb0be2b215)
2026-07-13 22:23:39 +00:00
bbedward 928b272628 config/hyprland: set empty kb layout string by default
fixes #2851
port 1.5

(cherry picked from commit 25847c3f03)
2026-07-13 21:43:46 +00:00
bbedward 781ed16538 launcher/spotlight: improve height-change animation
port 1.5

(cherry picked from commit 7b5c25c50f)
2026-07-13 20:39:09 +00:00
arfan 417da7408d workspace switcher: fix apps icon won't re focus (#2830)
* fix(workspace): update delegate data on window focus change

* fix(workspace): update delegate data handling specific for hyprland event

(cherry picked from commit 4fb6995796)
2026-07-13 16:18:43 -04:00
Arsenijs Kitajevs 0d550662de Fixed bluetooth UI bug mentioned in #2627 (#2850)
(cherry picked from commit d379d251b9)
2026-07-13 16:18:42 -04:00
bbedward 35e35a16fa dash: allow hiding all tabs
port 1.5

(cherry picked from commit 7535b70fa6)
2026-07-13 16:16:48 -04:00
Rafi d96559f7df core: fix security and concurrency issues found in a backend audit (#2805)
* core: fix security and concurrency issues found in backend audit

Security:
- privesc: pipe the sudo password via stdin (sudo -S) instead of
  embedding it in the command string, so it no longer appears in argv
  (readable by any local user via /proc/<pid>/cmdline or ps)
- greeter: tokenize a session .desktop Exec= line into argv and execve
  directly instead of running it through /bin/sh -c, closing a command-
  injection path via user-writable ~/.local/share/wayland-sessions
- plugins: reject path-separator/.. in plugin id/name before joining
  into a filesystem path, closing an arbitrary-directory-delete in the
  uninstall/update fallback
- keybinds/hyprland: always quote unrecognized bind actions/keys when
  writing generated Lua; only re-emit genuine round-tripped custom Lua
  verbatim (tracked via an explicit flag), closing a Lua-injection path
- desktop/mimeapps: reject newline/bracket in mime/desktop-id fields so
  they can't inject fake sections into the shared mimeapps.list

Robustness / concurrency:
- server: recover panics in the request-dispatch path so one bad
  handler can't crash the daemon and drop every client
- go-wayland: recover panics in the shared dispatch choke point so a
  malformed compositor event can't crash CLI tools / the daemon
- server: per-connection D-Bus client ID instead of a shared constant,
  fixing cross-client signal delivery and subscription teardown
- network: guard the NetworkManager device maps with a mutex (a
  concurrent map read/write here is an unrecoverable fatal error)
- cups: close the event channel on Stop() so Unsubscribe() of the last
  subscriber no longer deadlocks; allocate the fresh channel in Start()
- freedesktop: reuse the shared session conn for the settings watcher
  and tear it down in Close(), fixing a per-Manager conn+goroutine leak
- clipboard: mutex-guard lazy dbusConn creation
- geolocation: use WithMatchMember for the GeoClue2 LocationUpdated
  signal (was WithMatchSender with an interface.member string, so the
  match never fired and live location updates never arrived)
- screenshot: set failed=true on buffer/pool creation errors so the
  dispatch loop doesn't wait forever for a ready/failed that never comes

* apply code review comments

---------

Co-authored-by: bbedward <bbedward@gmail.com>
(cherry picked from commit ca89e12963)
2026-07-13 16:16:48 -04:00
Huỳnh Thiện Lộc 7f37799e53 fix(dash): resolve pointer cursor hover issue in connected mode (#2845)
* fix(dash): cursor not changing to pointer in Connected Mode (#2831)

Replaces the full-screen background dismissal MouseArea in
DankPopoutConnected.qml with four edge strips that exclude
the popup body. The full-screen MouseArea at z:-1 was
suppressing child cursorShape propagation on Wayland when
combined with the full-screen input mask.

Blame: the connected popout architecture itself (the issue
does not repro in Separate Mode where background dismissal
lives in a separate PanelWindow).

* fix(dash): resolve pointer cursor hover issue in connected mode

(cherry picked from commit 3f5a54aa88)
2026-07-13 16:16:48 -04:00
bbedward fcda17f517 launcher: add option to choose spotlight style on niri overview
(cherry picked from commit 3da19e5c15)
2026-07-13 16:15:11 -04:00
purian23 4b05e7a0fa refactor(dms-updater): remove hidden updates display in popout view
(cherry picked from commit 7ab0e01573)
2026-07-13 12:38:28 -04:00
bbedward 7c6ad65b5a i18n: add arabic
(cherry picked from commit 31ea83584b)
2026-07-13 12:38:27 -04:00
bbedward acda199e28 i18n: sync
(cherry picked from commit 06c0ea2afb)
2026-07-13 12:38:27 -04:00
bbedward b3a129af2f listview: workaround delegates overlapping and make spotlight launcher
have a stable anchor

port 1.5

(cherry picked from commit 2cb48aaf6b)
2026-07-13 16:31:09 +00:00
purian23 e9d958b32b feat(dms updater): add support for ignoring specific packages during system updates
- Added UI component & popout settings to manage ignored packages
- Added CLI support available in danklinux docs

Fixes: #2827
Closes: #2344, #1741
Port 1.5

(cherry picked from commit e4657aa5f9)
2026-07-13 16:02:35 +00:00
bbedward dd9edd8a00 launcher: add IPC feature-parity to spotlight-bar and improve list view
transitions

port 1.5

(cherry picked from commit 197d17ac4e)
2026-07-13 15:25:59 +00:00
bbedward 28c25d46d5 launcher: don't select pre-filled queries, exclude from memory
port 1.5

(cherry picked from commit ea66b136ba)
2026-07-13 14:32:10 +00:00
bbedward 58530ae56c time: add follow-locale option and squash separate greeter time options
port 1.5

(cherry picked from commit f4f47c0bc5)
2026-07-13 12:53:54 +00:00
purian23 c0134c249a fix(port-audit): update git fetch command to include prune opt
port 1.5

(cherry picked from commit 8a0ed8a50f)
2026-07-13 05:14:51 +00:00
purian23 f7157f0e92 refactor(debian): prefer native Debian Quickshell stable package support in dankinstall
Port 1.5

(cherry picked from commit 63eea01243)
2026-07-13 04:39:34 +00:00
purian23 e29b9749df fix(dash-tabs): update visibility logic for dash tab elements
Fixes #2822

Port 1.5

(cherry picked from commit f590a2965a)
2026-07-13 03:57:44 +00:00
purian23 6791c20f40 refactor(settings): allow search settings to remain on sidebar until dismissed
Closes #2780

Port 1.5

(cherry picked from commit 4ab03deded)
2026-07-13 03:30:58 +00:00
A042 6c2572fbfe fix: use sleep_monitor/wakeup_monitor for DPMS to preserve monitor layout (#2842)
Since Mango v0.15.0, disable_monitor sets only_sleep=0 which removes the
output from the layout entirely. On NVIDIA (proprietary driver), the DRM
handshake fails on re-enable, leaving a black screen recoverable only via
TTY switch.

sleep_monitor/wakeup_monitor set only_sleep=1, keeping the output in the
layout while powered off. This matches the pre-v0.15 behavior and fixes
DPMS wake on NVIDIA.

These commands are available since Mango v0.15.0 (commit b0326d7).

(cherry picked from commit da7cc5648b)
2026-07-12 23:14:04 -04:00
sweenu 3ee07f7f20 feat(ipc): add settings dump to print full live config as JSON (#2817)
Adds `dms ipc call settings dump`, returning the complete live
configuration via SettingsData.getCurrentSettingsJson() — the same
serialization used by the read-only banner's copy button.

Useful when settings.json is a read-only symlink (NixOS/home-manager):
changes made in the Settings UI only exist in the running shell's
memory, and this makes them retrievable from the CLI instead of only
through the clipboard button in the Settings modal.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Port 1.5

(cherry picked from commit 846d07d86a)
2026-07-13 02:58:09 +00:00
AiraNadih ef244cb645 fix(process-list): defer IPC actions until modal is loaded (#2829)
Port 1.5

(cherry picked from commit 55d88d8dfb)
2026-07-13 02:57:51 +00:00
Mati7235 860c347362 dms doctor: detect missing XDG_MENU_PREFIX when KDE apps are installed (#2840)
* dms doctor: warn when XDG_MENU_PREFIX is missing with KDE apps

Under non-Plasma compositors (niri, Hyprland, Sway), Dolphin's
'Open with...' dialog appears empty because KService/KApplicationTrader
requires a populated XDG menu tree, which depends on XDG_MENU_PREFIX.

This check warns the user when kde-cli-tools (keditfiletype) is
installed but XDG_MENU_PREFIX is not set.

Ref: https://specifications.freedesktop.org/menu-spec/latest/

* fix(dms doctor): improve warning message for missing XDG_MENU_PREFIX

Port 1.5

---------

Co-authored-by: Matias <git@matias7235.com.ar>
(cherry picked from commit a803f96f41)
2026-07-13 02:54:43 +00:00
bbedward c7b58e52d7 fix(printers): fix add by address with manually entered host
port 1.5

(cherry picked from commit 75443758f9)
2026-07-12 22:28:30 +00:00
purian23 e0fb817a75 add(workflows): update notify_issues input to release flows
Port 1.5

(cherry picked from commit eaecff93a4)
2026-07-12 20:27:47 +00:00
purian23 b5908f4d1b feat(release): add issue notification script for retesting on new releases
Port 1.5

(cherry picked from commit 6e7c20c59c)
2026-07-12 19:58:34 +00:00
purian23 ee6c91e094 refactor(workflows): update release refs to stable in CI configurations
Port 1.5

(cherry picked from commit 406dcfe110)
2026-07-12 19:23:22 +00:00
Huỳnh Thiện Lộc 59d367f4a4 feat(media): add toggle for album art accent colors (#2831) (#2832)
* fix(media): use system colours in player instead of album cover accent

Closes #2831

Removes ColorQuantizer-based album art accent extraction from
MediaAccentService. All accent properties now return Theme.primary
and Theme.onPrimary directly, so the Dank Dash player always
matches the system colour scheme.

Blame: ee6f7b47 (introduced MediaAccentService & ColorQuantizer)
       d799175c (tweaked seekbar accent colours)
       a62ae336 (further integrated accent into player/album art)
       c44ffae7 (monochrome art edge case fixes)

* feat(media): add toggle for album art accent colours (#2831)

Adds mediaUseAlbumArtAccent setting (default: off) to Settings.
When enabled, MediaAccentService extracts accent from album art
via ColorQuantizer. When disabled, uses Theme.primary.
Toggle is in Settings > Media Player.

* fix: use american english spelling (colour -> color)

Port 1.5

(cherry picked from commit e2b3a2e3ca)
2026-07-12 02:30:04 +00:00
purian23 ee84489b95 fix(void): updated dms-greeter stability support
- Updates will land in DMS v1.5.1 and users are advised to hold off on v1.5.0

Related #2788

Port 1.5

(cherry picked from commit 71ab752e1b)
2026-07-11 05:07:25 +00:00
purian23 b1beecb083 feat(void): switch package repositories to R2
(cherry picked from commit 204ecd0461)
2026-07-10 18:27:26 -04:00
purian23 ffd3840058 ci(void): publish XBPS repository to R2
(cherry picked from commit 0fdfac366e)
2026-07-10 18:27:25 -04:00
Artem Timofeev 447b4d89b6 fix(notifications): dismiss popups when senders close notifications (#2815)
When a sender calls CloseNotification (per freedesktop spec) for a
persistent notification (expire-timeout=0), the onDropped handler
removed the wrapper from internal arrays but did not dismiss the
visible popup. The popup remained on screen forever for non-critical
notifications like YubiKey touch prompts.

Set wrapper.popup=false in onDropped so the popup exits via the
normal signal chain. This is minimal; it does not change timeout
handling, queue management, or the isPersistent marker.

Also reverts the previous workaround that special-cased non-critical
expireTimeout=0 to use DMS timeout. Upstream spec-correct behavior
is to honor the sender timeout, and fix the dismissal bug instead.

Closes #2814

(cherry picked from commit 26b2955cf3)
2026-07-10 18:27:25 -04:00
Kangheng Liu 36772735f3 fix(player): combine trackid with text identity (#2808)
fixes #2807

(cherry picked from commit c445597f83)
2026-07-10 18:27:25 -04:00
bbedward 375afaaf3a widgets: fix dropdown/icon picker referencing a dead window
port 1.5

(cherry picked from commit 52123458c7)
2026-07-10 21:21:42 +00:00
bbedward fe0214d522 theme: prevent failed portal writes from reverting theme mode
related: #2786

port 1.5

(cherry picked from commit d82d86df5c)
2026-07-10 19:45:35 +00:00
bbedward fce1a09790 osd/media playback: delay showing until album art is ready and avoid
re-showing when duplicate metadata comes from mpris
related #2787
port 1.5

(cherry picked from commit 6a58adfb29)
2026-07-10 17:37:01 +00:00
bbedward 554e55c63a audio: only show mic volume OSD through DMS IPCs, not external source
changes
fixes #2790
port 1.5

(cherry picked from commit 05feb211ba)
2026-07-10 16:44:19 +00:00
purian23 f7d8e2b56c workflow: enhance porting logic to support inline verbiage & audits
(cherry picked from commit a3b2167e58)
2026-07-10 12:04:50 -04:00
purian23 1d4572ac1c workflow: updated target extraction regex options
(cherry picked from commit 494144a7c7)
2026-07-10 12:04:50 -04:00
bbedward c02a24fecf calendar: add action button to events for opening links
fixes #2799
port 1.5

(cherry picked from commit 2a2c1ca9e6)
2026-07-10 12:04:50 -04:00
bbedward bf644516f8 qs/common: fix path decoding
fixes #2802
port 1.5

(cherry picked from commit 56b7ecb008)
2026-07-10 12:04:50 -04:00
Evan Maddock 61f9ea4954 build: Add support for DESTDIR (#2783)
This makes it easier for distros to use the Makefile when creating
packages. It enables us to specify a base destination directory to
install the project files to. E.g., on Solus, when creating eopkgs, files
must be installed to a special directory path, which becomes the package
files. I believe Fedora packages, and others, are the same.

Signed-off-by: Evan Maddock <maddock.evan@vivaldi.net>
(cherry picked from commit 45f6232e32)
2026-07-10 12:04:50 -04:00
Scott McKendry 5a169bea72 fix(settings): battery tab items not in search (#2794)
(cherry picked from commit 4bdb7d17b2)
2026-07-10 12:04:50 -04:00
14Do 3d60f35683 fix(ListViewTransitions): null transitions when duration truncates to 0ms (#2791)
A zero-duration ViewTransition still engages ListView's transition
machinery but resolves within the same frame, so removed delegates are
never released and displaced items are never repositioned. On a filtered
ScriptModel that re-filters on every keystroke this leaves stale rows and
empty gaps behind, visible in any DankListView-backed list.

The shortest sub-duration (remove/add = expressiveDurations.fast =
base * 0.4) is coerced to an int, so it truncates to 0ms for any
animation base duration below 3: animation speed None (0), or a Custom
duration of 1-2ms (1*0.4=0.4->0, 2*0.4=0.8->0, only 3*0.4=1.2->1
survives). Presets (250/500/750) are unaffected, which is why this only
reproduces with animations disabled or a very small custom value.

Gate the four transitions to null whenever that shortest sub-duration
would truncate to 0, so the view takes the correct instant path instead
of a broken 0ms transition. Base >= 3 is unchanged.

(cherry picked from commit ce1595d62d)
2026-07-10 12:04:49 -04:00
bbedward b95f4cdbf3 settings: restore lost geometric centering option
port 1.5

(cherry picked from commit 0b69feaa1a)
2026-07-10 15:12:40 +00:00
bbedward b834e82668 network/iwd: improve bad credential handling
related #2804

port: 1.5
(cherry picked from commit 867102b82c)
2026-07-10 14:37:22 +00:00
bbedward 302cfda2f4 fix(media): resolve monochrome album art accents
port: 1.5
(cherry picked from commit c44ffae751)
2026-07-09 23:40:55 +00:00
bbedward c86676df9b auth: add some more intelligent pam config resolution for lock screen
and greeter
related #2789

port: 1.5
(cherry picked from commit 9cf2ca7196)
2026-07-09 19:12:54 +00:00
purian23 fa26dbb544 workflows: update deps
port/1.5

(cherry picked from commit c0eeed4e89)
2026-07-09 16:54:26 +00:00
purian23 067d01f678 workflow: update porting logic
(cherry picked from commit cb0dc9c68d)
2026-07-09 12:53:05 -04:00
purian23 95c8cc80f6 Add GitHub workflows for release management
(cherry picked from commit 8008238ca0)
2026-07-09 12:53:05 -04:00
purian23 b8f216dc0f fix(workflow): void stable packages
(cherry picked from commit a48cce59d4)
2026-07-09 12:53:05 -04:00
bbedward e84e63fa3a default apps: add configuration for geo: URIs
port: 1.5
(cherry picked from commit d0a4c1c56e)
2026-07-09 16:24:19 +00:00
bbedward 6d08b91004 fix(notifications): handle sound-name hint
(cherry picked from commit 0439d017b9)
2026-07-09 12:07:35 -04:00
606 changed files with 68867 additions and 57500 deletions
-56
View File
@@ -1,56 +0,0 @@
name: Sync flake.lock to dank-qml-common
on:
workflow_dispatch:
push:
paths:
- "dank-qml-common"
branches:
- master
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Point flake input at the submodule commit
run: |
set -euo pipefail
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only)
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])")
[ "$submodule_rev" = "$flake_rev" ] && { echo "flake.lock already matches $submodule_rev"; exit 0; }
nix flake lock --override-input dank-qml-common "github:AvengeMedia/dank-qml-common/$submodule_rev"
- name: Commit and push flake.lock update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if git diff --quiet flake.lock; then
echo "No changes to flake.lock"
exit 0
fi
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.lock
git commit -m "nix: sync flake.lock to dank-qml-common submodule"
git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
+1 -34
View File
@@ -27,8 +27,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
@@ -50,39 +48,8 @@ jobs:
- name: Build dms
run: go build -v ./cmd/dms
- name: Build dms (embedded shell)
run: make build
- name: Build dms (distropkg)
run: go build -v -tags 'distro_binary withshell' ./cmd/dms
run: go build -v -tags distro_binary ./cmd/dms
- name: Build dankinstall
run: go build -v ./cmd/dankinstall
build-freebsd:
runs-on: ubuntu-latest
defaults:
run:
working-directory: core
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
- name: Build all packages
env:
GOOS: freebsd
CGO_ENABLED: 0
run: go build -v ./...
- name: Build dms (distropkg, embedded shell)
run: |
make sync-shell
GOOS=freebsd CGO_ENABLED=0 go build -v -tags 'distro_binary withshell' ./cmd/dms
+2
View File
@@ -33,6 +33,8 @@ jobs:
- name: Run NixOS service start test
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
- name: Run greeter niri test
run: nix build .#nixosTests.x86_64-linux.greeter-niri-module -L
- name: Run home-manager module test
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
-2
View File
@@ -10,8 +10,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
+46 -94
View File
@@ -27,11 +27,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- { goos: linux, arch: amd64 }
- { goos: linux, arch: arm64 }
- { goos: freebsd, arch: amd64 }
- { goos: freebsd, arch: arm64 }
arch: [amd64, arm64]
defaults:
run:
@@ -39,8 +35,6 @@ jobs:
env:
TAG: ${{ inputs.tag }}
# linux assets keep their historical arch-only names
ASSET: ${{ matrix.goos == 'linux' && matrix.arch || format('{0}-{1}', matrix.goos, matrix.arch) }}
steps:
- name: Checkout
@@ -48,7 +42,6 @@ jobs:
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
@@ -56,7 +49,6 @@ jobs:
go-version-file: ./core/go.mod
- name: Format check
if: matrix.goos == 'linux'
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
@@ -65,43 +57,38 @@ jobs:
fi
- name: Run tests
if: matrix.goos == 'linux'
run: go test -v ./...
- name: Build dankinstall (${{ env.ASSET }})
if: matrix.goos == 'linux'
- name: Build dankinstall (${{ matrix.arch }})
env:
GOOS: ${{ matrix.goos }}
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ env.ASSET }}
-o ../../dankinstall-${{ matrix.arch }}
cd ../..
gzip -9 -k dankinstall-${{ env.ASSET }}
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
gzip -9 -k dankinstall-${{ matrix.arch }}
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
- name: Sync embedded shell
run: make sync-shell
- name: Build dms (${{ env.ASSET }})
- name: Build dms (${{ matrix.arch }})
env:
GOOS: ${{ matrix.goos }}
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ env.ASSET }}
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-${{ env.ASSET }}
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
gzip -9 -k dms-${{ matrix.arch }}
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
- name: Generate shell completions
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
if: matrix.arch == 'amd64'
run: |
set -eux
chmod +x dms-amd64
@@ -109,58 +96,46 @@ jobs:
./dms-amd64 completion fish > completion.fish
./dms-amd64 completion zsh > completion.zsh
- name: Build dms-distropkg (${{ env.ASSET }})
- name: Build dms-distropkg (${{ matrix.arch }})
env:
GOOS: ${{ matrix.goos }}
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ env.ASSET }}
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-distropkg-${{ env.ASSET }}
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
gzip -9 -k dms-distropkg-${{ matrix.arch }}
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
- name: Upload artifacts (${{ env.ASSET }})
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
- name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ env.ASSET }}
name: core-assets-${{ matrix.arch }}
path: |
core/dankinstall-${{ env.ASSET }}.gz
core/dankinstall-${{ env.ASSET }}.gz.sha256
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts (${{ env.ASSET }}, no installer)
if: matrix.goos != 'linux'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ env.ASSET }}
path: |
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts with completions
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ env.ASSET }}
name: core-assets-${{ matrix.arch }}
path: |
core/dankinstall-${{ env.ASSET }}.gz
core/dankinstall-${{ env.ASSET }}.gz.sha256
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
core/completion.bash
core/completion.fish
core/completion.zsh
@@ -216,13 +191,11 @@ jobs:
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
submodules: recursive
- name: Fetch updated tag after version bump
run: |
git fetch origin --force tag ${TAG}
git checkout ${TAG}
git submodule update --init --recursive
- name: Set up Go
uses: actions/setup-go@v6
@@ -269,23 +242,16 @@ jobs:
### Complete Packages
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
- **`dms-full-freebsd-amd64.tar.gz`** - Complete package for FreeBSD x86_64 systems
- **`dms-full-freebsd-arm64.tar.gz`** - Complete package for FreeBSD ARM64 systems
### Individual Components
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
- **`dms-cli-freebsd-amd64.gz`** - DMS CLI binary for FreeBSD x86_64 systems
- **`dms-cli-freebsd-arm64.gz`** - DMS CLI binary for FreeBSD ARM64 systems
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dms-distropkg-freebsd-amd64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD x86_64 systems
- **`dms-distropkg-freebsd-arm64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD ARM64 systems
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
- **`dms-qml.tar.gz`** - QML source code only
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging)
### Checksums
- **`*.sha256`** - SHA256 checksums for verifying download integrity
@@ -333,22 +299,6 @@ jobs:
# Copy completions
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Replace the DankCommon symlink with real submodule content for packaging
rm quickshell/DankCommon
cp -r dank-qml-common/DankCommon quickshell/DankCommon
# Create full source tarball (GitHub tag archives never contain submodule content)
VERSION_NUM=${TAG#v}
tar --exclude='.git' \
--exclude='.github' \
--exclude='dank-qml-common' \
--exclude='_release_assets' \
--exclude='_core_assets' \
--exclude='RELEASE_BODY.md' \
--transform "s,^\.,DankMaterialShell-${VERSION_NUM},S" \
-czf _release_assets/dms-source.tar.gz .
(cd _release_assets && sha256sum dms-source.tar.gz > dms-source.tar.gz.sha256)
# Create QML source package (exclude build artifacts and git files)
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
@@ -365,8 +315,8 @@ jobs:
# Generate checksum for QML package
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
# Create full packages for each os/architecture
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
# Create full packages for each architecture
for arch in amd64 arm64; do
mkdir -p _temp_full/dms
mkdir -p _temp_full/bin
mkdir -p _temp_full/completions
@@ -409,16 +359,18 @@ jobs:
## Installation Steps
The Quickshell UI is embedded in the `dms` binary. The bundled `dms/`
tree is optional — pass it with `-c` or `DMS_SHELL_DIR` to run a
modified copy instead of the embedded UI.
1. **Install quickshell assets:**
```bash
mkdir -p ~/.config/quickshell
cp -r dms ~/.config/quickshell/
```
1. **Install the DMS CLI binaries:**
2. **Install the DMS CLI binaries:**
```bash
sudo install -m 755 bin/dms /usr/local/bin/dms
```
2. **Install shell completions (optional):**
3. **Install shell completions (optional):**
```bash
# Bash
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
@@ -430,7 +382,7 @@ jobs:
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
```
3. **Start the shell:**
4. **Start the shell:**
```bash
dms run
```
+10 -2
View File
@@ -4,12 +4,14 @@ on:
workflow_dispatch:
inputs:
package:
description: 'Package to build'
description: 'Package to build (dms, dms-greeter, or both)'
required: false
default: 'dms'
type: choice
options:
- dms
- dms-greeter
- both
version:
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
required: false
@@ -29,7 +31,11 @@ jobs:
id: set-packages
run: |
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
if [ "$PACKAGE_INPUT" = "both" ]; then
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
else
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
fi
build-and-upload:
needs: determine-packages
@@ -157,6 +163,8 @@ jobs:
PACKAGE="${{ matrix.package }}"
if [ "$PACKAGE" = "dms" ]; then
COPR_PROJECT="avengemedia/dms"
elif [ "$PACKAGE" = "dms-greeter" ]; then
COPR_PROJECT="avengemedia/danklinux"
else
echo "❌ Unknown package: $PACKAGE"
exit 1
+57 -11
View File
@@ -9,6 +9,7 @@ on:
type: choice
options:
- dms
- dms-greeter
- dms-git
- all
default: "dms"
@@ -35,7 +36,6 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Check for updates
id: check
@@ -74,12 +74,27 @@ jobs:
fi
}
# Helper function to check dms-greeter stable tag
check_dms_greeter_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
# Run from tag with no package specified - update stable package
echo "packages=dms" >> $GITHUB_OUTPUT
# Run from tag with no package specified - update both stable packages
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
@@ -105,7 +120,7 @@ jobs:
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check the stable package and build list of those needing updates
# Check each stable package and build list of those needing updates
PACKAGES_TO_UPDATE=()
if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms")
@@ -113,6 +128,10 @@ jobs:
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
fi
if check_dms_greeter_stable; then
PACKAGES_TO_UPDATE+=("dms-greeter")
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -121,7 +140,7 @@ jobs:
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
echo "✓ Both packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
@@ -145,6 +164,18 @@ jobs:
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_dms_greeter_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
@@ -168,7 +199,6 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Wait before OBS upload
run: sleep 3
@@ -189,9 +219,9 @@ jobs:
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch
# Determine version for dms stable using the API
# Determine version for dms stable and dms-greeter using the API
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
@@ -257,7 +287,7 @@ jobs:
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog"
- name: Update stable version (dms)
- name: Update stable version (dms + dms-greeter)
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
@@ -293,12 +323,25 @@ jobs:
fi
fi
# Update dms-greeter changelog when dms-greeter is in the upload list
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-greeter/debian/changelog"
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
fi
# Update Debian _service files for packages in upload list (download_url paths)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms stable)
# Update download_url paths (for dms, dms-greeter stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
@@ -359,7 +402,7 @@ jobs:
UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=()
# PACKAGES can be a space-separated list (from the "all" check)
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
echo ""
@@ -462,6 +505,9 @@ jobs:
dms-git)
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
+3 -2
View File
@@ -9,6 +9,7 @@ on:
type: choice
options:
- dms
- dms-greeter
- dms-git
- all
default: "dms"
@@ -35,7 +36,6 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install dependencies
run: |
@@ -94,7 +94,6 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
@@ -134,6 +133,7 @@ jobs:
case "$PACKAGE" in
dms) PPA_NAME="dms" ;;
dms-git) PPA_NAME="dms-git" ;;
dms-greeter) PPA_NAME="danklinux" ;;
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
esac
@@ -148,3 +148,4 @@ jobs:
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
echo "- **DankLinux PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> "$GITHUB_STEP_SUMMARY"
+34 -7
View File
@@ -27,6 +27,11 @@ on:
type: boolean
required: false
default: true
build_greeter:
description: 'Build stable dms-greeter package'
type: boolean
required: false
default: true
permissions:
contents: read
@@ -78,7 +83,6 @@ jobs:
with:
ref: ${{ github.event.release.tag_name || (github.event.inputs.version && (startsWith(github.event.inputs.version, 'v') && github.event.inputs.version || format('v{0}', github.event.inputs.version))) || github.ref }}
fetch-depth: 0
submodules: recursive
- name: Record checked-out source commit
run: echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
@@ -101,6 +105,7 @@ jobs:
- name: Inject templates
run: |
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-greeter void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
@@ -174,16 +179,19 @@ jobs:
mkdir -p r2-repo/current
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}"
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
if [ "${{ github.event_name }}" = "schedule" ]; then
BUILD_DMS="false"
BUILD_GREETER="false"
BUILD_GIT="true"
fi
if [ "${{ github.event_name }}" = "release" ]; then
BUILD_DMS="true"
BUILD_GREETER="true"
BUILD_GIT="false"
fi
@@ -208,20 +216,23 @@ jobs:
echo "=== Starting Builds ==="
echo "DMS stable build enabled: $BUILD_DMS"
echo "Greeter stable build enabled: $BUILD_GREETER"
echo "Git build enabled: $BUILD_GIT"
echo "Force rebuild: $FORCE_REBUILD"
cd void-packages
if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then
echo "🔧 Updating stable template for $ARCHIVE_TAG"
if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then
echo "🔧 Updating stable templates for $ARCHIVE_TAG"
TARBALL="$(mktemp)"
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/archive/refs/tags/${ARCHIVE_TAG}.tar.gz"
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
rm -f "$TARBALL"
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
for pkg in dms dms-greeter; do
sed -i "s/^version=.*/version=${RELEASE_VER}/" "srcpkgs/${pkg}/template"
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" "srcpkgs/${pkg}/template"
done
fi
# 1. Build dms-git (development package)
@@ -235,7 +246,7 @@ jobs:
# into $wrksrc (create_wrksrc=yes handles the rest).
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
mkdir -p "$SRC_CACHE"
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
--exclude=void-packages \
--exclude=r2-repo \
--exclude=.git \
@@ -277,6 +288,22 @@ jobs:
fi
fi
# 3. Build stable dms-greeter package
if [ "$BUILD_GREETER" = "true" ]; then
GREETER_VER=$(grep -E '^version=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
GREETER_REV=$(grep -E '^revision=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
EXPECTED_GREETER_FILE="dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps"
if [ -f "../r2-repo/current/$EXPECTED_GREETER_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_GREETER_FILE already exists, skipping build."
else
echo "🔨 Compiling dms-greeter ($GREETER_VER)..."
./xbps-src pkg dms-greeter
rm -f "../r2-repo/current/${EXPECTED_GREETER_FILE}"
cp -L hostdir/binpkgs/dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps ../r2-repo/current/
fi
fi
- name: Index and sign repository
run: |
cd r2-repo/current
-2
View File
@@ -129,5 +129,3 @@ distro/void/masterdir*/
# Often gets built
core/dms
core/internal/shellembed/dist
-4
View File
@@ -1,4 +0,0 @@
[submodule "dank-qml-common"]
path = dank-qml-common
url = https://github.com/AvengeMedia/dank-qml-common.git
branch = master
-8
View File
@@ -36,14 +36,6 @@ repos:
language: system
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
pass_filenames: false
- repo: local
hooks:
- id: i18n-term-variants
name: i18n term variants (no case/punctuation duplicates)
entry: python3 quickshell/translations/check_term_variants.py
language: system
files: ^quickshell/(.*\.qml|translations/(check_term_variants\.py|extract_translations\.py))$
pass_filenames: false
- repo: local
hooks:
- id: no-console-in-qml
-8
View File
@@ -1,13 +1,5 @@
This file is more of a quick reference so I know what to account for before next releases.
# Next
- Go core migrated to dankgo (shared log/paths/errdefs/ipc/shellapp)
- Quickshell UI embedded in the dms binary, -c / DMS_SHELL_DIR override - breaking for setups relying on ~/.config/quickshell/dms auto-discovery (XDG search removed)
- release binaries + all -git packages build with withshell; sockets/pidfiles/env names unchanged, IPC APIVersion 28 wire-compatible
- -git packages (AUR dms-shell-git, fedora, opensuse, void, debian, ubuntu) no longer install /usr/share/quickshell/dms; dms greeter install falls back to the embedded UI when the greeter package is present
- ping over the socket returns {"pong":true} instead of "pong"
- startup: embedded UI resolution is keyed by build-time .dankrev (no per-start content hashing)
# 1.5.0
- Overhauled shadows
- App ID changed to com.danklinux.dms - breaking for window rules
-57
View File
@@ -6,20 +6,6 @@ To contribute fork this repository, make your changes, and open a pull request.
## Setup
Clone with submodules — the shared widget library ([dank-qml-common](https://github.com/AvengeMedia/dank-qml-common)) is vendored at `dank-qml-common/` and symlinked into `quickshell/DankCommon`:
```bash
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git
# or, in an existing clone:
git submodule update --init
```
To have `git pull` keep the submodule in sync automatically (moving it to the commit this repo points at, no separate `git submodule update` step), set:
```bash
git config submodule.recurse true
```
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
```bash
@@ -42,47 +28,6 @@ This will provide:
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
## Building and running
The Quickshell UI is embedded into the `dms` binary at build time. `make build` copies `quickshell/` into `core/internal/shellembed/dist/` (generated, never committed) and compiles with the `withshell` tag. `make dev` builds without the tag — that binary carries no UI and requires an explicit config dir.
```bash
make build # embedded binary at core/bin/dms
make dev # untagged development build
make run # dev build, then launch against the live quickshell/ tree
```
The UI config dir resolves in order: `-c <dir>`, `DMS_SHELL_DIR`, the dir a running instance is using, then the embedded UI. Each candidate must contain `shell.qml`. `make run` uses `-c $(pwd)/quickshell`, so QML edits hot-reload from the working tree.
The Go core depends on [dankgo](https://github.com/AvengeMedia/dankgo) for logging, XDG paths, the IPC transport, and the quickshell process lifecycle. To develop against a local dankgo checkout, create a gitignored `go.work` at the repo root:
```
go 1.26.1
use (
./core
../dankgo
)
```
## Shared widgets (dank-qml-common)
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
2. Commit and PR those changes in the `dank-qml-common` repo: `cd dank-qml-common && git switch -c my-change`, push, open the PR there.
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
The submodule URL in `.gitmodules` is HTTPS so CI and anonymous clones keep working. To push over SSH instead of being prompted for credentials, add a push rewrite to your git config — fetches stay HTTPS, pushes use SSH:
```bash
git config --global url."git@github.com:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/"
```
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
## VSCode Setup
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
@@ -159,8 +104,6 @@ Text {
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
+2 -19
View File
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build dev run clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
all: build
@@ -27,12 +27,6 @@ build:
@$(MAKE) -C $(CORE_DIR) build
@echo "Build complete"
dev:
@$(MAKE) -C $(CORE_DIR) dev
run: dev
@$(BUILD_DIR)/$(BINARY_NAME) run -c $(CURDIR)/$(SHELL_DIR)
clean:
@echo "Cleaning build artifacts..."
@$(MAKE) -C $(CORE_DIR) clean
@@ -41,12 +35,6 @@ clean:
lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh
# Pull the latest dank-qml-common and pin it everywhere it is consumed
# (submodule pointer + nix flake input). Commit both in one change.
update-common:
git submodule update --remote --merge dank-qml-common
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
# Installation targets
install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@@ -55,9 +43,8 @@ install-bin:
install-shell:
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
@test -e $(SHELL_DIR)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
@mkdir -p $(SHELL_INSTALL_DIR)
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@echo "Shell files installed"
@@ -72,9 +59,6 @@ install-completions:
@echo "Shell completions installed"
install-systemd:
ifneq ($(shell uname),Linux)
@echo "Skipping systemd user service (non-Linux); start the shell from your compositor config with 'dms run'"
else
@echo "Installing systemd user service..."
@mkdir -p $(SYSTEMD_USER_DIR)
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
@@ -82,7 +66,6 @@ else
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
endif
install-icon:
@echo "Installing icon..."
+1 -12
View File
@@ -98,7 +98,7 @@ Notification center with grouping, rich text support, and keyboard navigation.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
**Plugin System**
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
@@ -132,17 +132,6 @@ dms plugins search # Browse plugin registry
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
## Dank Projects
DMS is one piece of the suite. The rest lives in its own repos:
- [dank-greeter](https://github.com/AvengeMedia/dank-greeter) - greetd login screen with the Dank Material aesthetic. The Greeter tab in DMS settings is the front-end for it.
- [dankcalendar](https://github.com/AvengeMedia/dankcalendar) - Local, Google, Microsoft, and CalDAV calendars for the dank desktop.
- [dgop](https://github.com/AvengeMedia/dgop) - System monitoring CLI and API that powers the process list and dashboard widgets.
- [dsearch](https://github.com/AvengeMedia/danksearch) - Fast file search that powers file results in the launcher.
- [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
- [dankgo](https://github.com/AvengeMedia/dankgo) - Common Go modules behind the single binary apps.
## Development
See component-specific documentation:
+26 -50
View File
@@ -3,8 +3,6 @@ BINARY_NAME_INSTALL=dankinstall
SOURCE_DIR=cmd/dms
SOURCE_DIR_INSTALL=cmd/dankinstall
BUILD_DIR=bin
SHELL_SRC=../quickshell
EMBED_DIR=internal/shellembed/dist
PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin
@@ -23,46 +21,17 @@ BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUI
# Architecture to build for dist target (amd64, arm64, or all)
ARCH ?= all
# Target OSes for dist builds
DIST_OSES ?= linux freebsd
ifeq ($(ARCH),all)
DIST_ARCHS = amd64 arm64
else
DIST_ARCHS = $(ARCH)
endif
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
# Default target
all: build
# Copy the quickshell UI into the embed dir (gitignored) so tagged builds
# can bake it into the binary. Dev-only files are stripped; scripts/ is kept
# minus its dev entries, since Theme.qml and BluetoothService.qml run
# gtk.sh/qt.sh/bluez-card-profile.lua out of the resolved shell dir.
# tar -h dereferences the DankCommon submodule symlink; go:embed rejects
# symlinks. .qmlls.ini is excluded at copy time: it's a symlink into the
# quickshell runtime VFS and dereferencing it fails whenever the shell isn't
# running.
sync-shell:
@test -e $(SHELL_SRC)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
@rm -rf $(EMBED_DIR)
@mkdir -p $(EMBED_DIR)
@tar -C $(SHELL_SRC) --exclude=.qmlls.ini -chf - . | tar -C $(EMBED_DIR) -xf -
@rm -rf $(EMBED_DIR)/.git* $(EMBED_DIR)/.github
@find $(EMBED_DIR) -type d \( -name .claude -o -name .vscode \) -prune -exec rm -rf {} +
@rm -f $(EMBED_DIR)/AGENTS.md $(EMBED_DIR)/qmlformat-all.sh
@rm -f $(EMBED_DIR)/scripts/i18nsync.py $(EMBED_DIR)/scripts/build-vscode-vsix.sh $(EMBED_DIR)/scripts/qmllint-entrypoints.sh
@rm -f $(EMBED_DIR)/scripts/spam-notifications.sh $(EMBED_DIR)/scripts/verify-notifications.sh
@rm -f $(EMBED_DIR)/translations/*.py $(EMBED_DIR)/translations/WORKFLOW.md
@cd $(EMBED_DIR) && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum | sha256sum | cut -c1-16 > .dankrev
# Build the main binary (dms)
build: sync-shell
build:
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 $(GO) build -tags withshell $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
dankinstall:
@@ -71,18 +40,26 @@ dankinstall:
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
# Build distro binaries (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair
dist: sync-shell
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..."
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
dist:
ifeq ($(ARCH),all)
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
@mkdir -p $(BUILD_DIR)
@for os in $(DIST_OSES); do \
for arch in $(DIST_ARCHS); do \
echo "Building for $$os/$$arch..."; \
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch ./$(SOURCE_DIR) || exit 1; \
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \
done; \
done
@echo "Distribution builds complete"
@echo "Building for linux/amd64..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
@echo "Building for linux/arm64..."
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
@echo "Distribution builds complete:"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
else
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
@mkdir -p $(BUILD_DIR)
@echo "Building for linux/$(ARCH)..."
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
@echo "Distribution build complete:"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
endif
build-all: build dankinstall
@@ -122,7 +99,7 @@ uninstall-dankinstall:
clean:
@echo "Cleaning build artifacts..."
@rm -rf $(BUILD_DIR) $(EMBED_DIR)
@rm -rf $(BUILD_DIR)
@echo "Clean complete"
test:
@@ -164,11 +141,10 @@ print-version:
help:
@echo "Available targets:"
@echo " all - Build the main binary (dms) (default)"
@echo " build - Build the main binary (dms) with the embedded UI"
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
@echo " build - Build the main binary (dms)"
@echo " dankinstall - Build dankinstall binary"
@echo " dist - Build dms for linux/freebsd amd64/arm64 (no update/greeter)"
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow"
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
@echo " build-all - Build both binaries"
@echo " install - Install dms to $(INSTALL_DIR)"
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
+3 -3
View File
@@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication.
- `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages)
- `dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
### Color Picker
@@ -109,7 +109,7 @@ make test # Run tests
**Distribution build:**
```bash
make dist # Build without update features
make dist # Build without update/greeter features
```
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
@@ -170,7 +170,7 @@ sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t ki
| `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
| `--yes` | `-y` | Required for headless mode - confirms installation without interactive prompts |
| `--yes` | `-y` | Required for headless mode confirms installation without interactive prompts |
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
+1 -30
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
@@ -16,7 +15,7 @@ import (
var authCmd = &cobra.Command{
Use: "auth",
Short: "Manage DMS authentication sync",
Long: "Manage PAM/authentication setup for the DMS lock screen",
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
}
var authSyncCmd = &cobra.Command{
@@ -212,31 +211,3 @@ func syncAuthInTerminal(nonInteractive bool) error {
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
return runCommandInTerminal(shellCmd)
}
func runCommandInTerminal(shellCmd string) error {
terminals := []struct {
name string
args []string
}{
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
{"kitty", []string{"bash", "-c", shellCmd}},
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
}
for _, t := range terminals {
if _, err := exec.LookPath(t.name); err != nil {
continue
}
cmd := exec.Command(t.name, t.args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
}
+78 -30
View File
@@ -6,10 +6,10 @@ import (
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
"github.com/spf13/cobra"
)
@@ -19,6 +19,63 @@ var versionCmd = &cobra.Command{
Run: runVersion,
}
var runCmd = &cobra.Command{
Use: "run",
Short: "Launch quickshell with DMS configuration",
Long: "Launch quickshell with DMS configuration (qs -c dms)",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
daemon, _ := cmd.Flags().GetBool("daemon")
session, _ := cmd.Flags().GetBool("session")
if v, _ := cmd.Flags().GetString("log-level"); v != "" {
if err := os.Setenv("DMS_LOG_LEVEL", v); err != nil {
log.Fatalf("Failed to set DMS_LOG_LEVEL: %v", err)
}
}
if v, _ := cmd.Flags().GetString("log-file"); v != "" {
if err := os.Setenv("DMS_LOG_FILE", v); err != nil {
log.Fatalf("Failed to set DMS_LOG_FILE: %v", err)
}
}
log.ApplyEnvOverrides()
config.CleanupStrayHyprlandConfFile(log.Infof)
if daemon {
runShellDaemon(session)
} else {
runShellInteractive(session)
}
},
}
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Restart quickshell with DMS configuration",
Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
restartShell()
},
}
var restartDetachedCmd = &cobra.Command{
Use: "restart-detached <pid>",
Hidden: true,
Args: cobra.ExactArgs(1),
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
runDetachedRestart(args[0])
},
}
var killCmd = &cobra.Command{
Use: "kill",
Short: "Kill running DMS shell processes",
Long: "Kill all running quickshell processes with DMS configuration",
Run: func(cmd *cobra.Command, args []string) {
killShell()
},
}
var ipcCmd = &cobra.Command{
Use: "ipc",
Short: "Send IPC commands to running DMS shell",
@@ -207,41 +264,29 @@ func formatVersion(version string) string {
return fmt.Sprintf("dms %s", version)
}
var baseVersionRe = regexp.MustCompile(`^([\d.]+)`)
// Installed UI trees, for builds without an embedded UI.
var shellVersionPaths = []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
}
func getBaseVersion() string {
if ver := parseBaseVersion(shellembed.Version()); ver != "" {
return ver
paths := []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
}
for _, path := range shellVersionPaths {
content, err := os.ReadFile(path)
if err != nil {
continue
}
if ver := parseBaseVersion(string(content)); ver != "" {
return ver
for _, path := range paths {
if content, err := os.ReadFile(path); err == nil {
ver := strings.TrimSpace(string(content))
ver = strings.TrimPrefix(ver, "v")
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
if matches := re.FindStringSubmatch(ver); matches != nil {
return matches[1]
}
}
}
}
// Fallback
return "1.0.2"
}
func parseBaseVersion(raw string) string {
matches := baseVersionRe.FindStringSubmatch(strings.TrimPrefix(strings.TrimSpace(raw), "v"))
if matches == nil {
return ""
}
return matches[1]
}
func startDebugServer() error {
server.CLIVersion = Version
return server.Start(true)
@@ -698,9 +743,12 @@ func checkAllPluginsCLI() error {
}
func getCommonCommands() []*cobra.Command {
commands := shellApp.Commands()
return append(commands, []*cobra.Command{
return []*cobra.Command{
versionCmd,
runCmd,
restartCmd,
restartDetachedCmd,
killCmd,
ipcCmd,
debugSrvCmd,
pluginsCmd,
@@ -727,5 +775,5 @@ func getCommonCommands() []*cobra.Command {
trashCmd,
systemCmd,
switchUserCmd,
}...)
}
}
-1
View File
@@ -40,7 +40,6 @@ var resolveIncludeCmd = &cobra.Command{
"layout.kdl",
"outputs.kdl",
"binds.kdl",
"input.kdl",
"cursor.conf",
"layout.conf",
"outputs.conf",
+6 -8
View File
@@ -401,12 +401,12 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
}
func getDMSShellVersion() (version, path string) {
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
versionFile := filepath.Join(shellApp.ConfigPath(), "VERSION")
if err := findConfig(nil, nil); err == nil && configPath != "" {
versionFile := filepath.Join(configPath, "VERSION")
if data, err := os.ReadFile(versionFile); err == nil {
return strings.TrimSpace(string(data)), shellApp.ConfigPath()
return strings.TrimSpace(string(data)), configPath
}
return "installed", shellApp.ConfigPath()
return "installed", configPath
}
if dmsPath, err := config.LocateDMSConfig(); err == nil {
@@ -450,8 +450,8 @@ func checkDMSInstallation() []checkResult {
var results []checkResult
dmsPath := ""
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
dmsPath = shellApp.ConfigPath()
if err := findConfig(nil, nil); err == nil && configPath != "" {
dmsPath = configPath
} else if path, err := config.LocateDMSConfig(); err == nil {
dmsPath = path
}
@@ -833,8 +833,6 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
return "systemd-networkd"
case network.BackendConnMan:
return "ConnMan"
case network.BackendWpaSupplicant:
return "wpa_supplicant"
default:
return ""
}
+3 -22
View File
@@ -26,7 +26,7 @@ var updateCmd = &cobra.Command{
Use: "update",
Short: "Update DankMaterialShell to the latest version",
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
PreRunE: shellApp.ResolveConfig,
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
runUpdate()
},
@@ -98,7 +98,7 @@ func runUpdate() {
}
log.Info("Update complete! Restarting DMS...")
shellApp.Restart()
restartShell()
}
func updateArchLinux() error {
@@ -250,10 +250,6 @@ func updateOtherDistros() error {
return fmt.Errorf("failed to fetch changes: %w", err)
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
if currentTag != "" {
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
latestTagOutput, err := latestTagCmd.Output()
@@ -295,10 +291,6 @@ func updateOtherDistros() error {
return fmt.Errorf("update cancelled")
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
return nil
}
@@ -328,21 +320,10 @@ func updateOtherDistros() error {
return fmt.Errorf("update cancelled")
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
fmt.Println("\nUpdate complete!")
return nil
}
func updateSubmodules() error {
submoduleCmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
submoduleCmd.Stdout = os.Stdout
submoduleCmd.Stderr = os.Stderr
return submoduleCmd.Run()
}
func offerReclone(dmsPath string) bool {
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
reader := bufio.NewReader(os.Stdin)
@@ -361,7 +342,7 @@ func offerReclone(dmsPath string) bool {
}
fmt.Println("Cloning fresh copy...")
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
if err := cloneCmd.Run(); err != nil {
File diff suppressed because it is too large Load Diff
+150
View File
@@ -0,0 +1,150 @@
package main
import (
"errors"
"reflect"
"strings"
"testing"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
"github.com/spf13/cobra"
)
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
origGreeterConfigSyncFn := greeterConfigSyncFn
origSharedAuthSyncFn := sharedAuthSyncFn
t.Cleanup(func() {
greeterConfigSyncFn = origGreeterConfigSyncFn
sharedAuthSyncFn = origSharedAuthSyncFn
})
var calls []string
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
if dmsPath != "/tmp/dms" {
t.Fatalf("unexpected dmsPath %q", dmsPath)
}
if compositor != "niri" {
t.Fatalf("unexpected compositor %q", compositor)
}
if sudoPassword != "" {
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
}
calls = append(calls, "configs")
return nil
}
var gotOptions sharedpam.SyncAuthOptions
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
if sudoPassword != "" {
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
}
gotOptions = options
calls = append(calls, "auth")
return nil
}
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
ForceGreeterAuth: true,
}, func() {
calls = append(calls, "before-auth")
})
if err != nil {
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
}
wantCalls := []string{"configs", "before-auth", "auth"}
if !reflect.DeepEqual(calls, wantCalls) {
t.Fatalf("call order = %v, want %v", calls, wantCalls)
}
if !gotOptions.ForceGreeterAuth {
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
}
}
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
origGreeterConfigSyncFn := greeterConfigSyncFn
origSharedAuthSyncFn := sharedAuthSyncFn
t.Cleanup(func() {
greeterConfigSyncFn = origGreeterConfigSyncFn
sharedAuthSyncFn = origSharedAuthSyncFn
})
greeterConfigSyncFn = func(string, string, func(string), string) error {
return errors.New("config sync failed")
}
authCalled := false
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
authCalled = true
return nil
}
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
if err == nil || err.Error() != "config sync failed" {
t.Fatalf("expected config sync error, got %v", err)
}
if authCalled {
t.Fatal("expected auth sync not to run after config sync failure")
}
}
func TestGreeterStatusStateDirUsesNixOSDefault(t *testing.T) {
if got := greeterStatusStateDir("", true); got != nixOSGreeterStateDir {
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, nixOSGreeterStateDir)
}
}
func TestGreeterStatusStateDirHonorsExplicitOverrideOnNixOS(t *testing.T) {
command := "dms-greeter --cache-dir /srv/dms-greeter --command niri"
if got := greeterStatusStateDir(command, true); got != "/srv/dms-greeter" {
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, "/srv/dms-greeter")
}
}
func TestExtractGreeterWrapperFromEnvCommand(t *testing.T) {
command := "env LIBSEAT_BACKEND=logind DMS_VOID=1 /usr/bin/dms-greeter --command niri"
if got := extractGreeterWrapperFromCommand(command); got != "/usr/bin/dms-greeter" {
t.Fatalf("extractGreeterWrapperFromCommand() = %q, want %q", got, "/usr/bin/dms-greeter")
}
}
func TestRejectNixOSGreeterMutationBlocksImperativeCommands(t *testing.T) {
origGreeterIsNixOSFn := greeterIsNixOSFn
greeterIsNixOSFn = func() bool { return true }
t.Cleanup(func() {
greeterIsNixOSFn = origGreeterIsNixOSFn
})
for _, commandName := range []string{"install", "enable", "sync", "uninstall"} {
t.Run(commandName, func(t *testing.T) {
root := &cobra.Command{Use: "dms"}
greeterCommand := &cobra.Command{Use: "greeter"}
mutationCommand := &cobra.Command{Use: commandName}
root.AddCommand(greeterCommand)
greeterCommand.AddCommand(mutationCommand)
err := rejectNixOSGreeterMutation(mutationCommand)
if err == nil {
t.Fatalf("expected NixOS greeter %s to be rejected", commandName)
}
if !strings.Contains(err.Error(), "dms greeter "+commandName+" is disabled on NixOS") {
t.Fatalf("unexpected error: %v", err)
}
if strings.Contains(err.Error(), "/var/cache/dms-greeter") {
t.Fatalf("NixOS remediation should not recommend the non-NixOS cache path: %v", err)
}
})
}
}
func TestRejectNixOSGreeterMutationAllowsOtherDistros(t *testing.T) {
origGreeterIsNixOSFn := greeterIsNixOSFn
greeterIsNixOSFn = func() bool { return false }
t.Cleanup(func() {
greeterIsNixOSFn = origGreeterIsNixOSFn
})
if err := rejectNixOSGreeterMutation(&cobra.Command{Use: "sync"}); err != nil {
t.Fatalf("expected non-NixOS greeter command to be allowed, got %v", err)
}
}
+60 -1
View File
@@ -1,9 +1,19 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var customConfigPath string
var configPath string
var rootCmd = &cobra.Command{
Use: "dms",
Short: "dms CLI",
@@ -11,5 +21,54 @@ var rootCmd = &cobra.Command{
}
func init() {
rootCmd.PersistentFlags().StringVarP(shellApp.CustomConfigVar(), "config", "c", "", "Path to a UI config dir (containing shell.qml) to use instead of the embedded UI (env: DMS_SHELL_DIR)")
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
}
func findConfig(cmd *cobra.Command, args []string) error {
if customConfigPath != "" {
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
shellPath := filepath.Join(customConfigPath, "shell.qml")
info, statErr := os.Stat(shellPath)
if statErr == nil && !info.IsDir() {
configPath = customConfigPath
log.Debug("Using config from: %s", configPath)
return nil
}
if statErr != nil {
return fmt.Errorf("custom config path error: %w", statErr)
}
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
}
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
if len(getAllDMSPIDs()) == 0 {
os.Remove(configStateFile)
} else {
statePath := strings.TrimSpace(string(data))
shellPath := filepath.Join(statePath, "shell.qml")
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
log.Debug("Using config from active session state file: %s", statePath)
configPath = statePath
log.Debug("Using config from: %s", configPath)
return nil
}
os.Remove(configStateFile)
}
}
log.Debug("No custom path or active session, searching default XDG locations...")
var err error
configPath, err = config.LocateDMSConfig()
if err != nil {
return err
}
log.Debug("Using config from: %s", configPath)
return nil
}
+3 -3
View File
@@ -231,16 +231,16 @@ func setPopoutScreenshotMode(begin bool) {
fn = "begin"
}
cmdArgs := []string{"ipc"}
if pid, ok := shellApp.SessionPID(); ok {
if pid, ok := getFirstDMSPID(); ok {
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
} else {
if err := shellApp.ResolveConfig(nil, nil); err != nil {
if err := findConfig(nil, nil); err != nil {
return
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
cmdArgs = append(cmdArgs, "-p", configPath)
}
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
_ = exec.Command("qs", cmdArgs...).Run()
+6 -23
View File
@@ -11,6 +11,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
@@ -143,10 +144,6 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
niriFile: "alttab.kdl",
niriContent: func(_ string) string { return config.NiriAlttabConfig },
},
"input": {
niriFile: "input.kdl",
niriContent: func(_ string) string { return config.NiriInputConfig },
},
"outputs": {
niriFile: "outputs.kdl",
hyprFile: "outputs.lua",
@@ -208,12 +205,7 @@ func detectTerminal() (string, error) {
}
func detectCompositorForSetup() (string, error) {
var compositors []string
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
if utils.CommandExists(candidate) {
compositors = append(compositors, candidate)
}
}
compositors := greeter.DetectCompositors()
switch len(compositors) {
case 0:
@@ -222,20 +214,11 @@ func detectCompositorForSetup() (string, error) {
return strings.ToLower(compositors[0]), nil
}
fmt.Println("Multiple compositors detected:")
for i, compositor := range compositors {
fmt.Printf("%d) %s\n", i+1, compositor)
selected, err := greeter.PromptCompositorChoice(compositors)
if err != nil {
return "", err
}
fmt.Printf("\nChoice (1-%d): ", len(compositors))
var response string
fmt.Scanln(&response)
choice := 0
fmt.Sscanf(strings.TrimSpace(response), "%d", &choice)
if choice < 1 || choice > len(compositors) {
return "", fmt.Errorf("invalid choice")
}
return strings.ToLower(compositors[choice-1]), nil
return strings.ToLower(selected), nil
}
func runSetupDmsConfig(name string) error {
+137
View File
@@ -0,0 +1,137 @@
package main
import (
"context"
"fmt"
"os"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
// runit (Void Linux) service helpers. Services live in /etc/sv and are "enabled"
// by symlinking them into the /var/service supervision dir, so the greeter
// commands branch on isRunit() instead of shelling systemctl.
const (
runitSvDir = "/etc/sv"
runitServiceDir = "/var/service"
)
// isRunit reports whether this system is supervised by runit (Void Linux).
func isRunit() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func runitServiceInstalled(name string) bool {
fi, err := os.Stat(runitSvDir + "/" + name)
return err == nil && fi.IsDir()
}
func runitServiceEnabled(name string) bool {
_, err := os.Lstat(runitServiceDir + "/" + name)
return err == nil
}
// enableRunitService links a service into /var/service (idempotent).
func enableRunitService(name string) error {
if !runitServiceInstalled(name) {
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
}
if runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), "", "ln", "-sf",
runitSvDir+"/"+name, runitServiceDir+"/"+name)
}
// disableRunitService removes a service's supervision symlink.
func disableRunitService(name string) error {
if !runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), "", "rm", "-f",
runitServiceDir+"/"+name)
}
// ensureRunitSeat sets up the seat access a Wayland greeter needs on runit (the
// equivalent of logind on systemd): enables seatd and adds the greeter user to
// the seat/video/input groups. Failures are reported but non-fatal.
func ensureRunitSeat(greeterUser string) {
if runitServiceInstalled("seatd") {
if err := enableRunitService("seatd"); err != nil {
fmt.Printf(" ⚠ could not enable seatd: %v\n", err)
} else {
fmt.Println(" ✓ seatd enabled")
}
} else {
fmt.Println(" ⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
}
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
fmt.Printf(" ⚠ could not add %s to seat groups: %v\n", greeterUser, err)
} else {
fmt.Printf(" ✓ %s added to seat groups (_seatd, video, input)\n", greeterUser)
}
}
// ensureVoidLogindGreeter configures the elogind-backed greeter on Void.
func ensureVoidLogindGreeter(greeterUser string) {
for _, service := range []string{"dbus", "elogind"} {
if err := enableRunitService(service); err != nil {
fmt.Printf(" ⚠ could not enable %s: %v\n", service, err)
} else {
fmt.Printf(" ✓ %s enabled\n", service)
}
}
greeter.EnsureVoidGreetdRunScript(func(msg string) { fmt.Println(" " + msg) }, "")
if runitServiceEnabled("seatd") {
if err := disableRunitService("seatd"); err != nil {
fmt.Printf(" ⚠ could not disable seatd: %v\n", err)
} else {
fmt.Println(" ✓ seatd disabled (elogind manages the seat)")
}
}
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "video,input", greeterUser); err != nil {
fmt.Printf(" ⚠ could not add %s to video/input groups: %v\n", greeterUser, err)
} else {
fmt.Printf(" ✓ %s added to video/input groups (elogind manages the seat)\n", greeterUser)
}
}
// ensureGreetdPamRundir provides XDG_RUNTIME_DIR to runit greeter sessions.
func ensureGreetdPamRundir() {
const pamPath = "/etc/pam.d/greetd"
data, err := os.ReadFile(pamPath)
if err != nil {
fmt.Printf(" ⚠ could not read %s: %v\n", pamPath, err)
return
}
if strings.Contains(string(data), "pam_rundir") {
return
}
line := "session optional pam_rundir.so"
if err := privesc.Run(context.Background(), "", "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
fmt.Printf(" ⚠ could not add pam_rundir to %s: %v\n", pamPath, err)
return
}
fmt.Println(" ✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
}
// startGreeterHint returns the init-appropriate "start greetd now" command.
func startGreeterHint() string {
if isRunit() {
return " sudo sv up greetd"
}
return " sudo systemctl start greetd"
}
+8
View File
@@ -12,6 +12,14 @@ import (
var Version = "dev"
func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd)
+8
View File
@@ -12,6 +12,14 @@ import (
var Version = "dev"
func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
+5 -2
View File
@@ -101,8 +101,11 @@ func getServerSocketPath() string {
runtimeDir = os.TempDir()
}
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
return sessionSock
if parentPID, ok := sessionParentPID(os.Getenv("WAYLAND_DISPLAY")); ok {
sessionSock := filepath.Join(runtimeDir, fmt.Sprintf("danklinux-%d.sock", parentPID))
if _, err := os.Stat(sessionSock); err == nil {
return sessionSock
}
}
entries, err := os.ReadDir(runtimeDir)
+696 -6
View File
@@ -4,20 +4,591 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
)
type ipcTargets map[string]map[string][]string
// getProcessExitCode returns the exit code from a ProcessState.
// For normal exits, returns the exit code directly.
// For signal termination, returns 128 + signal number (Unix convention).
func getProcessExitCode(state *os.ProcessState) int {
if state == nil {
return 1
}
if code := state.ExitCode(); code != -1 {
return code
}
// Process was killed by signal - extract signal number
if status, ok := state.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
return 128 + int(status.Signal())
}
}
return 1
}
var isSessionManaged bool
func execDetachedRestart(targetPID int) {
selfPath, err := os.Executable()
if err != nil {
return
}
cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
cmd.Start()
}
func runDetachedRestart(targetPIDStr string) {
targetPID, err := strconv.Atoi(targetPIDStr)
if err != nil {
return
}
time.Sleep(200 * time.Millisecond)
proc, err := os.FindProcess(targetPID)
if err == nil {
proc.Signal(syscall.SIGTERM)
}
time.Sleep(500 * time.Millisecond)
killShell()
runShellDaemon(false)
}
func getRuntimeDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
return runtime
}
return os.TempDir()
}
func appendLogEnv(env []string) []string {
if v := os.Getenv("DMS_LOG_LEVEL"); v != "" {
env = append(env, "DMS_LOG_LEVEL="+v)
}
if v := os.Getenv("DMS_LOG_FILE"); v != "" {
env = append(env, "DMS_LOG_FILE="+v)
}
return env
}
func withDMSExecutable(env []string) []string {
selfPath, err := os.Executable()
if err != nil {
return env
}
return append(env, "DMS_EXECUTABLE="+selfPath)
}
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
}
func getSessionFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.session", os.Getpid()))
}
func writePIDFile(childPID int) error {
pidFile := getPIDFilePath()
if display := os.Getenv("WAYLAND_DISPLAY"); display != "" {
if err := os.WriteFile(getSessionFilePath(), []byte(display), 0o644); err != nil {
log.Warnf("Failed to write session file: %v", err)
}
}
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
}
func removePIDFile() {
os.Remove(getPIDFilePath())
os.Remove(getSessionFilePath())
}
func getAllDMSPIDs() []int {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var pids []int
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
pidFile := filepath.Join(dir, entry.Name())
data, err := os.ReadFile(pidFile)
if err != nil {
continue
}
childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
os.Remove(pidFile)
continue
}
proc, err := os.FindProcess(childPID)
if err != nil {
os.Remove(pidFile)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
os.Remove(pidFile)
continue
}
pids = append(pids, childPID)
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
if parentProc, err := os.FindProcess(parentPID); err == nil {
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
pids = append(pids, parentPID)
}
}
}
}
return pids
}
func runShellInteractive(session bool) {
isSessionManaged = session
go printASCII()
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
errChan := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
}()
ensureFontCache()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = withDMSExecutable(append(os.Environ(), "DMS_SOCKET="+socketPath))
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
cmd.Env = append(cmd.Env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
cmd.Env = appendLogEnv(cmd.Env)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
tracker := &stderrTracker{parent: os.Stderr}
cmd.Stderr = tracker
startTime := time.Now()
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting quickshell: %v", err)
}
// Write PID file for the quickshell child process
if err := writePIDFile(cmd.Process.Pid); err != nil {
log.Warnf("Failed to write PID file: %v", err)
}
defer removePIDFile()
defer func() {
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
if err := cmd.Wait(); err != nil {
errChan <- fmt.Errorf("quickshell exited: %w", err)
} else {
errChan <- fmt.Errorf("quickshell exited")
}
}()
for {
select {
case sig := <-sigChan:
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
return
}
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
case <-time.After(500 * time.Millisecond):
}
log.Infof("\nReceived signal %v, shutting down...", sig)
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
return
case err := <-errChan:
log.Error(err)
cancel()
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
}
}
}
func restartShell() {
pids := getAllDMSPIDs()
if len(pids) == 0 {
log.Info("No running DMS shell instances found. Starting daemon...")
runShellDaemon(false)
return
}
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("Error finding process %d: %v", pid, err)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
if err := proc.Signal(syscall.SIGUSR1); err != nil {
log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
} else {
log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
}
}
}
func killShell() {
pids := getAllDMSPIDs()
if len(pids) == 0 {
log.Info("No running DMS shell instances found.")
return
}
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("Error finding process %d: %v", pid, err)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
if err := proc.Kill(); err != nil {
log.Errorf("Error killing process %d: %v", pid, err)
} else {
log.Infof("Killed DMS process with PID %d", pid)
}
}
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") {
continue
}
if strings.HasSuffix(entry.Name(), ".pid") || strings.HasSuffix(entry.Name(), ".session") {
os.Remove(filepath.Join(dir, entry.Name()))
}
}
}
func runShellDaemon(session bool) {
isSessionManaged = session
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
if !isDaemonChild {
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
cmd.Env = os.Environ()
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
return
}
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
errChan := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
}()
ensureFontCache()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = withDMSExecutable(append(os.Environ(), "DMS_SOCKET="+socketPath))
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
// ! TODO - remove when QS 0.3 is up and we can use the pragma
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
cmd.Env = append(cmd.Env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
cmd.Env = appendLogEnv(cmd.Env)
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
log.Fatalf("Error opening /dev/null: %v", err)
}
defer devNull.Close()
cmd.Stdin = devNull
cmd.Stdout = devNull
tracker := &stderrTracker{parent: devNull}
cmd.Stderr = tracker
startTime := time.Now()
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
// Write PID file for the quickshell child process
if err := writePIDFile(cmd.Process.Pid); err != nil {
log.Warnf("Failed to write PID file: %v", err)
}
defer removePIDFile()
defer func() {
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
if err := cmd.Wait(); err != nil {
errChan <- fmt.Errorf("quickshell exited: %w", err)
} else {
errChan <- fmt.Errorf("quickshell exited")
}
}()
for {
select {
case sig := <-sigChan:
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
return
}
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
case <-time.After(500 * time.Millisecond):
}
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
return
case <-errChan:
cancel()
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
}
}
}
var qsHasAnyDisplay = sync.OnceValue(func() bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
@@ -61,17 +632,17 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
func buildQsIPCBaseArgs() ([]string, error) {
cmdArgs := []string{"ipc"}
switch pid, ok := shellApp.SessionPID(); {
switch pid, ok := getSessionDMSPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := shellApp.ResolveConfig(nil, nil); err != nil {
if err := findConfig(nil, nil); err != nil {
return nil, err
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
cmdArgs = append(cmdArgs, "-p", configPath)
}
return cmdArgs, nil
}
@@ -128,6 +699,101 @@ func getShellIPCCompletions(args []string, _ string) []string {
return nil
}
func getFirstDMSPID() (int, bool) {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
continue
}
proc, err := os.FindProcess(pid)
if err != nil {
continue
}
if proc.Signal(syscall.Signal(0)) != nil {
continue
}
return pid, true
}
return 0, false
}
func sessionParentPID(display string) (int, bool) {
if display == "" {
return 0, false
}
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
for _, entry := range entries {
name := entry.Name()
if !strings.HasPrefix(name, "danklinux-") || !strings.HasSuffix(name, ".session") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, name))
if err != nil || strings.TrimSpace(string(data)) != display {
continue
}
parentStr := strings.TrimSuffix(strings.TrimPrefix(name, "danklinux-"), ".session")
parentPID, err := strconv.Atoi(parentStr)
if err != nil {
continue
}
return parentPID, true
}
return 0, false
}
func getSessionDMSPID() (int, bool) {
parentPID, ok := sessionParentPID(os.Getenv("WAYLAND_DISPLAY"))
if !ok {
return getFirstDMSPID()
}
data, err := os.ReadFile(filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", parentPID)))
if err != nil {
return getFirstDMSPID()
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
return getFirstDMSPID()
}
proc, err := os.FindProcess(pid)
if err != nil || proc.Signal(syscall.Signal(0)) != nil {
return getFirstDMSPID()
}
return pid, true
}
func runShellIPCCommand(args []string) {
if len(args) == 0 {
printIPCHelp()
@@ -280,12 +946,36 @@ func rebuildFontCache() {
}
}
type stderrTracker struct {
mu sync.Mutex
buf strings.Builder
parent io.Writer
}
func (s *stderrTracker) Write(p []byte) (n int, err error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.buf.Len() < 8192 {
s.buf.Write(p)
}
if s.parent != nil {
return s.parent.Write(p)
}
return len(p), nil
}
func (s *stderrTracker) String() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.buf.String()
}
// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
func logStartupFailure(exitCode int, uptime time.Duration, stderrTail string) {
if uptime >= 5*time.Second || exitCode == 0 || exitCode > 128 {
func logStartupFailure(startTime time.Time, exitCode int, tracker *stderrTracker) {
if time.Since(startTime) >= 5*time.Second || exitCode == 0 || exitCode > 128 {
return
}
if containsFontCrashSignature(stderrTail) {
if containsFontCrashSignature(tracker.String()) {
log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
} else {
log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
-82
View File
@@ -1,82 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
"github.com/AvengeMedia/dankgo/shellapp"
)
var shellApp = shellapp.New(shellapp.Config{
ID: "danklinux",
EnvPrefix: "DMS",
QSAppID: "com.danklinux.dms",
Version: Version,
Embedded: embeddedShell{},
Boot: bootBackend,
PreLaunch: preLaunch,
ExtraEnv: dmsExtraEnv,
OnUIExit: logStartupFailure,
})
type embeddedShell struct{}
func (embeddedShell) Available() bool { return shellembed.Available() }
func (embeddedShell) Extract(baseDir string) (string, error) { return shellembed.Extract(baseDir) }
func (embeddedShell) Prune(baseDir, keep string) { shellembed.Prune(baseDir, keep) }
type dmsBackend struct {
srv *server.Server
done chan error
}
func (b *dmsBackend) SocketPath() string { return b.srv.SocketPath() }
func (b *dmsBackend) Close() { b.srv.Close() }
func (b *dmsBackend) Done() <-chan error { return b.done }
func bootBackend(ctx context.Context) (shellapp.Backend, error) {
config.CleanupStrayHyprlandConfFile(log.Infof)
server.CLIVersion = Version
srv := server.New()
if err := srv.Listen(); err != nil {
return nil, err
}
backend := &dmsBackend{srv: srv, done: make(chan error, 1)}
go func() {
defer func() {
if r := recover(); r != nil {
backend.done <- fmt.Errorf("server panic: %v", r)
}
}()
backend.done <- srv.Serve(false)
}()
return backend, nil
}
func preLaunch() {
go printASCII()
ensureFontCache()
}
func dmsExtraEnv(string) []string {
var env []string
if selfPath, err := os.Executable(); err == nil {
env = append(env, "DMS_EXECUTABLE="+selfPath)
}
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
return env
}
+61
View File
@@ -1,7 +1,9 @@
package main
import (
"fmt"
"os/exec"
"slices"
"strings"
)
@@ -26,3 +28,62 @@ func isArchPackageInstalled(packageName string) bool {
err := cmd.Run()
return err == nil
}
type systemdServiceState struct {
Name string
EnabledState string
NeedsDisable bool
Exists bool
}
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
cmd := exec.Command("systemctl", "is-enabled", serviceName)
output, err := cmd.Output()
stateStr := strings.TrimSpace(string(output))
if err != nil {
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
isKnownState := slices.Contains(knownStates, stateStr)
if !isKnownState {
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
}
}
shouldDisable := false
switch stateStr {
case "enabled", "enabled-runtime", "static", "indirect", "alias":
shouldDisable = true
case "disabled", "masked", "masked-runtime", "not-found":
shouldDisable = false
default:
shouldDisable = true
}
return stateStr, shouldDisable, nil
}
func getSystemdServiceState(serviceName string) (*systemdServiceState, error) {
state := &systemdServiceState{
Name: serviceName,
Exists: false,
}
enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName)
if err != nil {
return nil, fmt.Errorf("failed to check enabled state: %w", err)
}
state.EnabledState = enabledState
state.NeedsDisable = needsDisable
if enabledState == "not-found" {
state.Exists = false
return state, nil
}
state.Exists = true
return state, nil
}
+27 -27
View File
@@ -1,13 +1,14 @@
module github.com/AvengeMedia/DankMaterialShell/core
go 1.26.4
go 1.26.1
require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/alecthomas/chroma/v2 v2.24.1
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0
github.com/fsnotify/fsnotify v1.10.1
github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847
@@ -17,12 +18,12 @@ require (
github.com/stretchr/testify v1.11.1
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.3.0
github.com/yuin/goldmark v1.8.4
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.5.0
go.etcd.io/bbolt v1.4.3
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
golang.org/x/image v0.44.0
tailscale.com v1.100.0
golang.org/x/image v0.39.0
tailscale.com v1.96.5
)
require (
@@ -30,28 +31,28 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/charmbracelet/log v1.0.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.4 // indirect
github.com/coder/websocket v1.8.15 // indirect
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect
github.com/dlclark/regexp2/v2 v2.5.1 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 // indirect
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 // indirect
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/mdlayher/netlink v1.11.2 // indirect
github.com/mdlayher/socket v0.6.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mdlayher/netlink v1.11.1 // indirect
github.com/mdlayher/socket v0.6.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -61,15 +62,14 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
)
require (
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
@@ -79,22 +79,22 @@ require (
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-alpha.4
github.com/go-git/go-git/v6 v6.0.0-alpha.2
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0
github.com/mattn/go-isatty v0.0.23
github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/muesli/termenv v0.16.0
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
+56 -56
View File
@@ -1,7 +1,5 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b h1:UwX1H4BkzazL7ips9ljnHzBXGttYARcIi5Njj9aqIt4=
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
@@ -13,8 +11,8 @@ github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukD
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -50,23 +48,25 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20=
github.com/dlclark/regexp2/v2 v2.5.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -85,14 +85,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ=
github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E=
github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1 h1:gmqi2jvsreu0s8JMLylYDFq4sbjHwwlhktMw0DUg3mA=
github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrOXjo/0EnvRZx6D++I86UYjPgAQ=
github.com/go-git/go-git/v6 v6.0.0-alpha.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ=
github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 h1:wH21vHuv323v9x78JNFNJ6P7HEAsdwr9yq2k9/o4zEE=
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU=
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc=
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0/go.mod h1:1Lr7/vYEYyl6Ir9Ku0tKrCIRreM5zovv0Jdx2MPSM4s=
github.com/go-git/go-git/v6 v6.0.0-alpha.2 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI=
github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak=
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 h1:2WmHkJINIjgXXYDGik8d3oJvFA3DAwPy00csDJ3vo+o=
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -117,8 +117,8 @@ github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5G
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -128,16 +128,16 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mdlayher/netlink v1.11.1 h1:T136gDS6Gkt+hLncaBwKdW5GpEC8Z0ykqimOebVoal0=
github.com/mdlayher/netlink v1.11.1/go.mod h1:ao4LjamyK4Uq9L8+fQzqFYpAncbeCdwbvd9Edv/pYnc=
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -180,8 +180,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@@ -193,36 +193,36 @@ github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g=
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
@@ -235,5 +235,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
-1
View File
@@ -264,7 +264,6 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
{"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"input.kdl", NiriInputConfig},
{"outputs.kdl", ""},
{"cursor.kdl", ""},
{"windowrules.kdl", ""},
@@ -0,0 +1,17 @@
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
@@ -1,15 +0,0 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
input {
mouse {
accel-speed 0.0
}
touchpad {
tap
accel-speed 0.0
natural-scroll
}
}
+25 -2
View File
@@ -34,6 +34,31 @@ input {
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
@@ -52,7 +77,6 @@ input {
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
@@ -247,4 +271,3 @@ include optional=true "dms/alttab.kdl"
include optional=true "dms/binds.kdl"
include optional=true "dms/outputs.kdl"
include optional=true "dms/cursor.kdl"
include optional=true "dms/input.kdl"
+2 -2
View File
@@ -17,5 +17,5 @@ var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl
var NiriBindsConfig string
//go:embed embedded/niri-input.kdl
var NiriInputConfig string
//go:embed embedded/niri-greeter.kdl
var NiriGreeterConfig string
+2 -15
View File
@@ -520,7 +520,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
Progress: 0.90,
Step: "Cloning DankMaterialShell...",
IsComplete: false,
CommandInfo: "git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git",
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
}
configDir := filepath.Dir(dmsPath)
@@ -528,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return fmt.Errorf("failed to create quickshell config directory: %w", err)
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recurse-submodules",
cloneCmd := exec.CommandContext(ctx, "git", "clone",
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
@@ -553,8 +553,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
m.syncDMSSubmodules(ctx, dmsPath)
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
m.log("DankMaterialShell cloned successfully")
return nil
@@ -593,8 +591,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
m.syncDMSSubmodules(ctx, dmsPath)
m.log("DankMaterialShell updated successfully (git variant)")
return nil
}
@@ -613,19 +609,10 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
m.syncDMSSubmodules(ctx, dmsPath)
m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
return nil
}
func (m *ManualPackageInstaller) syncDMSSubmodules(ctx context.Context, dmsPath string) {
submoduleCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "submodule", "update", "--init", "--recursive")
if err := submoduleCmd.Run(); err != nil {
m.logError("Failed to update submodules", err)
}
}
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing xwayland-satellite from source...")
+15 -13
View File
@@ -1,15 +1,9 @@
package errdefs
import (
dankerrdefs "github.com/AvengeMedia/dankgo/errdefs"
)
type ErrorType = dankerrdefs.ErrorType
type CustomError = dankerrdefs.CustomError
type ErrorType int
const (
ErrTypeNotLinux ErrorType = dankerrdefs.AppErrorBase + iota
ErrTypeNotLinux ErrorType = iota
ErrTypeInvalidArchitecture
ErrTypeUnsupportedDistribution
ErrTypeUnsupportedVersion
@@ -28,8 +22,20 @@ const (
ErrTypeGeneric
)
type CustomError struct {
Type ErrorType
Message string
}
func (e *CustomError) Error() string {
return e.Message
}
func NewCustomError(errType ErrorType, message string) error {
return dankerrdefs.NewCustomError(errType, message)
return &CustomError{
Type: errType,
Message: message,
}
}
const (
@@ -41,10 +47,6 @@ const (
ErrWifiDisabled = "wifi-disabled"
ErrAlreadyConnected = "already-connected"
ErrConnectionFailed = "connection-failed"
ErrHotspotIPConfigFailed = "hotspot-ip-config-failed"
ErrHotspotSupplicantFailed = "hotspot-supplicant-failed"
ErrHotspotFailed = "hotspot-failed"
)
var (
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/dbusutil"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
)
@@ -0,0 +1,91 @@
# AppArmor profile for dms-greeter
#
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
# Manual edits will be overwritten on next sync.
#
# Mode: complain (denials are logged, nothing is blocked)
# To switch to enforce after validating with `aa-logprof`:
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
#
#include <tunables/global>
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# The launcher script itself
/usr/bin/dms-greeter r,
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
/var/cache/dms-greeter/ rw,
/var/cache/dms-greeter/** rwlk,
# DMS config — packaged path
/usr/share/quickshell/dms-greeter/ r,
/usr/share/quickshell/dms-greeter/** r,
/usr/share/quickshell/ r,
/usr/share/quickshell/** r,
# DMS config — system and user overrides
/etc/dms/ r,
/etc/dms/** r,
/usr/share/dms/ r,
/usr/share/dms/** r,
/home/*/.config/quickshell/ r,
/home/*/.config/quickshell/** r,
/root/.config/quickshell/ r,
/root/.config/quickshell/** r,
# greetd / PAM — read-only for session setup
/etc/greetd/ r,
/etc/greetd/** r,
/etc/pam.d/ r,
/etc/pam.d/** r,
/usr/lib/pam.d/ r,
/usr/lib/pam.d/** r,
# Compositor binaries — run unconfined so each compositor uses its own profile
/usr/bin/niri Ux,
/usr/bin/hyprland Ux,
/usr/bin/Hyprland Ux,
/usr/bin/sway Ux,
/usr/bin/labwc Ux,
/usr/bin/scroll Ux,
/usr/bin/miracle-wm Ux,
/usr/bin/mango Ux,
# Quickshell — run unconfined (has its own compositor profile on some distros)
/usr/bin/qs Ux,
/usr/bin/quickshell Ux,
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
/run/user/[0-9]*/ rw,
/run/user/[0-9]*/** rw,
# DRM / GPU devices (required for Wayland compositor startup)
/dev/dri/ r,
/dev/dri/* rw,
/dev/udmabuf rw,
# Input devices
/dev/input/ r,
/dev/input/* r,
# Systemd journal / logging
/run/systemd/journal/socket rw,
/dev/log rw,
# Shell helper binaries invoked by the launcher script
/usr/bin/env ix,
/usr/bin/mkdir ix,
/usr/bin/cat ix,
/usr/bin/grep ix,
/usr/bin/dirname ix,
/usr/bin/basename ix,
/usr/bin/command ix,
/bin/env ix,
/bin/mkdir ix,
# Signal management (compositor lifecycle)
signal (send, receive) set=("term", "int", "hup", "kill"),
}
File diff suppressed because it is too large Load Diff
+350
View File
@@ -0,0 +1,350 @@
package greeter
import (
"os"
"path/filepath"
"strings"
"testing"
)
func writeTestFile(t *testing.T, path string, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("failed to create parent dir for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("failed to write %s: %v", path, err)
}
}
func TestResolveGreeterThemeSyncState(t *testing.T) {
t.Parallel()
tests := []struct {
name string
settingsJSON string
sessionJSON string
wantSourcePath string
wantResolvedWallpaper string
wantDynamicOverrideUsed bool
}{
{
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": "Pictures/blue.jpg",
"matugenScheme": "scheme-tonal-spot",
"iconTheme": "Papirus"
}`,
sessionJSON: `{"isLightMode":true}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
wantDynamicOverrideUsed: true,
},
{
name: "dynamic theme without override uses desktop colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": ""
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "",
wantDynamicOverrideUsed: false,
},
{
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
settingsJSON: `{
"currentThemeName": "purple",
"greeterWallpaperPath": "/tmp/blue.jpg"
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "/tmp/blue.jpg",
wantDynamicOverrideUsed: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
writeTestFile(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
}
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
}
wantResolvedWallpaper := tt.wantResolvedWallpaper
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
}
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
}
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
}
})
}
}
func TestUpsertInitialSession(t *testing.T) {
t.Parallel()
baseConfig := `[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command niri"
`
t.Run("inserts initial session", func(t *testing.T) {
t.Parallel()
got := upsertInitialSession(baseConfig, "alice", "/var/cache/dms-greeter", true)
if !strings.Contains(got, "[initial_session]") {
t.Fatalf("expected [initial_session] section, got:\n%s", got)
}
if !strings.Contains(got, `user = "alice"`) {
t.Fatalf("expected alice user in initial session, got:\n%s", got)
}
if !strings.Contains(got, `dms greeter launch-session --from-memory --cache-dir`) {
t.Fatalf("expected stable launch-session command, got:\n%s", got)
}
if strings.Contains(got, `exec niri`) {
t.Fatalf("initial session must not bake the desktop Exec command, got:\n%s", got)
}
})
t.Run("updates existing initial session", func(t *testing.T) {
t.Parallel()
existing := baseConfig + `
[initial_session]
user = "bob"
command = "old-command"
`
got := upsertInitialSession(existing, "alice", "/var/cache/dms-greeter", true)
if strings.Contains(got, `user = "bob"`) {
t.Fatalf("expected bob to be replaced, got:\n%s", got)
}
if !strings.Contains(got, `dms greeter launch-session --from-memory`) {
t.Fatalf("expected launch-session command, got:\n%s", got)
}
})
t.Run("removes initial session when disabled", func(t *testing.T) {
t.Parallel()
existing := baseConfig + `
[initial_session]
user = "alice"
command = "niri"
`
got := upsertInitialSession(existing, "", "", false)
if strings.Contains(got, "[initial_session]") {
t.Fatalf("expected initial session removed, got:\n%s", got)
}
if !strings.Contains(got, "[default_session]") {
t.Fatalf("expected default session preserved, got:\n%s", got)
}
})
}
func TestStripDesktopExecCodes(t *testing.T) {
t.Parallel()
got := stripDesktopExecCodes("niri --session %f")
want := "niri --session"
if got != want {
t.Fatalf("stripDesktopExecCodes = %q, want %q", got, want)
}
}
func TestBuildGreetdCommand(t *testing.T) {
t.Parallel()
tests := []struct {
name string
wrapper string
compositor string
dmsPath string
useVoidLogind bool
want string
}{
{
name: "standard command",
wrapper: "/usr/bin/dms-greeter",
compositor: "Niri",
want: "/usr/bin/dms-greeter --command niri --cache-dir /var/cache/dms-greeter",
},
{
name: "void selects elogind and keeps custom DMS path",
wrapper: "/usr/bin/dms-greeter",
compositor: "Niri",
dmsPath: "/usr/share/quickshell/dms-greeter",
useVoidLogind: true,
want: "env LIBSEAT_BACKEND=logind DMS_VOID=1 /usr/bin/dms-greeter --command niri --cache-dir /var/cache/dms-greeter -p /usr/share/quickshell/dms-greeter",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := buildGreetdCommand(tt.wrapper, tt.compositor, tt.dmsPath, tt.useVoidLogind); got != tt.want {
t.Fatalf("buildGreetdCommand() = %q, want %q", got, tt.want)
}
})
}
}
func TestVoidLogindGreeterCommand(t *testing.T) {
t.Parallel()
const oldCommand = "/usr/bin/dms-greeter --command niri -C /etc/greetd/niri.kdl"
const want = "env LIBSEAT_BACKEND=logind DMS_VOID=1 " + oldCommand
if got := voidLogindGreeterCommand(oldCommand); got != want {
t.Fatalf("voidLogindGreeterCommand() = %q, want %q", got, want)
}
if got := voidLogindGreeterCommand(want); got != want {
t.Fatalf("voidLogindGreeterCommand() must be idempotent, got %q", got)
}
}
func TestResolveGreeterAutoLoginState(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": true,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"lastSuccessfulUser": "alice",
"lastSessionDesktopId": "niri.desktop"
}`)
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if !enabled || loginUser != "alice" || sessionID != "niri.desktop" {
t.Fatalf("got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
}
}
func TestResolveGreeterAutoLoginStateIgnoresStaleSessionExec(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": true,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"lastSuccessfulUser": "alice",
"lastSessionId": "/nix/store/old-session/share/wayland-sessions/example.desktop",
"lastSessionExec": "/nix/store/old-session/bin/start-example-session"
}`)
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if !enabled || loginUser != "alice" || sessionID != "example.desktop" {
t.Fatalf("got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
}
got := upsertInitialSession("", loginUser, cacheDir, true)
if strings.Contains(got, "/nix/store/old-session") {
t.Fatalf("initial session must not include stale store path, got:\n%s", got)
}
}
func TestResolveGreeterAutoLoginStateIgnoresMemoryFlag(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": false,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"autoLoginEnabled": true,
"lastSuccessfulUser": "alice",
"lastSessionExec": "niri"
}`)
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if enabled || loginUser != "" || sessionID != "" {
t.Fatalf("expected disabled with empty user/session, got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
}
}
func TestResolveSessionExecInDirs(t *testing.T) {
t.Parallel()
oldDir := filepath.Join(t.TempDir(), "wayland-sessions")
newDir := filepath.Join(t.TempDir(), "wayland-sessions")
writeTestFile(t, filepath.Join(oldDir, "example.desktop"), `[Desktop Entry]
Name=Example Session
Exec=/nix/store/old-session/bin/start-example-session
`)
writeTestFile(t, filepath.Join(newDir, "example.desktop"), `[Desktop Entry]
Name=Example Session
Exec=/run/current-system/sw/bin/start-example-session
`)
got, err := resolveSessionExecInDirs("example.desktop", []string{newDir, oldDir})
if err != nil {
t.Fatalf("resolveSessionExecInDirs returned error: %v", err)
}
if got != "/run/current-system/sw/bin/start-example-session" {
t.Fatalf("resolveSessionExecInDirs = %q", got)
}
}
func TestClearGreeterAutoLoginMemory(t *testing.T) {
t.Parallel()
memoryPath := filepath.Join(t.TempDir(), "memory.json")
writeTestFile(t, memoryPath, `{
"autoLoginEnabled": true,
"lastSuccessfulUser": "alice"
}`)
if err := clearGreeterAutoLoginMemory(memoryPath, ""); err != nil {
t.Fatalf("clearGreeterAutoLoginMemory returned error: %v", err)
}
data, err := os.ReadFile(memoryPath)
if err != nil {
t.Fatalf("failed to read memory file: %v", err)
}
if strings.Contains(string(data), "autoLoginEnabled") {
t.Fatalf("expected autoLoginEnabled removed, got: %s", string(data))
}
if !strings.Contains(string(data), "lastSuccessfulUser") {
t.Fatalf("expected other memory fields preserved, got: %s", string(data))
}
}
+200
View File
@@ -0,0 +1,200 @@
package greeter
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
func sessionDesktopIDFromPath(path string) string {
id := strings.TrimSpace(path)
if id == "" {
return ""
}
if strings.ContainsAny(id, "/\\") {
id = filepath.Base(id)
}
if id == "" {
return ""
}
if !strings.HasSuffix(id, ".desktop") {
id += ".desktop"
}
return id
}
func sessionDesktopIDFromMemory(mem greeterAutoLoginMemory) string {
if id := sessionDesktopIDFromPath(mem.LastSessionDesktopID); id != "" {
return id
}
return sessionDesktopIDFromPath(mem.LastSessionID)
}
func sessionDesktopDirs() []string {
seen := make(map[string]bool)
dirs := make([]string, 0, 8)
addBase := func(base string) {
base = strings.TrimSpace(base)
if base == "" {
return
}
for _, sub := range []string{"wayland-sessions", "xsessions"} {
dir := filepath.Join(base, sub)
if seen[dir] {
continue
}
seen[dir] = true
dirs = append(dirs, dir)
}
}
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
addBase(dataHome)
} else if home, err := os.UserHomeDir(); err == nil && home != "" {
addBase(filepath.Join(home, ".local", "share"))
}
if dataDirs := os.Getenv("XDG_DATA_DIRS"); dataDirs != "" {
for _, dir := range strings.Split(dataDirs, ":") {
addBase(dir)
}
} else {
addBase("/usr/local/share")
addBase("/usr/share")
}
return dirs
}
func ResolveSessionExec(sessionID string) (string, error) {
return resolveSessionExecInDirs(sessionID, sessionDesktopDirs())
}
func resolveSessionExecInDirs(sessionID string, dirs []string) (string, error) {
id := sessionDesktopIDFromPath(sessionID)
if id == "" {
return "", fmt.Errorf("session id is empty")
}
for _, dir := range dirs {
path := filepath.Join(dir, id)
execLine, err := execFromDesktopFile(path)
if err == nil {
return execLine, nil
}
if !os.IsNotExist(err) {
return "", err
}
}
return "", fmt.Errorf("session desktop file %q was not found", id)
}
// parseExecString splits a Desktop Entry Exec= value into argv without
// involving a shell, mirroring quickshell's DesktopEntry::parseExecString
// (string quoting, value escapes, field code stripping).
func parseExecString(execLine string) []string {
var args []string
var cur strings.Builder
inString := false
escape := 0
percent := false
for _, c := range execLine {
switch {
case escape == 0 && c == '\\':
escape = 1
case inString:
switch {
case c == '\\':
escape++
if escape == 4 {
cur.WriteByte('\\')
escape = 0
}
case escape == 2:
cur.WriteRune(c)
escape = 0
case escape != 0:
switch c {
case 's':
cur.WriteByte(' ')
case 'n':
cur.WriteByte('\n')
case 't':
cur.WriteByte('\t')
case 'r':
cur.WriteByte('\r')
default:
cur.WriteRune(c)
}
escape = 0
case c == '"' || c == '\'':
inString = false
default:
cur.WriteRune(c)
}
case escape != 0:
cur.WriteRune(c)
escape = 0
case percent:
if c == '%' {
cur.WriteByte('%')
}
percent = false
case c == '%':
percent = true
case c == '"' || c == '\'':
inString = true
case c == ' ':
if cur.Len() > 0 {
args = append(args, cur.String())
cur.Reset()
}
default:
cur.WriteRune(c)
}
}
if cur.Len() > 0 {
args = append(args, cur.String())
}
return args
}
func LaunchSessionByID(sessionID string) error {
execLine, err := ResolveSessionExec(sessionID)
if err != nil {
return err
}
argv := parseExecString(strings.TrimSpace(execLine))
if len(argv) == 0 {
return fmt.Errorf("session %q has an empty Exec command", sessionID)
}
resolved, err := exec.LookPath(argv[0])
if err != nil {
return fmt.Errorf("session %q command %q not found: %w", sessionID, argv[0], err)
}
env := append(os.Environ(), "XDG_SESSION_TYPE=wayland")
return syscall.Exec(resolved, argv, env)
}
func LaunchSessionFromMemory(cacheDir, homeDir string) error {
enabled, _, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
return err
}
if !enabled {
return fmt.Errorf("greeter auto-login is disabled")
}
if sessionID == "" {
return fmt.Errorf("greeter auto-login has no remembered session")
}
return LaunchSessionByID(sessionID)
}
@@ -0,0 +1,57 @@
package greeter
import (
"path/filepath"
"reflect"
"testing"
)
func TestParseExecString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
exec string
want []string
}{
{"plain", "niri --session", []string{"niri", "--session"}},
{"extra spaces", "niri --session", []string{"niri", "--session"}},
{"double quoted arg", `env "with space" run`, []string{"env", "with space", "run"}},
{"single quoted arg", `env 'with space' run`, []string{"env", "with space", "run"}},
{"escaped quote in quotes", `sh "say \\"hi\\""`, []string{"sh", `say "hi"`}},
{"field code dropped", "gnome-session %U", []string{"gnome-session"}},
{"field code mid-arg", "app --url=%u --run", []string{"app", "--url=", "--run"}},
{"literal percent", "app 100%% done", []string{"app", "100%", "done"}},
{"shell metachars stay literal", "sh -c $(reboot); echo", []string{"sh", "-c", "$(reboot);", "echo"}},
{"empty", "", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseExecString(tt.exec); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("parseExecString(%q) = %#v, want %#v", tt.exec, got, tt.want)
}
})
}
}
func TestExecFromDesktopFileOnlyReadsDesktopEntryGroup(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "example.desktop")
writeTestFile(t, path, `[Desktop Action other]
Exec=/wrong/binary
[Desktop Entry]
Name=Example
Exec = /right/binary --flag
`)
got, err := execFromDesktopFile(path)
if err != nil {
t.Fatalf("execFromDesktopFile returned error: %v", err)
}
if got != "/right/binary --flag" {
t.Fatalf("execFromDesktopFile = %q, want %q", got, "/right/binary --flag")
}
}
+548
View File
@@ -0,0 +1,548 @@
package greeter
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
var monitorWallpaperSanitizer = regexp.MustCompile(`[^a-zA-Z0-9]+`)
func userGreeterCacheDir(cacheDir, username string) string {
return filepath.Join(cacheDir, "users", username)
}
func isUserOwnedGreeterCacheSlot(path, username string) bool {
if strings.TrimSpace(username) == "" {
return false
}
userDir, err := filepath.Abs(userGreeterCacheDir(GreeterCacheDir, username))
if err != nil {
return false
}
abs, err := filepath.Abs(path)
if err != nil {
return false
}
return abs == userDir || strings.HasPrefix(abs, userDir+string(filepath.Separator))
}
func UserIsInGreeterGroup(username string) bool {
group := DetectGreeterGroup()
if !utils.HasGroup(group) {
return false
}
groupsCmd := exec.Command("groups", username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return false
}
return strings.Contains(string(groupsOutput), group)
}
func CanSyncOwnUserGreeterProfile(username string) bool {
currentUser, err := user.Current()
if err != nil || currentUser.Username != username {
return false
}
if !UserIsInGreeterGroup(username) {
return false
}
usersDir := filepath.Join(GreeterCacheDir, "users")
if st, err := os.Stat(usersDir); err != nil || !st.IsDir() {
return false
}
testFile := filepath.Join(usersDir, ".write-test-"+username)
file, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
if err != nil {
return false
}
_ = file.Close()
_ = os.Remove(testFile)
return true
}
func GreeterProfileSyncReady() bool {
if command := readGreeterSessionCommand(); command != "" && strings.Contains(command, "dms-greeter") {
return true
}
usersDir := filepath.Join(GreeterCacheDir, "users")
st, err := os.Stat(usersDir)
return err == nil && st.IsDir()
}
func readGreeterSessionCommand() string {
data, err := os.ReadFile("/etc/greetd/config.toml")
if err != nil {
return ""
}
inDefaultSession := false
for line := range strings.SplitSeq(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
inDefaultSession = strings.EqualFold(strings.Trim(trimmed, "[]"), "default_session")
continue
}
if !inDefaultSession {
continue
}
if idx := strings.Index(trimmed, "#"); idx >= 0 {
trimmed = strings.TrimSpace(trimmed[:idx])
}
if !strings.HasPrefix(trimmed, "command") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
if command != "" {
return command
}
}
return ""
}
// SyncUserProfileCache writes the current user's theme slot under users/<username>/
// without modifying greetd or other system configuration. Requires membership in the
// greeter group and a prior full greeter setup by an administrator.
func SyncUserProfileCache(logFunc func(string)) error {
if logFunc == nil {
logFunc = func(string) {}
}
if !GreeterProfileSyncReady() {
return fmt.Errorf("greeter is not set up on this system yet; an administrator must run 'dms greeter install' or 'dms greeter sync' once first")
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to resolve current user: %w", err)
}
if !CanSyncOwnUserGreeterProfile(currentUser.Username) {
group := DetectGreeterGroup()
return fmt.Errorf("cannot sync greeter profile: you must be in the %s group with write access to %s/users\nAsk an administrator to run:\n sudo usermod -aG %s %s\nThen log out and back in before running:\n dms greeter sync --profile",
group, GreeterCacheDir, group, currentUser.Username)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
return fmt.Errorf("failed to resolve greeter color source: %w", err)
}
if err := syncUserGreeterCacheSlot(homeDir, GreeterCacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{
profileOnly: true,
}); err != nil {
return err
}
logFunc(fmt.Sprintf(" → %s/users/%s/", GreeterCacheDir, currentUser.Username))
return nil
}
func canWriteUserGreeterCacheSlot(dest, username string) bool {
return isUserOwnedGreeterCacheSlot(dest, username) && CanSyncOwnUserGreeterProfile(username)
}
type userSlotSyncOpts struct {
sudoPassword string
profileOnly bool
username string
}
func (o userSlotSyncOpts) useDirectWrite(dest string) bool {
if !o.profileOnly {
return false
}
return canWriteUserGreeterCacheSlot(dest, o.username)
}
func isGreeterCachePath(path string) bool {
abs, err := filepath.Abs(path)
if err != nil {
return true
}
cacheAbs, err := filepath.Abs(GreeterCacheDir)
if err != nil {
return true
}
if abs == cacheAbs {
return true
}
return strings.HasPrefix(abs, cacheAbs+string(filepath.Separator))
}
func greeterCacheOwner() string {
greeterGroup := DetectGreeterGroup()
daemonUser := DetectGreeterUser()
return daemonUser + ":" + greeterGroup
}
func ensureGreeterCacheSubdir(dir string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dir) {
if err := os.MkdirAll(dir, 0o770); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
return nil
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "mkdir", "-p", dir); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), opts.sudoPassword, "chown", owner, dir); err != nil {
if fallbackErr := privesc.Run(context.Background(), opts.sudoPassword, "chown", "root:"+DetectGreeterGroup(), dir); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", dir, err)
}
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "chmod", "2770", dir); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", dir, err)
}
return nil
}
func setGreeterCacheFileOwnership(path, sudoPassword string) error {
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, path); err != nil {
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+DetectGreeterGroup(), path); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", path, err)
}
}
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", path); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", path, err)
}
return nil
}
func syncUserGreeterCacheSlot(homeDir, cacheDir, username string, state greeterThemeSyncState, logFunc func(string), opts userSlotSyncOpts) error {
if strings.TrimSpace(username) == "" {
return nil
}
opts.username = username
userDir := userGreeterCacheDir(cacheDir, username)
if err := ensureGreeterCacheSubdir(userDir, opts); err != nil {
return err
}
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
settingsBytes, err := os.ReadFile(settingsPath)
if err != nil {
return fmt.Errorf("failed to read settings for user cache slot: %w", err)
}
settingsMap := map[string]any{}
if strings.TrimSpace(string(settingsBytes)) != "" {
if err := json.Unmarshal(settingsBytes, &settingsMap); err != nil {
return fmt.Errorf("failed to parse settings for user cache slot: %w", err)
}
}
if customTheme, ok := settingsMap["customThemeFile"].(string); ok && strings.TrimSpace(customTheme) != "" {
resolvedTheme := customTheme
if !filepath.IsAbs(resolvedTheme) {
resolvedTheme = filepath.Join(homeDir, resolvedTheme)
}
if st, statErr := os.Stat(resolvedTheme); statErr == nil && !st.IsDir() {
destTheme := filepath.Join(userDir, "custom-theme.json")
if err := copyFileWithPrivesc(resolvedTheme, destTheme, opts); err != nil {
return err
}
settingsMap["customThemeFile"] = destTheme
}
}
settingsBytes, err = json.Marshal(settingsMap)
if err != nil {
return fmt.Errorf("failed to marshal settings for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "settings.json"), settingsBytes, opts); err != nil {
return err
}
sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json")
sessionBytes, err := os.ReadFile(sessionPath)
if err != nil {
return fmt.Errorf("failed to read session for user cache slot: %w", err)
}
sessionMap := map[string]any{}
if strings.TrimSpace(string(sessionBytes)) != "" {
if err := json.Unmarshal(sessionBytes, &sessionMap); err != nil {
return fmt.Errorf("failed to parse session for user cache slot: %w", err)
}
}
if err := localizeSessionWallpapers(sessionMap, userDir, opts); err != nil {
return err
}
sessionBytes, err = json.Marshal(sessionMap)
if err != nil {
return fmt.Errorf("failed to marshal session for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "session.json"), sessionBytes, opts); err != nil {
return err
}
colorsSource := state.effectiveColorsSource(homeDir)
if err := copyFileWithPrivesc(colorsSource, filepath.Join(userDir, "colors.json"), opts); err != nil {
return fmt.Errorf("failed to copy colors for user cache slot: %w", err)
}
if err := syncUserProfileImage(homeDir, userDir, opts); err != nil {
return err
}
rootOverride := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
userOverride := filepath.Join(userDir, "greeter_wallpaper_override.jpg")
if st, statErr := os.Stat(rootOverride); statErr == nil && !st.IsDir() {
if err := copyFileWithPrivesc(rootOverride, userOverride, opts); err != nil {
return fmt.Errorf("failed to copy greeter wallpaper override for user cache slot: %w", err)
}
} else if opts.useDirectWrite(userOverride) {
_ = os.Remove(userOverride)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", userOverride)
}
logFunc(fmt.Sprintf("✓ Synced per-user greeter cache for %s", username))
return nil
}
func localizeSessionWallpapers(session map[string]any, userDir string, opts userSlotSyncOpts) error {
stringKeys := []struct {
key string
prefix string
}{
{"wallpaperPath", "wallpaper"},
{"wallpaperPathLight", "wallpaper-light"},
{"wallpaperPathDark", "wallpaper-dark"},
}
for _, item := range stringKeys {
if err := localizeWallpaperStringField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
mapKeys := []struct {
key string
prefix string
}{
{"monitorWallpapers", "wallpaper-monitor"},
{"monitorWallpapersLight", "wallpaper-monitor-light"},
{"monitorWallpapersDark", "wallpaper-monitor-dark"},
}
for _, item := range mapKeys {
if err := localizeWallpaperMapField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
return nil
}
func localizeWallpaperStringField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok {
return nil
}
path, ok := raw.(string)
if !ok || strings.TrimSpace(path) == "" {
return nil
}
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix, opts)
if err != nil {
return err
}
if dest != "" {
session[key] = dest
}
return nil
}
func localizeWallpaperMapField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok || raw == nil {
return nil
}
values, ok := raw.(map[string]any)
if !ok {
return nil
}
for monitor, rawPath := range values {
path, ok := rawPath.(string)
if !ok || strings.TrimSpace(path) == "" {
continue
}
safeMonitor := monitorWallpaperSanitizer.ReplaceAllString(monitor, "-")
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix+"-"+safeMonitor, opts)
if err != nil {
return err
}
if dest != "" {
values[monitor] = dest
}
}
return nil
}
func copyWallpaperIntoUserCache(srcPath, userDir, prefix string, opts userSlotSyncOpts) (string, error) {
if strings.TrimSpace(srcPath) == "" {
return "", nil
}
st, err := os.Stat(srcPath)
if err != nil || st.IsDir() {
return "", nil
}
ext := filepath.Ext(srcPath)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, prefix+ext)
if err := copyFileWithPrivesc(srcPath, dest, opts); err != nil {
return "", err
}
return dest, nil
}
func copyFileWithPrivesc(src, dest string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
if !isGreeterCachePath(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", dest)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", src, dest); err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", src, dest, err)
}
return setGreeterCacheFileOwnership(dest, opts.sudoPassword)
}
func writeFileWithPrivesc(path string, data []byte, opts userSlotSyncOpts) error {
if opts.useDirectWrite(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
if !isGreeterCachePath(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
tmp, err := os.CreateTemp("", "dms-greeter-user-cache-*")
if err != nil {
return fmt.Errorf("failed to create temp file for %s: %w", path, err)
}
tmpPath := tmp.Name()
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to write temp file for %s: %w", path, err)
}
if err := tmp.Close(); err != nil {
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to close temp file for %s: %w", path, err)
}
defer os.Remove(tmpPath)
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", tmpPath, path); err != nil {
return fmt.Errorf("failed to install %s: %w", path, err)
}
return setGreeterCacheFileOwnership(path, opts.sudoPassword)
}
func resolveUserProfileImageSource(homeDir string) string {
candidates := []string{
filepath.Join(homeDir, ".face"),
filepath.Join(homeDir, ".face.icon"),
}
if homeDir != "" {
username := filepath.Base(homeDir)
if username != "" && username != "." && username != string(filepath.Separator) {
candidates = append([]string{filepath.Join("/var/lib/AccountsService/icons", username)}, candidates...)
}
}
for _, src := range candidates {
st, err := os.Stat(src)
if err == nil && !st.IsDir() && st.Size() > 0 {
return src
}
}
return ""
}
func syncUserProfileImage(homeDir, userDir string, opts userSlotSyncOpts) error {
for _, name := range []string{"profile.jpg", "profile.jpeg", "profile.png", "profile.webp"} {
path := filepath.Join(userDir, name)
if opts.useDirectWrite(path) {
_ = os.Remove(path)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
}
}
src := resolveUserProfileImageSource(homeDir)
if src == "" {
return nil
}
ext := filepath.Ext(src)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, "profile"+ext)
if err := copyFileWithPrivesc(src, dest, opts); err != nil {
return fmt.Errorf("failed to copy profile image for user cache slot: %w", err)
}
return nil
}
@@ -0,0 +1,81 @@
package greeter
import (
"path/filepath"
"testing"
)
func TestUserGreeterCacheDir(t *testing.T) {
t.Parallel()
got := userGreeterCacheDir("/var/cache/dms-greeter", "alice")
want := filepath.Join("/var/cache/dms-greeter", "users", "alice")
if got != want {
t.Fatalf("userGreeterCacheDir() = %q, want %q", got, want)
}
}
func TestResolveUserProfileImageSource(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
facePath := filepath.Join(homeDir, ".face")
writeTestFile(t, facePath, "face")
got := resolveUserProfileImageSource(homeDir)
if got != facePath {
t.Fatalf("resolveUserProfileImageSource() = %q, want %q", got, facePath)
}
}
func TestIsUserOwnedGreeterCacheSlot(t *testing.T) {
t.Parallel()
slot := filepath.Join(GreeterCacheDir, "users", "alice", "settings.json")
if !isUserOwnedGreeterCacheSlot(slot, "alice") {
t.Fatalf("expected alice to own %q", slot)
}
if isUserOwnedGreeterCacheSlot(slot, "bob") {
t.Fatalf("expected bob not to own alice slot")
}
if isUserOwnedGreeterCacheSlot(filepath.Join(GreeterCacheDir, "settings.json"), "alice") {
t.Fatalf("expected root cache file not to be a user slot")
}
}
func TestLocalizeSessionWallpapers(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
userDir := filepath.Join(homeDir, "users", "alice")
wallpaperPath := filepath.Join(homeDir, "wall.jpg")
writeTestFile(t, wallpaperPath, "wallpaper")
session := map[string]any{
"wallpaperPath": wallpaperPath,
"monitorWallpapers": map[string]any{
"DP-1": wallpaperPath,
},
}
if err := localizeSessionWallpapers(session, userDir, userSlotSyncOpts{}); err != nil {
t.Fatalf("localizeSessionWallpapers returned error: %v", err)
}
gotPath, ok := session["wallpaperPath"].(string)
if !ok || gotPath == "" {
t.Fatalf("expected localized wallpaperPath, got %#v", session["wallpaperPath"])
}
if gotPath == wallpaperPath {
t.Fatalf("expected copied wallpaper path, still points to source")
}
monitorMap, ok := session["monitorWallpapers"].(map[string]any)
if !ok {
t.Fatalf("expected monitorWallpapers map")
}
monitorPath, ok := monitorMap["DP-1"].(string)
if !ok || monitorPath == "" || monitorPath == wallpaperPath {
t.Fatalf("expected localized monitor wallpaper, got %#v", monitorMap["DP-1"])
}
}
+6 -2
View File
@@ -9,8 +9,8 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
// ErrConfirmationRequired is returned when --yes is not set and the user
@@ -223,12 +223,16 @@ func (r *Runner) Run() error {
// 9. Greeter setup (if dms-greeter was included)
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
compositorName := "niri"
if wm == deps.WindowManagerHyprland {
compositorName = "Hyprland"
}
fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
logFunc := func(line string) {
r.log(line)
fmt.Fprintf(os.Stdout, " greeter: %s\n", line)
}
if err := utils.RunDmsGreeterInstall(sudoPassword, logFunc); err != nil {
if err := greeter.AutoSetupGreeter(compositorName, sudoPassword, logFunc); err != nil {
// Non-fatal, matching TUI behavior
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
}
+5 -45
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -225,44 +224,6 @@ func (h *HyprlandProvider) validateAction(action string) error {
return nil
}
var luaExprActionPattern = regexp.MustCompile(`^(function\s*\(|hl\.)`)
// isRawLuaActionText reports that action is a Lua expression to re-emit
// verbatim rather than freeform dispatcher text to wrap for hyprctl. The
// balance check keeps malformed input from corrupting the generated file.
func isRawLuaActionText(action string) bool {
if !luaExprActionPattern.MatchString(action) {
return false
}
depth := 0
var quote byte
escaped := false
for i := 0; i < len(action); i++ {
c := action[i]
switch {
case escaped:
escaped = false
case quote != 0:
switch c {
case '\\':
escaped = true
case quote:
quote = 0
}
case c == '"' || c == '\'':
quote = c
case c == '(':
depth++
case c == ')':
depth--
if depth < 0 {
return false
}
}
}
return depth == 0 && quote == 0 && !escaped
}
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
if err := h.ensureWritableConfig(); err != nil {
return err
@@ -293,12 +254,11 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
canonicalKey := canonicalHyprlandOverrideKey(key)
normalizedKey := hyprlandOverrideMapKey(canonicalKey)
existingBinds[normalizedKey] = &hyprlandOverrideBind{
Key: canonicalKey,
Action: action,
Description: description,
Flags: flags,
Options: options,
RawLuaAction: isRawLuaActionText(action),
Key: canonicalKey,
Action: action,
Description: description,
Flags: flags,
Options: options,
}
return h.writeOverrideBinds(existingBinds)
@@ -463,56 +463,6 @@ func TestHyprlandSetBindLeavesConfOnlyInstallReadOnly(t *testing.T) {
}
}
func TestIsRawLuaActionText(t *testing.T) {
cases := []struct {
action string
want bool
}{
{`function() hl.plugin.scrolloverview.overview("toggle") end`, true},
{`hl.dsp.exec_cmd("foo")`, true},
{`hl.dsp.no_op()`, true},
{"workspace 3", false},
{"exec zeditor", false},
{`function() hl.foo( end`, false},
{`function() hl.foo("a) end`, false},
{`hl.foo()) hl.bar((`, false},
}
for _, tc := range cases {
if got := isRawLuaActionText(tc.action); got != tc.want {
t.Errorf("isRawLuaActionText(%q) = %v, want %v", tc.action, got, tc.want)
}
}
}
func TestHyprlandSetBindPreservesRawLuaAction(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte("-- DMS user keybind overrides\n"), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandProvider(tmpDir)
rawAction := `function() hl.plugin.scrolloverview.overview("toggle") end`
if err := provider.SetBind("SUPER + G", rawAction, "Toggle overview", nil); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
if err != nil {
t.Fatal(err)
}
got := string(data)
if !strings.Contains(got, `hl.bind("SUPER + G", `+rawAction) {
t.Fatalf("expected raw Lua action to be written verbatim, got:\n%s", got)
}
if strings.Contains(got, "hyprctl dispatch function") {
t.Fatalf("expected raw Lua action to not be wrapped in hyprctl dispatch, got:\n%s", got)
}
}
func TestHyprlandSetBindUpdatesSpacedLuaOverrideWithoutDuplicates(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
+207 -19
View File
@@ -1,32 +1,220 @@
package log
import (
danklog "github.com/AvengeMedia/dankgo/log"
"io"
"os"
"regexp"
"strings"
"sync"
"github.com/charmbracelet/lipgloss"
cblog "github.com/charmbracelet/log"
"github.com/mattn/go-isatty"
"github.com/muesli/termenv"
)
type Logger = danklog.Logger
// Logger embeds the Charm Logger and adds Printf/Fatalf
type Logger struct{ *cblog.Logger }
func init() {
danklog.SetEnvPrefix("DMS")
// Printf routes goose/info-style logs through Infof.
func (l *Logger) Printf(format string, v ...any) { l.Infof(format, v...) }
// Fatalf keeps gooses contract of exiting the program.
func (l *Logger) Fatalf(format string, v ...any) { l.Logger.Fatalf(format, v...) }
var (
logger *Logger
initLogger sync.Once
logMu sync.Mutex
logFile *os.File
logStderr io.Writer = os.Stderr
ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
)
// ansiStripWriter strips ANSI escape sequences before forwarding to w. Used
// for the file sink so colored stderr stays colored while the file stays plain.
type ansiStripWriter struct{ w io.Writer }
func (a *ansiStripWriter) Write(p []byte) (int, error) {
stripped := ansiRe.ReplaceAll(p, nil)
if _, err := a.w.Write(stripped); err != nil {
return 0, err
}
return len(p), nil
}
func GetLogger() *Logger { return danklog.GetLogger() }
func parseLogLevel(level string) cblog.Level {
switch strings.ToLower(level) {
case "debug":
return cblog.DebugLevel
case "info":
return cblog.InfoLevel
case "warn", "warning":
return cblog.WarnLevel
case "error":
return cblog.ErrorLevel
case "fatal":
return cblog.FatalLevel
default:
return cblog.InfoLevel
}
}
func GetQtLoggingRules() string { return danklog.GetQtLoggingRules() }
func GetQtLoggingRules() string {
level := os.Getenv("DMS_LOG_LEVEL")
if level == "" {
level = "info"
}
func SetLevel(level string) { danklog.SetLevel(level) }
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
// probes); suppressed except at debug level
var rules []string
switch strings.ToLower(level) {
case "fatal":
rules = []string{"*.debug=false", "*.info=false", "*.warning=false", "*.critical=false"}
case "error":
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
case "warn", "warning":
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"}
case "info":
rules = []string{"*.debug=false", "scene.warning=false"}
case "debug":
return ""
default:
rules = []string{"*.debug=false", "scene.warning=false"}
}
func SetLogFile(path string) error { return danklog.SetLogFile(path) }
return strings.Join(rules, ";")
}
func ApplyEnvOverrides() { danklog.ApplyEnvOverrides() }
// GetLogger returns a logger instance
func GetLogger() *Logger {
initLogger.Do(func() {
styles := cblog.DefaultStyles()
// Attempt to match the colors used by qml/quickshell logs
styles.Levels[cblog.FatalLevel] = lipgloss.NewStyle().
SetString(" FATAL").
Foreground(lipgloss.Color("1"))
styles.Levels[cblog.ErrorLevel] = lipgloss.NewStyle().
SetString(" ERROR").
Foreground(lipgloss.Color("9"))
styles.Levels[cblog.WarnLevel] = lipgloss.NewStyle().
SetString(" WARN").
Foreground(lipgloss.Color("3"))
styles.Levels[cblog.InfoLevel] = lipgloss.NewStyle().
SetString(" INFO").
Foreground(lipgloss.Color("2"))
styles.Levels[cblog.DebugLevel] = lipgloss.NewStyle().
SetString(" DEBUG").
Foreground(lipgloss.Color("4"))
func Debug(msg any, keyvals ...any) { danklog.Debug(msg, keyvals...) }
func Debugf(format string, v ...any) { danklog.Debugf(format, v...) }
func Info(msg any, keyvals ...any) { danklog.Info(msg, keyvals...) }
func Infof(format string, v ...any) { danklog.Infof(format, v...) }
func Warn(msg any, keyvals ...any) { danklog.Warn(msg, keyvals...) }
func Warnf(format string, v ...any) { danklog.Warnf(format, v...) }
func Error(msg any, keyvals ...any) { danklog.Error(msg, keyvals...) }
func Errorf(format string, v ...any) { danklog.Errorf(format, v...) }
func Fatal(msg any, keyvals ...any) { danklog.Fatal(msg, keyvals...) }
func Fatalf(format string, v ...any) { danklog.Fatalf(format, v...) }
base := cblog.New(logStderr)
base.SetStyles(styles)
base.SetReportTimestamp(false)
level := cblog.InfoLevel
if envLevel := os.Getenv("DMS_LOG_LEVEL"); envLevel != "" {
level = parseLogLevel(envLevel)
}
base.SetLevel(level)
base.SetPrefix(" go")
logger = &Logger{base}
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
logMu.Lock()
_ = setLogFile(logger, path)
logMu.Unlock()
}
})
return logger
}
// SetLevel updates the active log level. Accepts the same strings as
// DMS_LOG_LEVEL. Unknown values default to info.
func SetLevel(level string) {
GetLogger().SetLevel(parseLogLevel(level))
}
// SetLogFile makes the logger append to path in addition to stderr. Passing an
// empty string detaches the file sink. Atomic per-line writes (≤PIPE_BUF) on
// O_APPEND keep concurrent Go and QML writers from corrupting each other.
//
// Color handling: charmbracelet/log auto-detects color support from its
// io.Writer, and io.MultiWriter doesn't pass that through, so we force the ANSI
// profile when stderr is a TTY and route the file through ansiStripWriter so
// the file stays plain while stderr keeps its colors.
func SetLogFile(path string) error {
l := GetLogger()
logMu.Lock()
defer logMu.Unlock()
return setLogFile(l, path)
}
func setLogFile(l *Logger, path string) error {
if logFile != nil {
logFile.Close()
logFile = nil
}
if path == "" {
l.SetOutput(logStderr)
applyColorProfile(l, logStderr)
return nil
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
return err
}
logFile = f
out := io.MultiWriter(logStderr, &ansiStripWriter{w: f})
l.SetOutput(out)
applyColorProfile(l, logStderr)
return nil
}
// applyColorProfile forces the renderer's color profile to match what stderr
// would produce on its own, undoing the auto-downgrade triggered by wrapping
// stderr in a non-TTY writer (e.g. io.MultiWriter).
func applyColorProfile(l *Logger, stderr io.Writer) {
f, ok := stderr.(*os.File)
if !ok {
l.SetColorProfile(termenv.Ascii)
return
}
if isatty.IsTerminal(f.Fd()) {
l.SetColorProfile(termenv.ANSI)
return
}
l.SetColorProfile(termenv.Ascii)
}
// ApplyEnvOverrides re-reads DMS_LOG_LEVEL and DMS_LOG_FILE and reconfigures
// the singleton. Safe to call after CLI flags have rewritten the environment.
func ApplyEnvOverrides() {
GetLogger()
if level := os.Getenv("DMS_LOG_LEVEL"); level != "" {
SetLevel(level)
}
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
if err := SetLogFile(path); err != nil {
Warnf("Failed to open log file %q: %v", path, err)
}
}
}
// * Convenience wrappers
func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) }
func Debugf(format string, v ...any) { GetLogger().Debugf(format, v...) }
func Info(msg any, keyvals ...any) { GetLogger().Info(msg, keyvals...) }
func Infof(format string, v ...any) { GetLogger().Infof(format, v...) }
func Warn(msg any, keyvals ...any) { GetLogger().Warn(msg, keyvals...) }
func Warnf(format string, v ...any) { GetLogger().Warnf(format, v...) }
func Error(msg any, keyvals ...any) { GetLogger().Error(msg, keyvals...) }
func Errorf(format string, v ...any) { GetLogger().Errorf(format, v...) }
func Fatal(msg any, keyvals ...any) { GetLogger().Fatal(msg, keyvals...) }
func Fatalf(format string, v ...any) { GetLogger().Fatalf(format, v...) }
+46 -98
View File
@@ -386,12 +386,8 @@ func buildOnce(opts *Options) (bool, error) {
default:
syncAccentColor(primaryDark)
}
refreshGTKTheme(opts.Mode)
refreshGTKColorScheme()
}
if isDMSKDEColorSchemeActive(opts.ConfigDir) {
applyKDEColorScheme(opts.Mode)
refreshGTK(opts.Mode)
refreshGTK4()
}
if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) {
@@ -471,9 +467,12 @@ output_path = '%s'
case TemplateKindTerminal:
appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
case TemplateKindVSCode:
for _, editor := range vscodeEditors {
appendVSCodeConfig(cfgFile, editor.name, editor.extensionsDir(homeDir), opts.ShellDir)
}
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
@@ -630,23 +629,6 @@ func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []stri
return false
}
type vscodeEditor struct {
name string
dataDir string
}
var vscodeEditors = []vscodeEditor{
{"vscode", ".vscode"},
{"codium", ".vscode-oss"},
{"cursor", ".cursor"},
{"windsurf", ".windsurf"},
{"vscode-insiders", ".vscode-insiders"},
}
func (e vscodeEditor) extensionsDir(homeDir string) string {
return filepath.Join(homeDir, e.dataDir, "extensions")
}
func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) {
pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*")
matches, err := filepath.Glob(pattern)
@@ -925,7 +907,7 @@ func generateDank16Variants(primaryDark, primaryLight, surface string, mode Colo
}
func isDMSGTKActive(configDir string) bool {
gtkCSS := filepath.Join(configDir, "gtk-4.0", "gtk.css")
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
info, err := os.Lstat(gtkCSS)
if err != nil {
@@ -941,73 +923,11 @@ func isDMSGTKActive(configDir string) bool {
return err == nil && strings.Contains(string(data), "dank-colors.css")
}
// isDMSKDEColorSchemeActive only flips the scheme when the user is already on a
// DankMatugen one, leaving Breeze (or anything else) untouched.
func isDMSKDEColorSchemeActive(configDir string) bool {
data, err := os.ReadFile(filepath.Join(configDir, "kdeglobals"))
if err != nil {
return false
}
inGeneral := false
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "[") {
inGeneral = line == "[General]"
continue
}
if !inGeneral {
continue
}
if name, ok := strings.CutPrefix(line, "ColorScheme="); ok {
return strings.HasPrefix(strings.TrimSpace(name), "DankMatugen")
}
}
return false
}
func applyKDEColorScheme(mode ColorMode) {
if !utils.CommandExists("plasma-apply-colorscheme") {
return
}
scheme := "DankMatugenDark"
if mode == ColorModeLight {
scheme = "DankMatugenLight"
}
log.Infof("Applying KDE color scheme: %s", scheme)
if err := exec.Command("plasma-apply-colorscheme", scheme).Run(); err != nil {
log.Warnf("Failed to apply KDE color scheme: %v", err)
}
}
func gtkThemeInstalled(theme string) bool {
home, _ := os.UserHomeDir()
candidates := []string{
filepath.Join(home, ".local/share/themes", theme),
filepath.Join(home, ".themes", theme),
filepath.Join("/usr/share/themes", theme),
filepath.Join("/usr/local/share/themes", theme),
}
for _, dir := range candidates {
if info, err := os.Stat(dir); err == nil && info.IsDir() {
return true
}
}
return false
}
func refreshGTKTheme(mode ColorMode) {
theme := mode.GTKTheme()
if !gtkThemeInstalled(theme) {
log.Infof("Skipping gtk-theme refresh: %s is not installed", theme)
return
}
func refreshGTK(mode ColorMode) {
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
log.Warnf("Failed to reset gtk-theme: %v", err)
}
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", theme); err != nil {
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil {
log.Warnf("Failed to set gtk-theme: %v", err)
}
}
@@ -1028,8 +948,8 @@ func expectColorSchemeEcho(scheme string) {
// apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps
// following the portal color-scheme (Chromium) can drop the restore signal
// mid-repaint and latch the wrong mode, so this is opt-in.
func refreshGTKColorScheme() {
if os.Getenv("DMS_ENABLE_GTK_REFRESH") != "1" {
func refreshGTK4() {
if os.Getenv("DMS_ENABLE_GTK4_REFRESH") != "1" {
return
}
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
@@ -1047,13 +967,13 @@ func refreshGTKColorScheme() {
expectColorSchemeEcho(toggle)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK refresh: %v", err)
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
return
}
time.Sleep(50 * time.Millisecond)
time.Sleep(400 * time.Millisecond)
expectColorSchemeEcho(current)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK refresh: %v", err)
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
}
}
@@ -1076,6 +996,26 @@ func signalTerminals(opts *Options) {
}
}
func signalByName(name string, sig syscall.Signal) {
entries, err := os.ReadDir("/proc")
if err != nil {
return
}
for _, entry := range entries {
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
if err != nil {
continue
}
if strings.TrimSpace(string(comm)) == name {
syscall.Kill(pid, sig)
}
}
}
func syncColorScheme(mode ColorMode) {
scheme := "prefer-dark"
if mode == ColorModeLight {
@@ -1182,8 +1122,16 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
}
func checkVSCodeExtension(homeDir string) bool {
for _, editor := range vscodeEditors {
pattern := filepath.Join(editor.extensionsDir(homeDir), "danklinux.dms-theme-*")
extDirs := []string{
filepath.Join(homeDir, ".vscode/extensions"),
filepath.Join(homeDir, ".vscode-oss/extensions"),
filepath.Join(homeDir, ".config/Code - OSS/extensions"),
filepath.Join(homeDir, ".cursor/extensions"),
filepath.Join(homeDir, ".windsurf/extensions"),
}
for _, extDir := range extDirs {
pattern := filepath.Join(extDir, "danklinux.dms-theme-*")
if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 {
return true
}
-16
View File
@@ -1,16 +0,0 @@
package matugen
import (
"os/exec"
"strings"
"syscall"
"golang.org/x/sys/unix"
)
// procfs(5) is optional on FreeBSD; pkill(1) from base queries the kernel
// directly.
func signalByName(name string, sig syscall.Signal) {
signame := strings.TrimPrefix(unix.SignalName(sig), "SIG")
exec.Command("pkill", "-"+signame, "-x", name).Run()
}
-29
View File
@@ -1,29 +0,0 @@
package matugen
import (
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
)
func signalByName(name string, sig syscall.Signal) {
entries, err := os.ReadDir("/proc")
if err != nil {
return
}
for _, entry := range entries {
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
if err != nil {
continue
}
if strings.TrimSpace(string(comm)) == name {
syscall.Kill(pid, sig)
}
}
}
+393 -21
View File
@@ -5,20 +5,30 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
const (
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
legacyGreeterPamU2FComment = "# DMS greeter U2F"
GreetdPamPath = "/etc/pam.d/greetd"
DankshellPamPath = "/etc/pam.d/dankshell"
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
)
@@ -28,8 +38,7 @@ const (
// rest cover distros (or minimal installs) with no /etc/pam.d/login.
// lockscreenPamBaseDirs mirrors libpam's search order: /etc overrides, then the
// vendor dir (/usr/lib) and the stateless-distro default (/usr/share).
// /usr/local/etc/pam.d is OpenPAM's ports dir on FreeBSD (openpam_configure).
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d", "/usr/local/etc/pam.d"}
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d"}
// Standalone auth+account services, most universal first. login exists almost
// everywhere (util-linux); system-* cover Fedora/Arch/Gentoo/SUSE-Leap.
@@ -41,15 +50,13 @@ var lockscreenPamEntryCandidates = []string{
}
// Fallback for distros with no standalone login service, only shared building
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*), FreeBSD
// (system holds both stanzas, included by login).
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*).
var lockscreenPamSharedIncludePairs = []struct {
auth string
account string
}{
{auth: "common-auth", account: "common-account"},
{auth: "base-auth", account: "base-account"},
{auth: "system", account: "system"},
}
var includedPamAuthFiles = []string{
@@ -60,28 +67,34 @@ var includedPamAuthFiles = []string{
"system-local-login",
"common-auth-pc",
"login",
"system",
}
type AuthSettings struct {
EnableFprint bool `json:"enableFprint"`
EnableU2f bool `json:"enableU2f"`
EnableFprint bool `json:"enableFprint"`
EnableU2f bool `json:"enableU2f"`
GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"`
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
}
type SyncAuthOptions struct {
HomeDir string
HomeDir string
ForceGreeterAuth bool
}
type syncDeps struct {
pamDir string
dankshellPath string
dankshellU2fPath string
isNixOS func() bool
readFile func(string) ([]byte, error)
stat func(string) (os.FileInfo, error)
createTemp func(string, string) (*os.File, error)
removeFile func(string) error
runSudoCmd func(string, string, ...string) error
pamDir string
greetdPath string
dankshellPath string
dankshellU2fPath string
isNixOS func() bool
readFile func(string) ([]byte, error)
stat func(string) (os.FileInfo, error)
createTemp func(string, string) (*os.File, error)
removeFile func(string) error
runSudoCmd func(string, string, ...string) error
pamModuleExists func(string) bool
fingerprintAvailableForCurrentUser func() bool
}
type lockscreenPamIncludeDirective struct {
@@ -137,6 +150,7 @@ func (r lockscreenPamResolver) locate(target string) (string, error) {
func defaultSyncDeps() syncDeps {
return syncDeps{
pamDir: "/etc/pam.d",
greetdPath: GreetdPamPath,
dankshellPath: DankshellPamPath,
dankshellU2fPath: DankshellU2FPamPath,
isNixOS: IsNixOS,
@@ -147,6 +161,8 @@ func defaultSyncDeps() syncDeps {
runSudoCmd: func(password, command string, args ...string) error {
return privesc.Run(context.Background(), password, append([]string{command}, args...)...)
},
pamModuleExists: pamModuleExists,
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
}
}
@@ -175,10 +191,22 @@ func ReadAuthSettings(homeDir string) (AuthSettings, error) {
return settings, nil
}
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
settings, err := ReadAuthSettings(homeDir)
if err != nil {
return false, false, err
}
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
}
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
}
func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
}
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
homeDir := strings.TrimSpace(options.HomeDir)
if homeDir == "" {
@@ -201,9 +229,99 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S
return err
}
if _, err := deps.stat(deps.greetdPath); err != nil {
if os.IsNotExist(err) {
logFunc(" /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
return nil
}
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
}
if settings.GreeterPamExternallyManaged {
if err := removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, deps); err != nil {
return err
}
logFunc(" /etc/pam.d/greetd is externally managed. Skipping DMS greeter PAM sync.")
return nil
}
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
return err
}
return nil
}
func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
if deps.isNixOS() {
return nil
}
data, err := deps.readFile(deps.greetdPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
}
originalContent := string(data)
stripped, removed := stripManagedGreeterPamBlock(originalContent)
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
if !removed && !removedLegacy {
return nil
}
if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
}
logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
return nil
}
func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
if pamText == "" {
return false, false, false, false
}
lines := strings.Split(pamText, "\n")
inManaged := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
switch trimmed {
case GreeterPamManagedBlockStart:
managed = true
inManaged = true
continue
case GreeterPamManagedBlockEnd:
inManaged = false
continue
}
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
legacy = true
}
if !inManaged {
continue
}
if strings.Contains(trimmed, "pam_fprintd") {
fingerprint = true
}
if strings.Contains(trimmed, "pam_u2f") {
u2f = true
}
}
return managed, fingerprint, u2f, legacy
}
func StripManagedGreeterPamContent(pamText string) (string, bool) {
stripped, removed := stripManagedGreeterPamBlock(pamText)
stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
return stripped, removed || removedLegacy
}
func PamTextIncludesFile(pamText, filename string) bool {
lines := strings.Split(pamText, "\n")
for _, line := range lines {
@@ -912,6 +1030,186 @@ func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword strin
return nil
}
func stripManagedGreeterPamBlock(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
inManagedBlock := false
removed := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == GreeterPamManagedBlockStart {
inManagedBlock = true
removed = true
continue
}
if trimmed == GreeterPamManagedBlockEnd {
inManagedBlock = false
removed = true
continue
}
if inManagedBlock {
removed = true
continue
}
filtered = append(filtered, line)
}
return strings.Join(filtered, "\n"), removed
}
func stripLegacyGreeterPamLines(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
removed := false
for i := 0; i < len(lines); i++ {
trimmed := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
removed = true
if i+1 < len(lines) {
nextLine := strings.TrimSpace(lines[i+1])
if strings.HasPrefix(nextLine, "auth") &&
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
i++
}
}
continue
}
filtered = append(filtered, lines[i])
}
return strings.Join(filtered, "\n"), removed
}
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
lines := strings.Split(content, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
block := strings.Join(blockLines, "\n")
prefix := strings.Join(lines[:i], "\n")
suffix := strings.Join(lines[i:], "\n")
switch {
case prefix == "":
return block + "\n" + suffix, nil
case suffix == "":
return prefix + "\n" + block, nil
default:
return prefix + "\n" + block + "\n" + suffix, nil
}
}
}
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
}
func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
var wantFprint, wantU2f bool
fprintToggleEnabled := forceAuth
u2fToggleEnabled := forceAuth
if forceAuth {
wantFprint = deps.pamModuleExists("pam_fprintd.so")
wantU2f = deps.pamModuleExists("pam_u2f.so")
} else {
fprintToggleEnabled = settings.GreeterEnableFprint
u2fToggleEnabled = settings.GreeterEnableU2f
fprintModule := deps.pamModuleExists("pam_fprintd.so")
u2fModule := deps.pamModuleExists("pam_u2f.so")
wantFprint = settings.GreeterEnableFprint && fprintModule
wantU2f = settings.GreeterEnableU2f && u2fModule
if settings.GreeterEnableFprint && !fprintModule {
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
}
if settings.GreeterEnableU2f && !u2fModule {
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
}
}
if deps.isNixOS() {
logFunc(" NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
return nil
}
pamData, err := deps.readFile(deps.greetdPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
}
originalContent := string(pamData)
content, _ := stripManagedGreeterPamBlock(originalContent)
content, _ = stripLegacyGreeterPamLines(content)
includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
if wantFprint && includedFprintFile != "" {
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
wantFprint = false
}
if wantU2f && includedU2fFile != "" {
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
wantU2f = false
}
if !wantFprint && includedFprintFile != "" {
if fprintToggleEnabled {
logFunc(" Fingerprint auth is still enabled via included " + includedFprintFile + ".")
if fprintAvailableForCurrentUser {
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
} else {
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
}
} else {
if fprintAvailableForCurrentUser {
logFunc(" Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
} else {
logFunc(" pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
logFunc(" Password auth remains the effective login path.")
}
}
}
if !wantU2f && includedU2fFile != "" {
if u2fToggleEnabled {
logFunc(" Security-key auth is still enabled via included " + includedU2fFile + ".")
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
} else {
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
logFunc(" " + pamManagerHintForCurrentDistro())
}
}
if wantFprint || wantU2f {
blockLines := []string{GreeterPamManagedBlockStart}
if wantFprint {
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=2 timeout=10")
}
if wantU2f {
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
}
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
if err != nil {
return err
}
}
if content == originalContent {
return nil
}
if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
}
if wantFprint || wantU2f {
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
} else {
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
}
return nil
}
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
if err != nil {
@@ -938,6 +1236,26 @@ func writeManagedPamFile(content string, destPath string, sudoPassword string, d
return nil
}
func pamManagerHintForCurrentDistro() string {
osInfo, err := distros.GetOSInfo()
if err != nil {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
switch config.Family {
case distros.FamilyFedora:
return "Disable it in authselect to force password-only greeter login."
case distros.FamilyDebian, distros.FamilyUbuntu:
return "Disable it in pam-auth-update to force password-only greeter login."
default:
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
}
func pamModuleExists(module string) bool {
for _, libDir := range []string{
"/usr/lib64/security",
@@ -950,9 +1268,6 @@ func pamModuleExists(module string) bool {
"/usr/lib/aarch64-linux-gnu/security",
"/run/current-system/sw/lib64/security",
"/run/current-system/sw/lib/security",
"/usr/local/lib/security",
"/usr/local/lib",
"/usr/lib",
} {
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
return true
@@ -960,3 +1275,60 @@ func pamModuleExists(module string) bool {
}
return false
}
func hasEnrolledFingerprintOutput(output string) bool {
lower := strings.ToLower(output)
if strings.Contains(lower, "no fingers enrolled") ||
strings.Contains(lower, "no fingerprints enrolled") ||
strings.Contains(lower, "no prints enrolled") {
return false
}
if strings.Contains(lower, "has fingers enrolled") ||
strings.Contains(lower, "has fingerprints enrolled") {
return true
}
for _, line := range strings.Split(lower, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "finger:") {
return true
}
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
return true
}
}
return false
}
func FingerprintAuthAvailableForCurrentUser() bool {
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
if username == "" {
username = strings.TrimSpace(os.Getenv("USER"))
}
if username == "" {
out, err := exec.Command("id", "-un").Output()
if err == nil {
username = strings.TrimSpace(string(out))
}
}
return fingerprintAuthAvailableForUser(username)
}
func fingerprintAuthAvailableForUser(username string) bool {
username = strings.TrimSpace(username)
if username == "" {
return false
}
if !pamModuleExists("pam_fprintd.so") {
return false
}
if _, err := exec.LookPath("fprintd-list"); err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
if err != nil {
return false
}
return hasEnrolledFingerprintOutput(string(out))
}
+271 -94
View File
@@ -18,6 +18,42 @@ func writeTestFile(t *testing.T, path string, content string) {
}
}
type pamTestEnv struct {
pamDir string
greetdPath string
dankshellPath string
dankshellU2fPath string
tmpDir string
homeDir string
availableModules map[string]bool
fingerprintAvailable bool
}
func newPamTestEnv(t *testing.T) *pamTestEnv {
t.Helper()
root := t.TempDir()
pamDir := filepath.Join(root, "pam.d")
tmpDir := filepath.Join(root, "tmp")
homeDir := filepath.Join(root, "home")
for _, dir := range []string{pamDir, tmpDir, homeDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("failed to create %s: %v", dir, err)
}
}
return &pamTestEnv{
pamDir: pamDir,
greetdPath: filepath.Join(pamDir, "greetd"),
dankshellPath: filepath.Join(pamDir, "dankshell"),
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
tmpDir: tmpDir,
homeDir: homeDir,
availableModules: map[string]bool{},
}
}
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
t.Helper()
writeTestFile(t, filepath.Join(e.pamDir, name), content)
@@ -28,6 +64,59 @@ func (e *pamTestEnv) writeSettings(t *testing.T, content string) {
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
}
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
return syncDeps{
pamDir: e.pamDir,
greetdPath: e.greetdPath,
dankshellPath: e.dankshellPath,
dankshellU2fPath: e.dankshellU2fPath,
isNixOS: func() bool { return isNixOS },
readFile: os.ReadFile,
stat: os.Stat,
createTemp: func(_ string, pattern string) (*os.File, error) {
return os.CreateTemp(e.tmpDir, pattern)
},
removeFile: os.Remove,
runSudoCmd: func(_ string, command string, args ...string) error {
switch command {
case "cp":
if len(args) != 2 {
return fmt.Errorf("unexpected cp args: %v", args)
}
data, err := os.ReadFile(args[0])
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
return err
}
return os.WriteFile(args[1], data, 0o644)
case "chmod":
if len(args) != 2 {
return fmt.Errorf("unexpected chmod args: %v", args)
}
return nil
case "rm":
if len(args) != 2 || args[0] != "-f" {
return fmt.Errorf("unexpected rm args: %v", args)
}
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
return err
}
return nil
default:
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
}
},
pamModuleExists: func(module string) bool {
return e.availableModules[module]
},
fingerprintAvailableForCurrentUser: func() bool {
return e.fingerprintAvailable
},
}
}
func readFileString(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
@@ -614,6 +703,98 @@ func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) {
})
}
func TestSyncGreeterPamConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("adds managed block for enabled auth modules", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true}
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
for _, want := range []string{
GreeterPamManagedBlockStart,
"auth sufficient pam_fprintd.so max-tries=2 timeout=10",
"auth sufficient pam_u2f.so cue nouserok timeout=10",
GreeterPamManagedBlockEnd,
} {
if !strings.Contains(got, want) {
t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got)
}
}
if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") {
t.Fatalf("managed block was not inserted before first auth line:\n%s", got)
}
})
t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.fingerprintAvailable = true
original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
env.writePamFile(t, "greetd", original)
env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n")
settings := AuthSettings{GreeterEnableFprint: true}
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
if got != original {
t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original)
}
if strings.Contains(got, GreeterPamManagedBlockStart) {
t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got)
}
})
}
func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "greetd", "#%PAM-1.0\n"+
legacyGreeterPamFprintComment+"\n"+
"auth sufficient pam_fprintd.so max-tries=1\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_u2f.so cue nouserok timeout=10\n"+
GreeterPamManagedBlockEnd+"\n"+
"auth include system-auth\n")
if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil {
t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) {
t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got)
}
if !strings.Contains(got, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got)
}
}
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: []string{e.pamDir},
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: func(module string) bool { return e.availableModules[module] },
}
}
func TestListLockscreenPamServices(t *testing.T) {
t.Parallel()
@@ -900,98 +1081,10 @@ func containsSubstr(items []string, substr string) bool {
return false
}
type pamTestEnv struct {
pamDir string
dankshellPath string
dankshellU2fPath string
tmpDir string
homeDir string
availableModules map[string]bool
}
func newPamTestEnv(t *testing.T) *pamTestEnv {
t.Helper()
root := t.TempDir()
pamDir := filepath.Join(root, "pam.d")
tmpDir := filepath.Join(root, "tmp")
homeDir := filepath.Join(root, "home")
for _, dir := range []string{pamDir, tmpDir, homeDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("failed to create %s: %v", dir, err)
}
}
return &pamTestEnv{
pamDir: pamDir,
dankshellPath: filepath.Join(pamDir, "dankshell"),
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
tmpDir: tmpDir,
homeDir: homeDir,
availableModules: map[string]bool{},
}
}
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
return syncDeps{
pamDir: e.pamDir,
dankshellPath: e.dankshellPath,
dankshellU2fPath: e.dankshellU2fPath,
isNixOS: func() bool { return isNixOS },
readFile: os.ReadFile,
stat: os.Stat,
createTemp: func(_ string, pattern string) (*os.File, error) {
return os.CreateTemp(e.tmpDir, pattern)
},
removeFile: os.Remove,
runSudoCmd: func(_ string, command string, args ...string) error {
switch command {
case "cp":
if len(args) != 2 {
return fmt.Errorf("unexpected cp args: %v", args)
}
data, err := os.ReadFile(args[0])
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
return err
}
return os.WriteFile(args[1], data, 0o644)
case "chmod":
if len(args) != 2 {
return fmt.Errorf("unexpected chmod args: %v", args)
}
return nil
case "rm":
if len(args) != 2 || args[0] != "-f" {
return fmt.Errorf("unexpected rm args: %v", args)
}
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
return err
}
return nil
default:
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
}
},
}
}
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: []string{e.pamDir},
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: func(module string) bool { return e.availableModules[module] },
}
}
func TestSyncAuthConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("creates lockscreen targets", func(t *testing.T) {
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
@@ -999,7 +1092,10 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
@@ -1010,24 +1106,105 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
}
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") {
t.Fatalf("expected greetd skip log, got %v", logs)
}
})
t.Run("removes dankshell-u2f when disabled", func(t *testing.T) {
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writeSettings(t, `{"enableU2f":false}`)
env.availableModules["pam_fprintd.so"] = true
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
dankshell := readFileString(t, env.dankshellPath)
if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") {
t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell-u2f to be removed, stat err = %v", err)
t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err)
}
greetd := readFileString(t, env.greetdPath)
if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=2 timeout=10") {
t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd)
}
if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") {
t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd)
}
})
t.Run("externally managed greetd is stripped and greeter sync skipped", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.writeSettings(t, `{"greeterPamExternallyManaged":true,"greeterEnableFprint":true}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_fprintd.so max-tries=2 timeout=10\n"+
GreeterPamManagedBlockEnd+"\n")
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
greetd := readFileString(t, env.greetdPath)
if strings.Contains(greetd, GreeterPamManagedBlockStart) || strings.Contains(greetd, "pam_fprintd") {
t.Fatalf("expected DMS-managed block stripped from externally managed greetd:\n%s", greetd)
}
if !strings.Contains(greetd, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd lines to remain:\n%s", greetd)
}
if !containsSubstr(logs, "externally managed") {
t.Fatalf("expected externally-managed skip log, got %v", logs)
}
})
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`)
originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
env.writePamFile(t, "greetd", originalGreetd)
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err)
}
if got := readFileString(t, env.greetdPath); got != originalGreetd {
t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd)
}
if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") {
t.Fatalf("expected informational NixOS logs, got %v", logs)
}
})
}
@@ -34,7 +34,7 @@ import (
"unsafe"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) {
@@ -0,0 +1,25 @@
package qmlchecks
import (
"os"
"strings"
"testing"
)
func TestGreeterExternalAuthStatusUsesEffectiveFingerprintAvailability(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Greetd/GreeterContent.qml")
if err != nil {
t.Fatalf("read greeter QML: %v", err)
}
content := string(data)
for _, required := range []string{
"readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f",
"if (greeterPamHasFprint && greeterPamHasU2f)",
"if (greeterPamHasFprint)",
} {
if !strings.Contains(content, required) {
t.Fatalf("greeter external-auth status must contain %q", required)
}
}
}
@@ -0,0 +1,22 @@
package qmlchecks
import (
"os"
"strings"
"testing"
)
func TestGreeterRememberLastSessionFallsBackToDesktopID(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Greetd/GreeterContent.qml")
if err != nil {
t.Fatalf("read greeter QML: %v", err)
}
content := string(data)
if !strings.Contains(content, "GreetdMemory.lastSessionDesktopId || desktopIdFromPath(GreetdMemory.lastSessionId)") {
t.Fatalf("remembered greeter sessions should derive a desktop id from legacy absolute session paths")
}
if !strings.Contains(content, "GreeterState.sessionDesktopIds[i] === savedDesktopId") {
t.Fatalf("remembered greeter sessions should match current sessions by desktop id")
}
}
@@ -22,15 +22,31 @@ func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") {
t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control")
}
}
// Wayland IMEs commit unconsumed printable keys as text-input text rather
// than forwarding raw keys, so the lock screen needs an IME commit sink
// alongside raw key handling.
if !strings.Contains(content, "id: imeCommitSink") {
t.Fatalf("passwordField must keep the imeCommitSink TextInput so IME-routed keyboards can type (#2950)")
func TestLockScreenAuthenticationCardOwnsFactorControls(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Settings/LockScreenTab.qml")
if err != nil {
t.Fatalf("read lock screen settings QML: %v", err)
}
if !strings.Contains(content, "Qt.ImhSensitiveData") {
t.Fatalf("imeCommitSink must advertise hidden-text hints so IMEs treat it as a password field")
content := string(data)
authCard := strings.Index(content, `title: I18n.tr("Authentication")`)
behaviorCard := strings.Index(content, `title: I18n.tr("Behavior")`)
fingerprintToggle := strings.Index(content, `settingKey: "enableFprint"`)
u2fToggle := strings.Index(content, `settingKey: "enableU2f"`)
u2fSource := strings.Index(content, `settingKey: "lockU2fPamPath"`)
if authCard < 0 || behaviorCard < 0 || fingerprintToggle < 0 || u2fToggle < 0 || u2fSource < 0 {
t.Fatalf("expected authentication card, factor toggles, and U2F source setting")
}
for name, position := range map[string]int{
"fingerprint toggle": fingerprintToggle,
"U2F toggle": u2fToggle,
"U2F source": u2fSource,
} {
if position < authCard || position > behaviorCard {
t.Fatalf("%s must remain in the authentication card", name)
}
}
}
+4 -2
View File
@@ -1,12 +1,14 @@
package apppicker
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/desktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "apppicker.open", "browser.open":
handleOpen(conn, req, manager)
@@ -15,7 +17,7 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
}
}
func handleOpen(conn *models.Conn, req models.Request, manager *Manager) {
func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
target, ok := models.Get[string](req, "target")
+1 -1
View File
@@ -3,7 +3,7 @@ package apppicker
import (
"sync"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type Manager struct {
+19 -17
View File
@@ -1,10 +1,12 @@
package bluez
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/dankgo/ipc/params"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
type BluetoothEvent struct {
@@ -12,7 +14,7 @@ type BluetoothEvent struct {
Data BluetoothState `json:"data"`
}
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "bluetooth.getState":
handleGetState(conn, req, manager)
@@ -45,11 +47,11 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleStartDiscovery(conn *models.Conn, req models.Request, manager *Manager) {
func handleStartDiscovery(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.StartDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -57,7 +59,7 @@ func handleStartDiscovery(conn *models.Conn, req models.Request, manager *Manage
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery started"})
}
func handleStopDiscovery(conn *models.Conn, req models.Request, manager *Manager) {
func handleStopDiscovery(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.StopDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -65,7 +67,7 @@ func handleStopDiscovery(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery stopped"})
}
func handleSetPowered(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) {
powered, err := params.Bool(req.Params, "powered")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -80,7 +82,7 @@ func handleSetPowered(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "powered state updated"})
}
func handlePairDevice(conn *models.Conn, req models.Request, manager *Manager) {
func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -95,7 +97,7 @@ func handlePairDevice(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing initiated"})
}
func handleConnectDevice(conn *models.Conn, req models.Request, manager *Manager) {
func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -110,7 +112,7 @@ func handleConnectDevice(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
}
func handleDisconnectDevice(conn *models.Conn, req models.Request, manager *Manager) {
func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -125,7 +127,7 @@ func handleDisconnectDevice(conn *models.Conn, req models.Request, manager *Mana
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
}
func handleRemoveDevice(conn *models.Conn, req models.Request, manager *Manager) {
func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -140,7 +142,7 @@ func handleRemoveDevice(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device removed"})
}
func handleTrustDevice(conn *models.Conn, req models.Request, manager *Manager) {
func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -155,7 +157,7 @@ func handleTrustDevice(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device trusted"})
}
func handleUntrustDevice(conn *models.Conn, req models.Request, manager *Manager) {
func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -170,7 +172,7 @@ func handleUntrustDevice(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device untrusted"})
}
func handlePairingSubmit(conn *models.Conn, req models.Request, manager *Manager) {
func handlePairingSubmit(conn net.Conn, req models.Request, manager *Manager) {
token, err := params.String(req.Params, "token")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -188,7 +190,7 @@ func handlePairingSubmit(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing response submitted"})
}
func handlePairingCancel(conn *models.Conn, req models.Request, manager *Manager) {
func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) {
token, err := params.String(req.Params, "token")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -203,7 +205,7 @@ func handlePairingCancel(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing cancelled"})
}
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
@@ -214,7 +216,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Data: initialState,
}
if err := conn.WriteResponse(models.Response[BluetoothEvent]{
if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
@@ -226,7 +228,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Type: "state_changed",
Data: state,
}
if err := conn.WriteResponse(models.Response[BluetoothEvent]{
if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
Result: &event,
}); err != nil {
return
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/dbusutil"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/godbus/dbus/v5"
)
@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type SubscriptionBroker struct {
+1 -1
View File
@@ -3,7 +3,7 @@ package bluez
import (
"sync"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
)
@@ -1,167 +0,0 @@
package brightness
import (
"fmt"
"math"
"os"
"path/filepath"
"strings"
"unsafe"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/syncmap"
"golang.org/x/sys/unix"
)
// sys/sys/backlight.h: brightness is a 0-100 percent;
// BACKLIGHTGETSTATUS/BACKLIGHTUPDATESTATUS = _IOWR('G', 0/1, struct
// backlight_props{uint32 brightness; uint32 nlevels; uint32 levels[100]}).
const (
backlightDevDir = "/dev/backlight"
backlightGetStatus = 0xc1984700
backlightUpdateStatus = 0xc1984701
)
type backlightProps struct {
brightness uint32
nlevels uint32
levels [100]uint32
}
type BacklightBackend struct {
devices syncmap.Map[string, string]
}
func NewBacklightBackend() (*BacklightBackend, error) {
b := &BacklightBackend{}
if err := b.scanDevices(); err != nil {
return nil, err
}
return b, nil
}
func isGenericBacklightName(name string) bool {
rest, ok := strings.CutPrefix(name, "backlight")
if !ok || rest == "" {
return false
}
for _, r := range rest {
if r < '0' || r > '9' {
return false
}
}
return true
}
func (b *BacklightBackend) scanDevices() error {
entries, err := os.ReadDir(backlightDevDir)
if err != nil {
return fmt.Errorf("read %s: %w", backlightDevDir, err)
}
// backlight_register (sys/dev/backlight/backlight.c) publishes each unit
// as backlight/backlightN plus a driver-named alias for the same cdev;
// dedupe on the device number and keep the descriptive alias.
names := make(map[uint64]string)
for _, entry := range entries {
var st unix.Stat_t
if err := unix.Stat(filepath.Join(backlightDevDir, entry.Name()), &st); err != nil {
continue
}
rdev := uint64(st.Rdev)
current, exists := names[rdev]
if exists && !isGenericBacklightName(current) {
continue
}
names[rdev] = entry.Name()
}
for _, name := range names {
b.devices.Store("backlight:"+name, filepath.Join(backlightDevDir, name))
}
return nil
}
func backlightIoctl(fd int, req uint, props *backlightProps) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(unsafe.Pointer(props)))
if errno != 0 {
return errno
}
return nil
}
func readBrightness(path string) (int, error) {
fd, err := unix.Open(path, unix.O_RDWR, 0)
if err != nil {
return 0, err
}
defer unix.Close(fd)
var props backlightProps
if err := backlightIoctl(fd, backlightGetStatus, &props); err != nil {
return 0, err
}
return int(props.brightness), nil
}
func (b *BacklightBackend) Rescan() error {
return b.scanDevices()
}
func (b *BacklightBackend) GetDevices() ([]Device, error) {
devices := make([]Device, 0)
b.devices.Range(func(id, path string) bool {
brightness, err := readBrightness(path)
if err != nil {
log.Debugf("failed to read brightness for %s: %v", id, err)
return true
}
devices = append(devices, Device{
Class: ClassBacklight,
ID: id,
Name: strings.TrimPrefix(id, "backlight:"),
Current: brightness,
Max: 100,
CurrentPercent: brightness,
Backend: "backlight",
})
return true
})
return devices, nil
}
func (b *BacklightBackend) SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error {
if percent < 0 || percent > 100 {
return fmt.Errorf("percent out of range: %d", percent)
}
path, ok := b.devices.Load(id)
if !ok {
return fmt.Errorf("device not found: %s", id)
}
value := percent
switch {
case percent == 0:
value = 1
case exponential:
value = 1 + int(math.Round(math.Pow(float64(percent-1)/99.0, exponent)*99.0))
}
fd, err := unix.Open(path, unix.O_RDWR, 0)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer unix.Close(fd)
props := backlightProps{brightness: uint32(value)}
if err := backlightIoctl(fd, backlightUpdateStatus, &props); err != nil {
return fmt.Errorf("set brightness: %w", err)
}
log.Debugf("set %s to %d%% (hw %d) via backlight(4)", id, percent, value)
return nil
}
@@ -1,162 +0,0 @@
package brightness
import (
"net"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
// devd(8) publishes device events on this SOCK_SEQPACKET socket, one
// "!system=... subsystem=... type=..." record per packet.
const devdSocketPath = "/var/run/devd.seqpacket.pipe"
const (
devdMaxRetries = 5
devdBaseDelay = 2 * time.Second
devdMaxDelay = 60 * time.Second
)
type DevdMonitor struct {
stop chan struct{}
rescanMutex sync.Mutex
rescanTimer *time.Timer
rescanPending bool
}
func newDevdMonitor(manager *Manager) *DevdMonitor {
m := &DevdMonitor{
stop: make(chan struct{}),
}
go m.run(manager)
return m
}
func (m *DevdMonitor) run(manager *Manager) {
failures := 0
for {
if err := m.monitorLoop(manager); err != nil {
log.Errorf("Devd monitor error: %v", err)
}
select {
case <-m.stop:
return
default:
}
failures++
if failures > devdMaxRetries {
log.Errorf("Devd monitor exceeded %d retries, giving up", devdMaxRetries)
return
}
delay := min(devdBaseDelay*time.Duration(1<<(failures-1)), devdMaxDelay)
log.Infof("Devd monitor reconnecting in %v (attempt %d/%d)", delay, failures, devdMaxRetries)
select {
case <-m.stop:
return
case <-time.After(delay):
}
}
}
func (m *DevdMonitor) monitorLoop(manager *Manager) error {
conn, err := net.Dial("unixpacket", devdSocketPath)
if err != nil {
return err
}
defer conn.Close()
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-m.stop:
conn.Close()
case <-done:
}
}()
log.Info("Devd monitor started for backlight/drm events")
buf := make([]byte, 8192)
for {
n, err := conn.Read(buf)
if err != nil {
select {
case <-m.stop:
return nil
default:
return err
}
}
m.handleEvent(manager, string(buf[:n]))
}
}
func (m *DevdMonitor) handleEvent(manager *Manager, event string) {
notification, ok := strings.CutPrefix(event, "!")
if !ok {
return
}
fields := parseDevdEvent(notification)
switch fields["system"] {
case "DRM":
m.debouncedRescan(manager)
case "DEVFS":
if fields["subsystem"] != "CDEV" {
return
}
if !strings.HasPrefix(fields["cdev"], "backlight/") {
return
}
m.debouncedRescan(manager)
}
}
func parseDevdEvent(s string) map[string]string {
fields := make(map[string]string)
for _, part := range strings.Fields(s) {
k, v, ok := strings.Cut(part, "=")
if !ok {
continue
}
fields[k] = v
}
return fields
}
func (m *DevdMonitor) debouncedRescan(manager *Manager) {
m.rescanMutex.Lock()
defer m.rescanMutex.Unlock()
m.rescanPending = true
if m.rescanTimer != nil {
m.rescanTimer.Reset(2 * time.Second)
return
}
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
m.rescanMutex.Lock()
pending := m.rescanPending
m.rescanPending = false
m.rescanMutex.Unlock()
if !pending {
return
}
manager.Rescan()
})
}
func (m *DevdMonitor) Close() {
close(m.stop)
}
+12 -10
View File
@@ -1,13 +1,15 @@
package brightness
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/dankgo/ipc/params"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
switch req.Method {
case "brightness.getState":
handleGetState(conn, req, m)
@@ -26,11 +28,11 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
}
}
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleSetBrightness(conn *models.Conn, req models.Request, m *Manager) {
func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -54,7 +56,7 @@ func handleSetBrightness(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleIncrement(conn *models.Conn, req models.Request, m *Manager) {
func handleIncrement(conn net.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -73,7 +75,7 @@ func handleIncrement(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleDecrement(conn *models.Conn, req models.Request, m *Manager) {
func handleDecrement(conn net.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -92,19 +94,19 @@ func handleDecrement(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleRescan(conn *models.Conn, req models.Request, m *Manager) {
func handleRescan(conn net.Conn, req models.Request, m *Manager) {
m.Rescan()
models.Respond(conn, req.ID, m.GetState())
}
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("brightness-%d", req.ID)
ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID)
initialState := m.GetState()
if err := conn.WriteResponse(models.Response[State]{
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
@@ -112,7 +114,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
}
for state := range ch {
if err := conn.WriteResponse(models.Response[State]{
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &state,
}); err != nil {
+45 -15
View File
@@ -20,7 +20,7 @@ func NewManagerWithOptions(exponential bool) (*Manager, error) {
}
go m.initLogind()
go m.initNative()
go m.initSysfs()
go m.initDDC()
return m, nil
@@ -40,6 +40,39 @@ func (m *Manager) initLogind() {
log.Info("Logind backend initialized - will use for brightness control")
}
func (m *Manager) initSysfs() {
log.Debug("Initializing sysfs backend...")
sysfs, err := NewSysfsBackend()
if err != nil {
log.Warnf("Failed to initialize sysfs backend: %v", err)
return
}
devices, err := sysfs.GetDevices()
if err != nil {
log.Warnf("Failed to get initial sysfs devices: %v", err)
m.sysfsBackend = sysfs
m.sysfsReady = true
m.updateState()
m.initUdev()
return
}
log.Infof("Sysfs backend initialized with %d devices", len(devices))
for _, d := range devices {
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
}
m.sysfsBackend = sysfs
m.sysfsReady = true
m.updateState()
m.initUdev()
}
func (m *Manager) initUdev() {
m.udevMonitor = NewUdevMonitor(m)
}
func (m *Manager) initDDC() {
ddc, err := NewDDCBackend()
if err != nil {
@@ -63,9 +96,9 @@ func (m *Manager) Rescan() {
}
}
if m.nativeReady && m.nativeBackend != nil {
if err := m.nativeBackend.Rescan(); err != nil {
log.Debugf("Native backend rescan failed: %v", err)
if m.sysfsReady && m.sysfsBackend != nil {
if err := m.sysfsBackend.Rescan(); err != nil {
log.Debugf("Sysfs rescan failed: %v", err)
}
}
@@ -117,10 +150,10 @@ func stateChanged(old, new State) bool {
func (m *Manager) updateState() {
allDevices := make([]Device, 0)
if m.nativeReady && m.nativeBackend != nil {
devices, err := m.nativeBackend.GetDevices()
if m.sysfsReady && m.sysfsBackend != nil {
devices, err := m.sysfsBackend.GetDevices()
if err != nil {
log.Debugf("Failed to get native backend devices: %v", err)
log.Debugf("Failed to get sysfs devices: %v", err)
}
if err == nil {
allDevices = append(allDevices, devices...)
@@ -199,21 +232,18 @@ func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, expone
m.stateMutex.Unlock()
var err error
switch {
case deviceClass == ClassDDC:
if deviceClass == ClassDDC {
log.Debugf("Calling DDC backend for %s", deviceID)
err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() {
m.updateState()
m.debouncedBroadcast(deviceID)
})
case m.logindReady && m.logindBackend != nil:
} else if m.logindReady && m.logindBackend != nil {
log.Debugf("Calling logind backend for %s", deviceID)
err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent)
case m.nativeBackend != nil:
log.Debugf("Calling native backend for %s", deviceID)
err = m.nativeBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
default:
err = fmt.Errorf("no brightness backend for %s", deviceID)
} else {
log.Debugf("Calling sysfs backend for %s", deviceID)
err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
}
if err != nil {
@@ -1,24 +0,0 @@
package brightness
import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
func (m *Manager) initNative() {
log.Debug("Initializing backlight backend...")
backend, err := NewBacklightBackend()
if err != nil {
log.Warnf("Failed to initialize backlight backend: %v", err)
return
}
devices, err := backend.GetDevices()
if err == nil {
log.Infof("Backlight backend initialized with %d devices", len(devices))
}
m.nativeBackend = backend
m.nativeReady = true
m.updateState()
m.monitor = newDevdMonitor(m)
}
@@ -1,36 +0,0 @@
package brightness
import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
func (m *Manager) initNative() {
log.Debug("Initializing sysfs backend...")
sysfs, err := NewSysfsBackend()
if err != nil {
log.Warnf("Failed to initialize sysfs backend: %v", err)
return
}
devices, err := sysfs.GetDevices()
if err != nil {
log.Warnf("Failed to get initial sysfs devices: %v", err)
m.sysfsBackend = sysfs
m.nativeBackend = sysfs
m.nativeReady = true
m.updateState()
m.monitor = NewUdevMonitor(m)
return
}
log.Infof("Sysfs backend initialized with %d devices", len(devices))
for _, d := range devices {
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
}
m.sysfsBackend = sysfs
m.nativeBackend = sysfs
m.nativeReady = true
m.updateState()
m.monitor = NewUdevMonitor(m)
}
@@ -43,7 +43,7 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
nativeReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
@@ -114,7 +114,7 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
nativeReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
@@ -180,9 +180,8 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
m := &Manager{
logindBackend: nil,
sysfsBackend: sysfs,
nativeBackend: sysfs,
logindReady: false,
nativeReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
@@ -244,7 +243,7 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
nativeReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
+5 -16
View File
@@ -4,7 +4,7 @@ import (
"sync"
"time"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type DeviceClass string
@@ -33,25 +33,14 @@ type DeviceUpdate struct {
Device Device `json:"device"`
}
type Backend interface {
Rescan() error
GetDevices() ([]Device, error)
SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error
}
type deviceMonitor interface {
Close()
}
type Manager struct {
logindBackend *LogindBackend
sysfsBackend *SysfsBackend
nativeBackend Backend
ddcBackend *DDCBackend
monitor deviceMonitor
udevMonitor *UdevMonitor
logindReady bool
nativeReady bool
sysfsReady bool
ddcReady bool
exponential bool
@@ -181,8 +170,8 @@ func (m *Manager) Close() {
return true
})
if m.monitor != nil {
m.monitor.Close()
if m.udevMonitor != nil {
m.udevMonitor.Close()
}
if m.logindBackend != nil {
@@ -33,7 +33,7 @@ func setupTestManager(t *testing.T) (*Manager, string) {
m := &Manager{
sysfsBackend: sysfs,
nativeReady: true,
sysfsReady: true,
stopChan: make(chan struct{}),
}
+3 -1
View File
@@ -1,10 +1,12 @@
package browser
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "browser.open":
url, ok := models.Get[string](req, "url")
+1 -1
View File
@@ -3,7 +3,7 @@ package browser
import (
"sync"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type Manager struct {
+25 -23
View File
@@ -1,15 +1,17 @@
package clipboard
import (
"encoding/json"
"errors"
"fmt"
"net"
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/dankgo/ipc/params"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
switch req.Method {
case "clipboard.getState":
handleGetState(conn, req, m)
@@ -56,11 +58,11 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
}
}
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleGetHistory(conn *models.Conn, req models.Request, m *Manager) {
func handleGetHistory(conn net.Conn, req models.Request, m *Manager) {
history := m.GetHistory()
for i := range history {
history[i].Data = nil
@@ -68,7 +70,7 @@ func handleGetHistory(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, history)
}
func handleGetEntry(conn *models.Conn, req models.Request, m *Manager) {
func handleGetEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -88,7 +90,7 @@ func handleGetEntry(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, entry)
}
func handleDeleteEntry(conn *models.Conn, req models.Request, m *Manager) {
func handleDeleteEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -103,12 +105,12 @@ func handleDeleteEntry(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry deleted"})
}
func handleClearHistory(conn *models.Conn, req models.Request, m *Manager) {
func handleClearHistory(conn net.Conn, req models.Request, m *Manager) {
m.ClearHistory()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "history cleared"})
}
func handleCopy(conn *models.Conn, req models.Request, m *Manager) {
func handleCopy(conn net.Conn, req models.Request, m *Manager) {
text, err := params.String(req.Params, "text")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -123,7 +125,7 @@ func handleCopy(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
}
func handleCopyEntry(conn *models.Conn, req models.Request, m *Manager) {
func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -171,7 +173,7 @@ func handleCopyEntry(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
}
func handlePaste(conn *models.Conn, req models.Request, m *Manager) {
func handlePaste(conn net.Conn, req models.Request, m *Manager) {
text, err := m.PasteText()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -181,7 +183,7 @@ func handlePaste(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, map[string]string{"text": text})
}
func handleSendPaste(conn *models.Conn, req models.Request) {
func handleSendPaste(conn net.Conn, req models.Request) {
shift, _ := models.Get[bool](req, "shift")
if err := clipboardstore.SendPasteKeystroke(shift); err != nil {
@@ -192,14 +194,14 @@ func handleSendPaste(conn *models.Conn, req models.Request) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paste sent"})
}
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("clipboard-%d", req.ID)
ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID)
initialState := m.GetState()
if err := conn.WriteResponse(models.Response[State]{
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
@@ -207,7 +209,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
}
for state := range ch {
if err := conn.WriteResponse(models.Response[State]{
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &state,
}); err != nil {
@@ -216,7 +218,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
}
}
func handleSearch(conn *models.Conn, req models.Request, m *Manager) {
func handleSearch(conn net.Conn, req models.Request, m *Manager) {
p := SearchParams{
Query: params.StringOpt(req.Params, "query", ""),
MimeType: params.StringOpt(req.Params, "mimeType", ""),
@@ -239,11 +241,11 @@ func handleSearch(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.Search(p))
}
func handleGetConfig(conn *models.Conn, req models.Request, m *Manager) {
func handleGetConfig(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetConfig())
}
func handleSetConfig(conn *models.Conn, req models.Request, m *Manager) {
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
cfg := m.GetConfig()
if v, ok := models.Get[float64](req, "maxHistory"); ok {
@@ -273,7 +275,7 @@ func handleSetConfig(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
}
func handleStore(conn *models.Conn, req models.Request, m *Manager) {
func handleStore(conn net.Conn, req models.Request, m *Manager) {
data, err := params.String(req.Params, "data")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -290,7 +292,7 @@ func handleStore(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
}
func handlePinEntry(conn *models.Conn, req models.Request, m *Manager) {
func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -305,7 +307,7 @@ func handlePinEntry(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
}
func handleUnpinEntry(conn *models.Conn, req models.Request, m *Manager) {
func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -320,17 +322,17 @@ func handleUnpinEntry(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
}
func handleGetPinnedEntries(conn *models.Conn, req models.Request, m *Manager) {
func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
pinned := m.GetPinnedEntries()
models.Respond(conn, req.ID, pinned)
}
func handleGetPinnedCount(conn *models.Conn, req models.Request, m *Manager) {
func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
count := m.GetPinnedCount()
models.Respond(conn, req.ID, map[string]int{"count": count})
}
func handleCopyFile(conn *models.Conn, req models.Request, m *Manager) {
func handleCopyFile(conn net.Conn, req models.Request, m *Manager) {
filePath, err := params.String(req.Params, "filePath")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -311,15 +311,14 @@ func TestHandleGetEntry_ReturnsExistingEntry(t *testing.T) {
history := m.GetHistory()
require.Len(t, history, 1)
mc := newClipboardTestConn()
conn := models.NewConn(mc)
conn := newClipboardTestConn()
handleGetEntry(conn, models.Request{
ID: 1,
Params: map[string]any{"id": float64(history[0].ID)},
}, m)
var resp models.Response[Entry]
require.NoError(t, json.NewDecoder(mc.writeBuf).Decode(&resp))
require.NoError(t, json.NewDecoder(conn.writeBuf).Decode(&resp))
assert.Empty(t, resp.Error)
require.NotNil(t, resp.Result)
assert.Equal(t, history[0].ID, resp.Result.ID)
@@ -328,8 +327,7 @@ func TestHandleGetEntry_ReturnsExistingEntry(t *testing.T) {
func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
m := newTestManagerWithDB(t)
mc := newClipboardTestConn()
conn := models.NewConn(mc)
conn := newClipboardTestConn()
handleGetEntry(conn, models.Request{
ID: 1,
@@ -337,7 +335,7 @@ func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
}, m)
var resp models.Response[any]
require.NoError(t, json.NewDecoder(mc.writeBuf).Decode(&resp))
require.NoError(t, json.NewDecoder(conn.writeBuf).Decode(&resp))
assert.Empty(t, resp.Error)
assert.Nil(t, resp.Result)
}
+31 -29
View File
@@ -1,10 +1,12 @@
package cups
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/dankgo/ipc/params"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
type CUPSEvent struct {
@@ -18,7 +20,7 @@ type TestPageResult struct {
Message string `json:"message"`
}
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "cups.subscribe":
handleSubscribe(conn, req, manager)
@@ -75,7 +77,7 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
}
}
func handleGetPrinters(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) {
printers, err := manager.GetPrinters()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -84,7 +86,7 @@ func handleGetPrinters(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, printers)
}
func handleGetJobs(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -99,7 +101,7 @@ func handleGetJobs(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, jobs)
}
func handlePausePrinter(conn *models.Conn, req models.Request, manager *Manager) {
func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -113,7 +115,7 @@ func handlePausePrinter(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paused"})
}
func handleResumePrinter(conn *models.Conn, req models.Request, manager *Manager) {
func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -127,7 +129,7 @@ func handleResumePrinter(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "resumed"})
}
func handleCancelJob(conn *models.Conn, req models.Request, manager *Manager) {
func handleCancelJob(conn net.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -141,7 +143,7 @@ func handleCancelJob(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job canceled"})
}
func handlePurgeJobs(conn *models.Conn, req models.Request, manager *Manager) {
func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -155,7 +157,7 @@ func handlePurgeJobs(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "jobs canceled"})
}
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
@@ -166,7 +168,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Data: initialState,
}
if err := conn.WriteResponse(models.Response[CUPSEvent]{
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
@@ -178,7 +180,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Type: "state_changed",
Data: state,
}
if err := conn.WriteResponse(models.Response[CUPSEvent]{
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
Result: &event,
}); err != nil {
return
@@ -186,7 +188,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
}
}
func handleGetDevices(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
devices, err := manager.GetDevices()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -195,7 +197,7 @@ func handleGetDevices(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, devices)
}
func handleGetPPDs(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
ppds, err := manager.GetPPDs()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -204,7 +206,7 @@ func handleGetPPDs(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, ppds)
}
func handleGetClasses(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
classes, err := manager.GetClasses()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -213,7 +215,7 @@ func handleGetClasses(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, classes)
}
func handleCreatePrinter(conn *models.Conn, req models.Request, manager *Manager) {
func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) {
name, err := params.StringNonEmpty(req.Params, "name")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -244,7 +246,7 @@ func handleCreatePrinter(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer created"})
}
func handleDeletePrinter(conn *models.Conn, req models.Request, manager *Manager) {
func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -258,7 +260,7 @@ func handleDeletePrinter(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer deleted"})
}
func handleAcceptJobs(conn *models.Conn, req models.Request, manager *Manager) {
func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -272,7 +274,7 @@ func handleAcceptJobs(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "accepting jobs"})
}
func handleRejectJobs(conn *models.Conn, req models.Request, manager *Manager) {
func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -286,7 +288,7 @@ func handleRejectJobs(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "rejecting jobs"})
}
func handleSetPrinterShared(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -306,7 +308,7 @@ func handleSetPrinterShared(conn *models.Conn, req models.Request, manager *Mana
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sharing updated"})
}
func handleSetPrinterLocation(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -326,7 +328,7 @@ func handleSetPrinterLocation(conn *models.Conn, req models.Request, manager *Ma
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location updated"})
}
func handleSetPrinterInfo(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -346,7 +348,7 @@ func handleSetPrinterInfo(conn *models.Conn, req models.Request, manager *Manage
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "info updated"})
}
func handleMoveJob(conn *models.Conn, req models.Request, manager *Manager) {
func handleMoveJob(conn net.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -366,7 +368,7 @@ func handleMoveJob(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job moved"})
}
func handlePrintTestPage(conn *models.Conn, req models.Request, manager *Manager) {
func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -381,7 +383,7 @@ func handlePrintTestPage(conn *models.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"})
}
func handleAddPrinterToClass(conn *models.Conn, req models.Request, manager *Manager) {
func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -401,7 +403,7 @@ func handleAddPrinterToClass(conn *models.Conn, req models.Request, manager *Man
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer added to class"})
}
func handleRemovePrinterFromClass(conn *models.Conn, req models.Request, manager *Manager) {
func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -421,7 +423,7 @@ func handleRemovePrinterFromClass(conn *models.Conn, req models.Request, manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer removed from class"})
}
func handleDeleteClass(conn *models.Conn, req models.Request, manager *Manager) {
func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -435,7 +437,7 @@ func handleDeleteClass(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "class deleted"})
}
func handleRestartJob(conn *models.Conn, req models.Request, manager *Manager) {
func handleRestartJob(conn net.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -449,7 +451,7 @@ func handleRestartJob(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job restarted"})
}
func handleHoldJob(conn *models.Conn, req models.Request, manager *Manager) {
func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -465,7 +467,7 @@ func handleHoldJob(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
}
func handleTestConnection(conn *models.Conn, req models.Request, manager *Manager) {
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
host, err := params.StringNonEmpty(req.Params, "host")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
+28 -28
View File
@@ -41,7 +41,7 @@ func TestHandleGetPrinters(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -66,7 +66,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -98,7 +98,7 @@ func TestHandleGetJobs(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -125,7 +125,7 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -150,7 +150,7 @@ func TestHandlePausePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -177,7 +177,7 @@ func TestHandleResumePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -204,7 +204,7 @@ func TestHandleCancelJob(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -231,7 +231,7 @@ func TestHandlePurgeJobs(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -258,7 +258,7 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -285,7 +285,7 @@ func TestHandleGetDevices(t *testing.T) {
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getDevices"}
handleGetDevices(conn, req, m)
@@ -307,7 +307,7 @@ func TestHandleGetPPDs(t *testing.T) {
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getPPDs"}
handleGetPPDs(conn, req, m)
@@ -330,7 +330,7 @@ func TestHandleGetClasses(t *testing.T) {
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getClasses"}
handleGetClasses(conn, req, m)
@@ -351,7 +351,7 @@ func TestHandleCreatePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -375,7 +375,7 @@ func TestHandleCreatePrinter_MissingParams(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
handleCreatePrinter(conn, req, m)
@@ -394,7 +394,7 @@ func TestHandleDeletePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -417,7 +417,7 @@ func TestHandleAcceptJobs(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -440,7 +440,7 @@ func TestHandleRejectJobs(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -463,7 +463,7 @@ func TestHandleSetPrinterShared(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -486,7 +486,7 @@ func TestHandleSetPrinterLocation(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -509,7 +509,7 @@ func TestHandleSetPrinterInfo(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -532,7 +532,7 @@ func TestHandleMoveJob(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -555,7 +555,7 @@ func TestHandlePrintTestPage(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -579,7 +579,7 @@ func TestHandleAddPrinterToClass(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -602,7 +602,7 @@ func TestHandleRemovePrinterFromClass(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -625,7 +625,7 @@ func TestHandleDeleteClass(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -648,7 +648,7 @@ func TestHandleRestartJob(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -671,7 +671,7 @@ func TestHandleHoldJob(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -694,7 +694,7 @@ func TestHandleHoldJob_WithHoldUntil(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -302,7 +302,7 @@ func TestHandleTestConnection_Success(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -325,7 +325,7 @@ func TestHandleTestConnection_Success(t *testing.T) {
func TestHandleTestConnection_MissingHost(t *testing.T) {
m := NewTestManager(nil, nil)
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -351,7 +351,7 @@ func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
@@ -379,7 +379,7 @@ func TestHandleRequest_TestConnection(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf})
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type CUPSState struct {
+11 -10
View File
@@ -2,9 +2,10 @@ package dbus
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/dankgo/ipc/params"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
type objectParams struct {
@@ -42,7 +43,7 @@ func extractObjectParams(p map[string]any, requirePath bool) (objectParams, erro
return objectParams{bus: bus, dest: dest, path: path, iface: iface}, nil
}
func HandleRequest(conn *models.Conn, req models.Request, m *Manager, clientID string) {
func HandleRequest(conn net.Conn, req models.Request, m *Manager, clientID string) {
switch req.Method {
case "dbus.call":
handleCall(conn, req, m)
@@ -65,7 +66,7 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager, clientID s
}
}
func handleCall(conn *models.Conn, req models.Request, m *Manager) {
func handleCall(conn net.Conn, req models.Request, m *Manager) {
op, err := extractObjectParams(req.Params, true)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -94,7 +95,7 @@ func handleCall(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleGetProperty(conn *models.Conn, req models.Request, m *Manager) {
func handleGetProperty(conn net.Conn, req models.Request, m *Manager) {
op, err := extractObjectParams(req.Params, true)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -116,7 +117,7 @@ func handleGetProperty(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleSetProperty(conn *models.Conn, req models.Request, m *Manager) {
func handleSetProperty(conn net.Conn, req models.Request, m *Manager) {
op, err := extractObjectParams(req.Params, true)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -143,7 +144,7 @@ func handleSetProperty(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
}
func handleGetAllProperties(conn *models.Conn, req models.Request, m *Manager) {
func handleGetAllProperties(conn net.Conn, req models.Request, m *Manager) {
op, err := extractObjectParams(req.Params, true)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -159,7 +160,7 @@ func handleGetAllProperties(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleIntrospect(conn *models.Conn, req models.Request, m *Manager) {
func handleIntrospect(conn net.Conn, req models.Request, m *Manager) {
bus, err := params.String(req.Params, "bus")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -183,7 +184,7 @@ func handleIntrospect(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleListNames(conn *models.Conn, req models.Request, m *Manager) {
func handleListNames(conn net.Conn, req models.Request, m *Manager) {
bus, err := params.String(req.Params, "bus")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -199,7 +200,7 @@ func handleListNames(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager, clientID string) {
func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) {
if id := params.StringOpt(req.Params, "clientId", ""); id != "" {
clientID = id
}
@@ -224,7 +225,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager, clientID
models.Respond(conn, req.ID, result)
}
func handleUnsubscribe(conn *models.Conn, req models.Request, m *Manager) {
func handleUnsubscribe(conn net.Conn, req models.Request, m *Manager) {
subID, err := params.String(req.Params, "subscriptionId")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/dbusutil"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/godbus/dbus/v5"
)
+1 -1
View File
@@ -1,7 +1,7 @@
package dbus
import (
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
)
+4 -2
View File
@@ -1,10 +1,12 @@
package evdev
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
switch req.Method {
case "evdev.getState":
handleGetState(conn, req, m)
@@ -13,6 +15,6 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
}
}
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
+6 -9
View File
@@ -52,8 +52,7 @@ func TestHandleRequest(t *testing.T) {
closeChan: make(chan struct{}),
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "evdev.getState",
@@ -63,7 +62,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, m)
var resp models.Response[State]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -82,8 +81,7 @@ func TestHandleRequest(t *testing.T) {
closeChan: make(chan struct{}),
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 456,
Method: "evdev.unknownMethod",
@@ -93,7 +91,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, m)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 456, resp.ID)
@@ -112,8 +110,7 @@ func TestHandleGetState(t *testing.T) {
closeChan: make(chan struct{}),
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 789,
Method: "evdev.getState",
@@ -123,7 +120,7 @@ func TestHandleGetState(t *testing.T) {
handleGetState(conn, req, m)
var resp models.Response[State]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 789, resp.ID)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/fsnotify/fsnotify"
evdev "github.com/holoplot/go-evdev"
)
+12 -11
View File
@@ -2,12 +2,13 @@ package freedesktop
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/dankgo/ipc/params"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "freedesktop.getState":
handleGetState(conn, req, manager)
@@ -32,11 +33,11 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetIconFile(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
iconPath, err := params.String(req.Params, "path")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -51,7 +52,7 @@ func handleSetIconFile(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon file set"})
}
func handleSetRealName(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
name, err := params.String(req.Params, "name")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -66,7 +67,7 @@ func handleSetRealName(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "real name set"})
}
func handleSetEmail(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
email, err := params.String(req.Params, "email")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -81,7 +82,7 @@ func handleSetEmail(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "email set"})
}
func handleSetLanguage(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
language, err := params.String(req.Params, "language")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -96,7 +97,7 @@ func handleSetLanguage(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "language set"})
}
func handleSetLocation(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
location, err := params.String(req.Params, "location")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -111,7 +112,7 @@ func handleSetLocation(conn *models.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
}
func handleGetUserIconFile(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager) {
username, err := params.String(req.Params, "username")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -127,7 +128,7 @@ func handleGetUserIconFile(conn *models.Conn, req models.Request, manager *Manag
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Value: iconFile})
}
func handleGetColorScheme(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.updateSettingsState(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -137,7 +138,7 @@ func handleGetColorScheme(conn *models.Conn, req models.Request, manager *Manage
models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme})
}
func handleSetIconTheme(conn *models.Conn, req models.Request, manager *Manager) {
func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) {
iconTheme, err := params.String(req.Params, "iconTheme")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -60,12 +60,11 @@ func mockGetAllAccountsProperties() *dbus.Call {
}
func TestRespondError_Freedesktop(t *testing.T) {
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
models.RespondError(conn, 123, "test error")
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -74,13 +73,12 @@ func TestRespondError_Freedesktop(t *testing.T) {
}
func TestRespond_Freedesktop(t *testing.T) {
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -107,14 +105,13 @@ func TestHandleGetState(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "freedesktop.getState"}
handleGetState(conn, req, manager)
var resp models.Response[FreedeskState]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -133,8 +130,7 @@ func TestHandleSetIconFile(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
@@ -144,7 +140,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -167,8 +163,7 @@ func TestHandleSetIconFile(t *testing.T) {
accountsObj: mockAccountsObj,
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
@@ -180,7 +175,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -200,8 +195,7 @@ func TestHandleSetIconFile(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
@@ -213,7 +207,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -228,8 +222,7 @@ func TestHandleSetRealName(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setRealName",
@@ -239,7 +232,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -262,8 +255,7 @@ func TestHandleSetRealName(t *testing.T) {
accountsObj: mockAccountsObj,
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setRealName",
@@ -275,7 +267,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -293,8 +285,7 @@ func TestHandleSetEmail(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setEmail",
@@ -304,7 +295,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -327,8 +318,7 @@ func TestHandleSetEmail(t *testing.T) {
accountsObj: mockAccountsObj,
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setEmail",
@@ -340,7 +330,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -358,8 +348,7 @@ func TestHandleSetLanguage(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setLanguage",
@@ -369,7 +358,7 @@ func TestHandleSetLanguage(t *testing.T) {
handleSetLanguage(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -384,8 +373,7 @@ func TestHandleSetLocation(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setLocation",
@@ -395,7 +383,7 @@ func TestHandleSetLocation(t *testing.T) {
handleSetLocation(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -410,8 +398,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.getUserIconFile",
@@ -421,7 +408,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -438,8 +425,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.getUserIconFile",
@@ -451,7 +437,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -470,14 +456,13 @@ func TestHandleGetColorScheme(t *testing.T) {
stateMutex: sync.RWMutex{},
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager)
var resp models.Response[map[string]uint32]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -502,14 +487,13 @@ func TestHandleGetColorScheme(t *testing.T) {
settingsObj: mockSettingsObj,
}
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager)
var resp models.Response[map[string]uint32]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -531,8 +515,7 @@ func TestHandleRequest(t *testing.T) {
}
t.Run("unknown method", func(t *testing.T) {
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.unknown",
@@ -541,7 +524,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -549,8 +532,7 @@ func TestHandleRequest(t *testing.T) {
})
t.Run("valid method - getState", func(t *testing.T) {
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: "freedesktop.getState",
@@ -559,7 +541,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager)
var resp models.Response[FreedeskState]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -578,8 +560,7 @@ func TestHandleRequest(t *testing.T) {
}
for _, method := range tests {
mc := newMockNetConn()
conn := models.NewConn(mc)
conn := newMockNetConn()
req := models.Request{
ID: 123,
Method: method,
@@ -589,7 +570,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/dbusutil"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/godbus/dbus/v5"
)
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"sync"
"time"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
)
+7 -5
View File
@@ -1,7 +1,9 @@
package location
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
@@ -11,7 +13,7 @@ type LocationEvent struct {
Data State `json:"data"`
}
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "location.getState":
handleGetState(conn, req, manager)
@@ -23,11 +25,11 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
@@ -38,7 +40,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Data: initialState,
}
if err := conn.WriteResponse(models.Response[LocationEvent]{
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
@@ -50,7 +52,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Type: "state_changed",
Data: state,
}
if err := conn.WriteResponse(models.Response[LocationEvent]{
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
Result: &event,
}); err != nil {
return

Some files were not shown because too many files have changed in this diff Show More