Compare commits

..
610 Commits
Author SHA1 Message Date
bbedward aeacaf7afc distro/freebsd: drop elogind references 2026-10-22 10:38:15 -04:00
bbedward 14b3ddaf83 plugins: label declared permissions as unenforced 2026-10-22 09:44:13 -04:00
bbedward 6e86b53ccd plugins: keep manifest paths inside the plugin directory 2026-10-22 09:44:08 -04:00
bbedward c5d1d4fe46 greeter: remove the embedded legacy greeter (again) 2026-10-22 09:37:07 -04:00
purian23 39a58f77ec distos: compare ubuntu published commit by prefix 2026-10-22 06:06:38 -04:00
purian23 47c9d8d89b notifications: keep popup content while it exits after the notification closes
Port 2.0
2026-10-21 21:58:13 -04:00
bbedward d8786eaaf7 bar: keep far-edge island hosts at sheet size so the bar never jumps on expand
fixes #3855
port 2.0
2026-10-21 21:57:20 -04:00
Anton Kesy 4f969c8e10 docs: use dux project names in README (#3847) 2026-10-21 15:50:06 -04:00
bbedward 1b59a474dd battery: throttle pulse animation to 20fps
ref #3849
2026-10-21 13:59:43 -04:00
bbedward 28f921fa02 dash: hold panel height while dragging cards unless hung from a top bar
lyrics: ignore floored position samples and shell refresh emits
2026-10-21 13:15:53 -04:00
bbedward 8f2a486006 theme: publish typography for dux apps 2026-10-21 12:26:05 -04:00
bbedward 27a703367f core: only use the simple animation driver for the scrolling wallpaper
fixes #3837
2026-10-21 11:18:18 -04:00
bbedward 8e851b3dc4 bar: slide an auto-hidden island by its compact height
fixes #3838
2026-10-21 11:05:57 -04:00
bbedward 112d05901f workspaces: keep the pill highlight while hovering its app icons
fixes #3839
2026-10-21 10:39:40 -04:00
bbedward afeea489c9 theme: skip matugen runs whose inputs match the last applied run
fixes #3841
2026-10-21 10:34:22 -04:00
bbedward 90d3c9731c niri: only emit displayScales when a scale changed
ref #3842
port 2.0
2026-10-21 10:14:52 -04:00
Marlon Kranz b81954454a fix(notifications): close popups when their screen stops showing them (#3844) 2026-10-21 08:02:49 -04:00
Adarsh 479b1434de Theme Antigravity IDE via the VS Code extension (#3836) 2026-10-21 03:57:45 -04:00
Tomás 203b1369b3 Keep MangoWM workspace apps on their own monitor (#3832)
port 2.0
2026-10-20 20:54:58 -04:00
Leonard 5ffc581ecf refactor(ui): match the dock settings page with the bar settings page (#3829)
port 2.0
2026-10-20 20:54:40 -04:00
bbedward 3284acab28 matugen: neovim template reads the dms config dir"
port 2.0
2026-10-20 17:03:12 -04:00
bbedward 1fb9f5271a ci: cache go modules from core/go.sum, retry downloads 2026-10-20 15:24:59 -04:00
bbedward 9b413296b1 bump VERSION to v2.0.0 2026-10-20 15:17:09 -04:00
bbedward d04eae4210 submodule: dank-qml-common is now dux-qml-common 2026-10-20 14:18:33 -04:00
bbedward 5ec42c91dc migrate: keep the greeter ACL when merging legacy dirs 2026-10-20 13:56:08 -04:00
bbedward b97fda0639 branding: dux labs, repo urls 2026-10-20 13:44:31 -04:00
bbedward 5aa26677c1 i18n: regenerate catalogs 2026-10-20 13:42:06 -04:00
bbedward b94fb6a2c8 greeter: fix opensuse docs anchor 2026-10-20 13:41:57 -04:00
bbedward 516be7ca89 ci: dux-ci bot identity 2026-10-20 13:31:43 -04:00
bbedward 7c39d251b7 docs: ko-fi moved to duxlabs 2026-10-20 13:21:37 -04:00
bbedward cff4496ec5 deps: dux-go, rebrand follow-ups 2026-10-20 13:02:11 -04:00
bbedward 9e177f2293 bump qml-common 2026-10-20 13:01:41 -04:00
bbedward 807853c9a4 branding: rebrand to dux 2 2026-10-20 12:59:09 -04:00
bbedward 789a968cde branding: rebrand to dux 2026-10-20 12:23:25 -04:00
bbedward 788b722702 island: keep every compact face at the compact size
fixes #3815
2026-10-20 10:21:48 -04:00
bbedward bbb91a5a72 i18n: sync 2026-10-20 09:53:06 -04:00
bbedward 8a30a83896 changelog: track the seen release with a config dir marker instead of cache.json
Setups with a read-only session json broke the changelog marker

fixes #3825
2026-10-20 09:45:34 -04:00
bbedward 572cbcd9d3 ui/prompts: revert expressive style for auth dialogs outside of lock
screen

Can reconsider when we have a setting for it
2026-10-20 09:15:31 -04:00
purian23 bdc6a5d214 feat(PikaOS): add official support & installer options
Fixes #2772
2026-10-20 02:38:30 -04:00
bbedward 8be08999ca bar: host edge click strips in the bar body 2026-10-19 21:47:44 -04:00
apollo79 806799614c media: add rotating cookie album style (#3819) 2026-10-19 17:28:44 -04:00
bbedward 9b62944e9f i18n: sync 2026-10-19 17:17:20 -04:00
bbedward 7fafc08196 settings: add create link for wallpaper creator 2026-10-19 16:09:05 -04:00
Tomé Carvalhoandcopilot-swe-agent[bot] f43e3705b8 feat(bar): add "hide on last display" option (#3816)
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2026-10-19 15:45:10 -04:00
Evgeny Zemtsov bb3e3adaab network(networkmanager): rebuild the Wi-Fi list once per scan (#3818) 2026-10-19 15:39:56 -04:00
dms-ci[bot] d04525b2bd nix: update vendorHash for go.mod changes 2026-10-19 19:32:11 +00:00
bbedward 2b9e48d750 core: bump dankgo 2026-10-19 15:31:16 -04:00
Marlon Kranz fdb9410bc4 fix(wallpaper): keep a loading wallpaper when the surface regenerates (#3824) 2026-10-19 15:17:50 -04:00
dms-ci[bot] 078e3bfbcd nix: update vendorHash for go.mod changes 2026-10-19 18:56:45 +00:00
bbedward beab7d86e0 chore: update dependencies, hero 2026-10-19 14:55:44 -04:00
bbedward 115ed9f338 i18n: sync 2026-10-19 12:57:49 -04:00
bbedward 4155804e57 windows: size floating windows from the preferred height when only a placeholder screen is left
ref #3545
2026-10-19 12:55:40 -04:00
apollo79 7bcbc34c57 feat(lyrics): add seek-to-line on clicking lyrics line (#3823) 2026-10-19 12:53:45 -04:00
bbedward fcb74f5884 greeting: redesign welcome page & changelog page
+ enable changelog
2026-10-19 12:05:38 -04:00
bbedward 317d9399c7 notifications: stop restarting the popup timer after the enter animation
fixes #3820
2026-10-19 10:37:56 -04:00
bbedward d6a362d6b3 launcher: default action setting, focus a running window or launch
fixes #3268
2026-10-19 10:34:14 -04:00
Joseph Petersen 8628daac31 network: rediscover wifi devices before scanning (#3821) 2026-10-19 10:03:14 -04:00
bbedward 6a2b4378d6 night mode: push the automation schedule before night mode is first enabled
fixes #3186
2026-10-19 09:08:52 -04:00
bbedward a9dd3be408 notifications: keep popups clear of an auto-hide bar on their edge
fixes #3809
2026-10-18 23:03:09 -04:00
cmleon51 880135da67 brightness: dim outputs without a backlight or ddc through gamma (#3813) 2026-10-18 23:02:21 -04:00
bbedward e6f215b91d control center: hide the shell's own sound effect stream from the playback list
fixes #3810
2026-10-18 22:55:07 -04:00
bbedward 602c20e6a0 island: stop forwarding wheel from the dismiss layer to the bar
fixes #3416
fixes #3380
2026-10-18 22:50:19 -04:00
bbedward 35aff6b0f6 clarify CONTRIBUTING 2026-10-18 21:58:46 -04:00
bbedward a72f2ac43d launcher: name launch scopes app-dms-<id>-<random> so portals see the app id
Fixes #3814
2026-10-18 21:48:00 -04:00
bbedward e4f0c5335a plugins: changes for browser and plugin detail page 2026-10-18 21:10:17 -04:00
Guillem CANAL 5a480101c6 launcher: add "Open with..." action to file search results (#3801) 2026-10-18 12:01:15 -04:00
bbedward 2592e2f33e clipboard: serve selection sends off the shared wayland dispatcher
fixes #3812
2026-10-18 11:59:02 -04:00
purian23 207cbe5aae feat(switch): dms switch stable|beta replaces installed packages in place
- Settings, plugins and session state are backed up before dms changes w/restore options when returning to stable
- Snapshots are only taken when dms itself changes channel
- Info annotated on danklinux docs
2026-10-17 22:20:12 -04:00
lung595 47885e4981 plugins: detail page actions, one-click upvote, author page and undo after uninstall (3/3) (#3753) 2026-10-17 12:20:46 -04:00
lung595 2ad442fb37 plugins: discover shelves, quick filters and typo-tolerant search in the browser (2/3) (#3752) 2026-10-17 11:33:29 -04:00
bbedward c0c054552e media: drop scroll-to-cycle output on the device button 2026-10-17 10:36:11 -04:00
purian23 ae361e37fc systemd: dms systemd command binds dms.service to the preferred compositor session unit
- niri, mango & hyprland supported
- `dms systemd enable/disable/status/start/stop/restart` supported
2026-10-17 03:47:06 -04:00
purian23 d8a87a8e13 ci: bump create-github-app-token to v3 in common-parity 2026-10-16 23:00:10 -04:00
purian23 ba3574efaa refactor(dankinstall): add new dankcalendar stable detection across distros 2026-10-16 22:43:15 -04:00
purian23 da3b658b2f feat(Enterprise Linux 10): official dms support for RHEL, CentOS Stream, Rocky Linux & AlmaLinux
- dankinstall enables EPEL 10 and installs from the existing COPRs. niri is the supported compositor on EL.

- Hyprland, mango and miracle-wm on EL are supported thanks to @kmf's dank-ws-copr repos

Closes #3442
2026-10-16 21:54:37 -04:00
bbedward 8e690022f1 displays: fall back to a matching named profile when the active one loses an output
fixes #3705
2026-10-16 10:23:02 -04:00
bbedward e9ce1a9d46 settings: point adw-gtk3 help link at the application themes docs
fixes #3789
2026-10-16 09:27:26 -04:00
bbedward 506c6f5504 i18n: sync 2026-10-16 09:19:46 -04:00
Guillem CANAL 5f87b7ceea launcher: allow users to customize launcher keybindings (#3800) 2026-10-16 08:58:54 -04:00
purian23 5c40670a6d settings: collapse inactive shadow sliders under their toggles 2026-10-16 00:26:11 -04:00
Alex Hulbert cd42276870 feat(island): Add shadow to island and satellites support (#3798) 2026-10-16 00:10:35 -04:00
purian23 b08f3f8ea1 shared: bump qml-common 2026-10-15 19:36:18 -04:00
bbedward a5435c585f ban AI written PR descriptions and comments 2026-10-15 18:39:48 -04:00
purian23 806b86754c ci: bump prek to 0.5.5 2026-10-15 17:15:47 -04:00
bbedward 9d5c02002f settings: re-extract index 2026-10-15 11:30:08 -04:00
Marlon Kranz e2ecc1f2a9 fix(cli): time out when the DMS server doesn't answer (#3791) 2026-10-15 11:23:00 -04:00
bbedward 27ee4f505a chore: bump qml-common 2026-10-15 11:22:40 -04:00
Marlon Kranz d14f80ec32 fix(theme): regenerate once when the matugen scheme or monitor changes (#3793) 2026-10-15 11:19:50 -04:00
Marlon Kranz 549cbf6d08 feat(theme): terminal background opacity and wallpaper-tinted colors (#3662) 2026-10-15 11:17:29 -04:00
Marlon Kranz 6b40abe4b9 fix(settings): keep row descriptions out of their card's search entry (#3794) 2026-10-15 11:13:48 -04:00
bbedward 1184b9ca7b core: bump dgop 2026-10-15 09:48:36 -04:00
Artrixandclaude[bot] 0983c2267a audio: show headphones icon for bluetooth headset (#3783)
* audio: show headphones icon for bluetooth headset

* audio: fix bluetooth headset icons and preserve mute states

* Delete audio-output-icon.test.mjs

* Update quickshell/Services/AudioService.qml

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-10-15 09:23:12 -04:00
Marlon Kranz eb837255e0 fix(keybinds): use an existing font weight for unselected tabs (#3790)
Theme has no fontWeightNormal, so every unselected category tab in the
keybinds cheat sheet assigned undefined to font.weight. QML rejects
that and keeps the last value, so a tab stayed Medium after it was
deselected, and unselected tabs ignored the font weight setting.
Use Theme.fontWeight, the shell's regular weight.
2026-10-15 09:14:19 -04:00
Marlon Kranz 879533440e fix(island): close a focused island with one outside click (#3792)
On Hyprland, an island holding keyboard focus runs a focus grab that
only whitelisted the bar window. The full-screen dismiss layer wasn't
on the list, so the first click outside only ended the grab and the
card needed a second click to close. That hit every card that takes
keyboard focus (weather right away, media after a click inside).

Add the dismiss layer to the grab, the way DPopout whitelists its
background window.
2026-10-15 09:13:15 -04:00
bbedward 0f33c4b830 niri: drop touchpad osd ipc, rely on the event stream 2026-10-15 09:03:48 -04:00
bbedward 290a1a085b desktop widgets: intuitiveness improvements to editor 2026-10-15 08:39:49 -04:00
purian23 c4aea85296 niri: drive touchpad OSD from native compositor events 2026-10-15 02:01:02 -04:00
purian23 87ce92703c fix(ipc): open bar widget popouts at the right bar, hidden bars included
- References #3786
2026-10-15 01:40:05 -04:00
Huỳnh Thiện Lộc ad964512a9 fix(widgets): drop exclusive keyboard focus during screenshot (#3787) 2026-10-15 00:19:43 -04:00
purian23 3c13700fc5 feat(niri): add toggle functionality & OSD for touchpad control 2026-10-14 17:42:27 -04:00
Huỳnh Thiện Lộc 8ff7ce2cea feat(island): add system tray slot and overflow menu to compact home (#3780)
* feat(island): add system tray slot and overflow menu to compact home

- Add system tray slot and overflow expander menu in Dynamic Island
- Share tray icon path resolution and DBus context menu fallback in TrayMenuManager
- Support edge-anchored context menu positioning across top, bottom, left, and right
- Lock bottom and edge anchoring in DContextMenu to prevent animation jitter

* fix(island): only instantiate tray menus when tray slot is enabled

* feat(island): expand tray item and caret click area to slot height

* fix(island): use Theme.surfaceTextHover and restore DContextMenu height behavior

* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island

* Revert "refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island"

This reverts commit 29888f89cd.

* refactor(tray): use shared TrayMenuManager helpers and TrayItemIcon, support tray order and IPC in island

* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid

* feat(island): support tray item drag and drop reordering

* fix(island): prevent re-entrant tray drag reordering on model destruction

* fix(tray): remove native submenu anchor hydration to prevent popup stutter

* fix(tray): decouple submenu hydrator from anchor window to avoid stutter

* fix(tray): restore anchor window on submenu hydrator and disable on island

* tray: fix bar overflow surface & reuse max visible items on the island
2026-10-14 16:55:26 -04:00
Mario Mohar 699eb23775 color picker: remove recent colors with a hover button, delete or backspace (#3764)
* color picker: remove recent colors with right-click or delete

* color picker: remove recent colors with a hover button, delete or backspace
2026-10-13 21:06:02 -04:00
bbedward 57cfb5e493 bar: keep shared corners on the horizontal bar regardless of map order
fixes #3770
2026-10-13 20:58:51 -04:00
Bernardo Gomes 66ffc133ca plugins: only flag updates when remote head is not contained in local history (#3778)
* plugins: only flag updates when remote head is not contained in local history

* meta: remove inline comments
2026-10-13 20:52:04 -04:00
purian23 35041d03e1 island: honor launcher unload on close 2026-10-13 19:35:12 -04:00
purian23 c18d989fc5 refactor(dock): restore launcher removal in apps dock widget, unify config & visual effects options 2026-10-13 18:22:45 -04:00
purian23 39b41fb880 bars: update island & dbar defaults 2026-10-13 17:31:07 -04:00
purian23 12feb8068b island: center align satellites with the island instance 2026-10-13 16:09:03 -04:00
purian23 e23bd4073f island: restore normal control center height until edit mode 2026-10-13 16:03:25 -04:00
Bernardo Gomes f03ae838c9 doctor: warn when a qt platform theme plugin targets another qt minor (#3771)
Qt skips QPA plugins built for a different major.minor without any
visible error. Out-of-tree plugins such as the AUR qt6ct-kde keep the
file in place after a Qt update, so the check reported them as
installed while Qt apps and tray icons lost their theme. Compare the
version in the plugin's .note.qt.metadata ELF note with the installed
Qt and warn on a mismatch.
2026-10-13 15:15:13 -04:00
bbedward f4e3697803 bar: center apps dock icons and separator on the bar
fixes #3776
2026-10-13 13:49:03 -04:00
bbedward 5f31b2e3d5 lock: start the pam conversation on submit, not on lock
fixes #3775
2026-10-13 12:43:21 -04:00
bbedward ba8c4e60a1 lock: hold the fingerprint scan from a sleep request until PrepareForSleep
fixes #3774
2026-10-13 10:18:12 -04:00
purian23 c3e93ee39a feat(settings): syntax highlight built-in upgrade logs & offer reboot on kernel updates 2026-10-12 23:59:35 -04:00
Huỳnh Thiện Lộc d8a4ffe47b feat(bar): support auto-hide for island in overlay mode (#3772)
* feat(bar): support auto-hide for island in overlay mode

* fix(settings): restore selectedIslandDocked scope for overlay islands

* fix(bar): keep edge hover strip while island auto-hide is revealed
2026-10-12 23:20:38 -04:00
purian23 eda2f4459b refactor(island): performance & stability improvements
- update qml-common
2026-10-12 23:13:42 -04:00
bbedward d041ffccb0 cc: remove border in edit mode 2026-10-12 15:50:50 -04:00
bbedward 14de920cd2 idle: wake monitors on input whenever dms powered them off
fixes #3742
2026-10-12 15:35:08 -04:00
Alexander 80711f1c21 systemd: use explicit shell readiness (#3514) 2026-10-12 14:33:44 -04:00
bbedward 593046064b icc: keep profiles and assignments in the dms config dir 2026-10-12 14:25:09 -04:00
Marlon Kranz 4a8f1062be island: open media and the power menu in one step (#3756)
* island: open media and the power menu in one step, fix dash edits

- Clicking media on the home face opens the player. Closing a sheet
  goes back to the face the user picked, and an opt-in setting
  switches to media when playback starts.
- The power menu can open in the island (Super+X, the dock button),
  with its own route row in the island settings.
- An island collapses when another popout opens on its screen.
- The widgets page hides the center section the island replaces.
- Dragging or resizing a dashboard card no longer blanks the overview:
  Qt hands the committed list over as a non-array, which read as empty.
- The island blur region stays inside the rounded fill.

* fix(island): address review feedback on switching faces by mouse

Opening the player with one click removed the only mouse path from
the home face to the media face. A right-click on the collapsed island
now swaps between the two while media is available.

The swap resizes the island under the pointer, so hover-open stays off
until the pointer leaves. Without a face to swap to, the press is not
accepted and reaches the bar's own right-click action as before.
2026-10-12 12:52:12 -04:00
bbedward e5bb8f610a cc: adjust spacing of half height rows 2026-10-12 12:50:34 -04:00
dms-ci[bot] 0699bc1c97 nix: sync flake.lock to dank-qml-common submodule 2026-10-12 16:00:59 +00:00
bbedward 9433bf7500 i18n: sync 2026-10-12 12:00:27 -04:00
93bd3eecd1 notifications: allow extra conditions per rule (#3683)
* notifications: allow extra conditions per rule

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* notifications: add translator context to the rule condition strings

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* don't use deprecated widgets

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bbedward <[email protected]>
2026-10-12 11:52:59 -04:00
bbedward 566ccf2fde ci: add dfiles to release fanout, run nix sync jobs on stable branches 2026-10-12 11:45:27 -04:00
bbedward eefc823de3 ci: add deprecated widget check to review workflow 2026-10-12 11:43:02 -04:00
Kadir 8cf89f17c0 session: end the logind session on logout when the compositor has no quit request (#3700) 2026-10-12 11:26:27 -04:00
Kadir 718ba6b118 workspace rename: prefill the focused workspace name on hyprland (#3698)
The rename IPC always read the name from NiriService, so on Hyprland the
field opened empty. Hyprland names are stored as "<id> <name>", so the
prefill drops that prefix to keep a second rename from stacking it.
2026-10-12 11:23:00 -04:00
bbedward 3ff724e4ea bluetooth: treat alias-is-address as the only unnamed case when sorting scan results
fixes #3761
2026-10-12 11:17:54 -04:00
hkoeck 10eb5f6ec3 lock: show the PAM prompt and track the conversation state (#3691)
Pam.qml holds the prompt text PAM asked for and separates "a response is
in flight" from "the conversation is open", so the UI stops treating an
open conversation as busy. The lock screen shows the prompt instead of a
hardcoded "Password", and clears the field when a new prompt arrives.
2026-10-12 11:16:18 -04:00
a8bb04a2b2 displays: send the vrr setting on the wlr-output path (#3701)
* displays: send the vrr setting on the wlr-output path

The toggle shown on compositors without their own output writer (sway,
labwc, ...) was dropped when the heads were built, so it never applied.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* remove test

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bbedward <[email protected]>
2026-10-12 11:13:45 -04:00
Meng fb1686888d fix(network): store pending PSK in IWD ConnectWiFi to prevent double password prompt (#3743) 2026-10-12 11:11:33 -04:00
bbedward 3e5cfe650c settings: keep hover and click on the row header once a body is open
settings: fix greeter FAB button
desktop: change styling of edit menu

fixes #3767
2026-10-12 11:02:38 -04:00
purian23 ffcaad73b8 desktop menu: slight design tweak 2026-10-12 03:22:59 -04:00
Mario Mohar 0fb2297c15 processlist: search every process, not just the top 100 by cpu (#3763)
* processlist: search every process, not just the top 100 by cpu

* processlist: drop the process search test
2026-10-12 02:47:23 -04:00
purian23 ce389d2e87 feat(input): focus follows mouse on niri and mango 2026-10-12 02:27:21 -04:00
Perdixky 80ca0f0ee5 hyprland: add focus follows mouse toggle (#3755) 2026-10-12 02:15:46 -04:00
Huỳnh Thiện Lộc 132dca18ae fix(control-center): handle escape shortcut at window scope (#3762)
* fix(control-center): handle escape shortcut at window scope

* fix(control-center): dismiss selector sheets before detail navigation on escape
2026-10-12 01:25:25 -04:00
purian23 4eda5ccd8f control center: the final battle test qol update 2026-10-12 01:17:18 -04:00
Marlon Kranz 6640a943b3 doctor: fix the qtengine and xdg menu prefix checks (#3757)
qtengine had its own plugin check, which printed its rows at the end of
the Environment section instead of under QT_QPA_PLATFORMTHEME, with
different names and messages than qt6ct or kde.

Fold it into the shared platform theme check: the Qt6 plugin is always
checked, the Qt5 plugin too when QT_QPA_PLATFORMTHEME names qtengine and
a Qt5 build tool can be asked. Also add a row for
~/.config/qtengine/config.json, which is what makes Qt apps follow the
DMS colors, and say in verbose mode that an unset
QT_QPA_PLATFORMTHEME_QT6 falls back to QT_QPA_PLATFORMTHEME.

The XDG_MENU_PREFIX check only looked in /etc/xdg/menus, so a menu file
in ~/.config/menus, which the menu spec searches first, was reported as
missing. Search the user config dir and every XDG_CONFIG_DIRS entry.
2026-10-11 16:47:41 -04:00
cmleon51 36ff537e9a settings: explain why the autostart settings page is empty (#3754)
* settings: explain why the autostart settings page is empty

Fixed an issue where the autostart apps page was blank if
"xdg-desktop-autostart" was inactive.

The page was still reachable through the search function but not present
in the "Applicatiosn" settings sidebar tab.

* settings: point unavailable autostart applications to new docs section
2026-10-11 16:46:42 -04:00
lung595 846a0ff22f plugins: ranking module and directory metadata for the plugin browser (1/3) (#3751)
* plugins: forward directory metadata (version, icon, permissions, dates) to the shell

* plugins: ranking module for recommendations, trending and picks

* plugins: mark new and recently updated plugins on the browser cards, keep badges clear of the vote count

* i18n: allow the lowercase plugin recency badge next to the New action
2026-10-11 16:45:25 -04:00
bbedward 40b6eaa4fe notifications: pin hovered popups to their slot
fixes #3758
2026-10-11 16:44:50 -04:00
purian23 60736a7381 feat(settings): switch panes with Alt+Arrow keys 2026-10-11 12:34:19 -04:00
purian23 1966c8760d fix(audio): manage sink volume writes & prevent overwrites during adjustments
Closes #2060
2026-10-11 12:18:34 -04:00
bbedward b988cb14b7 cc: don't resize during tile drags and center trash in footer 2026-10-11 11:49:41 -04:00
purian23 002ab6592d settings: fix collapsible header focus ring and fill to the column 2026-10-11 03:58:39 -04:00
purian23 adaa5e2bb0 fix(clipboard): multi-second tab switch bind & stale pinned count
- qml common fix & update
2026-10-11 03:39:24 -04:00
purian23 a1e6f6d6ac refactor(binds): updated dms setup defautls for each supported compositor 2026-10-11 00:54:55 -04:00
purian23 3a48f59067 refactor(desktop): updated design & options on right-click menu 2026-10-10 22:18:31 -04:00
purian23 f372777829 cc: implement undo functionality ctrl+z & drop rings w/misc qol 2026-10-10 19:08:05 -04:00
bbedward d7d7f73420 cc: fix rogue tooltip 2026-10-10 18:17:44 -04:00
bbedward f1ea842bca cc: don't shift edit button slot, bump qml-common 2026-10-10 18:14:12 -04:00
bbedward 2cc720cf2d control center: battery styles on every tile size 2026-10-10 18:00:39 -04:00
bbedward d8aca6ab48 grids: hand committed layouts and display picks to callbacks, not var signals
fixes #3746
2026-10-10 18:00:39 -04:00
lung595 3246138096 plugins: wrap long ToggleSetting labels instead of drawing under the toggle (#3744)
The label had no width, so a long one kept its natural width and drew
under the toggle in a narrow settings panel. Give it the column width,
like the description below it; StyledText already wraps by default.
2026-10-10 17:52:04 -04:00
cmleon51 6f0674ff72 popout: let the content window take hyprland focus before whitelisting bars (#3745) 2026-10-10 17:51:43 -04:00
bbedward 3e2d969e5d keybinds: set topMargin on cheatsheet
fixes #3740
2026-10-10 11:55:40 -04:00
bbedward 583333dfb5 matugen: drop stale preview result when the key returns to the loaded one
fixes #3741
2026-10-10 11:45:07 -04:00
bbedward 5193fcd473 media: queue album art for the quantizer 2026-10-10 11:35:40 -04:00
bbedward 68043277c6 sysupdate: only select the pkg backend on freebsd
clipboard: refuse copies when tracking is disabled so the cli falls back locally
2026-10-10 11:29:05 -04:00
bbedward b0b14700e8 desktop widgets: fix editor 2026-10-10 11:19:45 -04:00
Marlon Kranzandpurian23 6d331b8fde fix(settings): show what frame mode controls (#3654)
* fix(settings): show what frame mode controls

With frame mode on, many settings did nothing or were hidden while
still applying, and nothing said so. The frame takes its color and
opacity from the first non-island bar, but that bar's Surface card is
hidden while the frame styles it, so neither had a visible control.

Add Background and Override/Opacity rows to the Appearance Frame card,
bound to the same frame bar the renderer reads. Settings the frame
provides in connected mode get a "Managed by Frame in Connected Mode"
note (Interface style border, corner style, background blur and dock
opacity, shadow targets, launcher border); settings with no effect get
"Disabled by Frame Mode" (maximize detection, bar shadow, the bar's
overlay layer in separate mode). Bar size, the bar's overlay layer in
connected mode, dock Over fullscreen and the controls of overlay-layer
docks and launchers are shown again, since they still apply.
Notification Over fullscreen shows as forced on when the frame covers
every display, and launcher rows the Spotlight style ignores are
disabled.

Also relabel the frame Width row as Thickness, raise the inset padding
maximum to its automatic range, and drop a FrameWindow handler for the
removed frameOpacity signal. No new strings.

* settings: show what frame mode controls, fix review findings

---------

Co-authored-by: purian23 <[email protected]>
2026-10-10 00:37:54 -04:00
Marlon Kranzandpurian23 0444835016 fix(frame): make connected mode honor the settings it overrides (#3665)
* fix(frame): make connected mode honor the settings it overrides

Several frame-mode behaviors contradicted what the settings UI shows:

- Frame -> Blur gated blur for every shell surface in connected mode,
  including the Settings window, polkit, context menus, OSDs and toasts.
  WindowBlur now defaults to Background blur; only surfaces painted
  with the frame color (connected popouts, slideouts, Control Center)
  follow Frame -> Blur, and frame-owned surfaces are blurred by the
  frame as before.
- The notepad slideout painted its own fill at the notepad
  transparency. On screens where the frame owns connected chrome it now
  claims a frame surface (new connected_arc slot 5), so the frame paints
  it flush with the cutout, with fillets, and follows the slide.
- Notification popups painted a card surface on top of the frame body,
  stacking fills. That layer is transparent when the frame paints it.
- Connected mode stripped shadow, border and corner styling from every
  bar, including bars on displays without a frame, and hid the rows.
  Styling is now stripped and restored per bar based on whether it
  covers a frame display, re-checked on screen, frame display and
  display-name-mode changes.
- Auto popup gaps and Gap size were hidden in separate mode although
  they still apply there; they now hide only in connected mode.
- The launcher style picker was hidden in connected mode while Island
  still overrode the connected launcher. Connected mode now always uses
  the connected launcher; the picker shows Full, disabled, with a note
  linking to the Bar layout picker.
- The frame shadow ignored Shadows -> Apply to -> Bars.

* fix(frame): address review feedback on connected mode

- Skip bar style reconciliation while the screen list is empty, so a
  hotplug doesn't restore and re-strip every bar.
- Replace the 16 copies of the connector blur anchor and cutout in
  FrameWindow with a ConnectorBlurRegion inline component.
- Let connected mode use the island launcher again now that the island
  can sit in any bar. The picker offers Full and Island there and hides
  Spotlight, which opens the full launcher in that mode anyway; the note
  and the SettingsControlledBy bar target are dropped.

* frame: address review findings on connected mode

---------

Co-authored-by: purian23 <[email protected]>
2026-10-10 00:04:17 -04:00
purian23 938971e758 (settings): moar cleanup work on inactive sliders 2026-10-09 22:59:31 -04:00
Huỳnh Thiện Lộc 400f62316c feat(wallpaper): allow recursive cycling through subdirectories (#3739)
* feat(wallpaper): allow recursive cycling through subdirectories

* fix(wallpaper): persist root folder when recursively cycling without folder set
2026-10-09 22:53:29 -04:00
purian23 be26a06c2a feat(dbar): freedom to remove the only bar with hint warnings 2026-10-09 22:13:45 -04:00
purian23 3aa8cd91e8 feat(control center): remaster attempt #23 - new quick tiles widget option for smaller drag & drop targets 2026-10-09 21:32:57 -04:00
purian23 26301462b7 feat(settings): add dock jump shortcuts & ipc commands for settings categories 2026-10-09 20:33:53 -04:00
purian23 8163b4654b ubuntu: bundle Go toolchain for Launchpad environment 2026-10-09 20:08:32 -04:00
bbedward 834806a9ce bump qml-common 2026-10-09 17:37:43 -04:00
dms-ci[bot] e60b038f09 nix: sync flake.lock to dank-qml-common submodule 2026-10-09 20:21:36 +00:00
bbedward d0f9954361 settings: move hero to qml-common 2026-10-09 16:20:59 -04:00
bbedward f988a57494 settings: some mild refactors based on feedback 2026-10-09 16:11:53 -04:00
Mario Mohar e2a0b37d03 notifications: add invokeLast ipc to run the newest notification's default action (#3734)
* notifications: add invokeLast ipc to run the newest notification's default action

* notifications: drop the invokeLast test file
2026-10-09 15:29:21 -04:00
bbedward aef801fc4b wallpaper: import material layouts, dms://wallpaper/install uri, profiles 2026-10-09 14:51:07 -04:00
bbedward 534913dc0b wheel: use WheelInput horizontal helper, make default action cycle
window on WS pill
2026-10-09 12:45:27 -04:00
bbedward a0ea04178f charts: swallow shift+wheel at the scroll bounds
fixes #3736
2026-10-09 12:22:53 -04:00
bbedward c417cd10ea modal: forward unhandled content keys to the modal focus scopes
fixes #3735
2026-10-09 12:06:12 -04:00
bbedward 4dd159c7b2 popout: keep the deferred surface show alive under a disabled bar widget
fixes #3728
2026-10-09 11:38:39 -04:00
bbedward 5113441810 core: bump to go 1.27, tailscale client 1.104, cut tests
fixes #3737
2026-10-09 11:11:07 -04:00
purian23 e10102edc0 feat(mango): native mouse/touchpad & keyboard input support
- Hide blur toggle options until mango support is added
2026-10-09 05:55:47 -04:00
purian23 ea68399ef3 feat(blur): add configurable blur strength slider in all supported compositors
- Updated Hyprland defaults
2026-10-09 05:19:46 -04:00
purian23 2747a75198 hyprland: bring up features parity overhaul w/0.56+ and upcoming stable releases
- new blur variants, glow, wobble, motion blur and groupbar support
- new inputs support for mouse, touchpad and keyboard settings
- greeter, cheatsheet and scroll capture dispatch in Lua
- scrolling workspace screenshot focus fix
- fix scrolling screenshots from being cut-off on fractional-scale displays
- workspaces are keyed by address on the beta branch and upcoming stable release where named workspaces have no id
- bind flags, per-device binds, new window-rule keys and effects
- outputs default to preferred/auto/auto and gate HDR/VRR on hardwareDetails
- new dms setup/installer systemd session support on hyprland-session.target
2026-10-09 05:14:55 -04:00
Mario Mohar f9574ef9ae plugins: write the first state save after a reload (#3733) 2026-10-09 04:20:19 -04:00
purian23 39b0d40877 mango: bring up newly supported config dialects, systemd sessions & setups
- Auto detect the snake_case dialect of Mango master from the binary, write configs and respell existing fragments with:
 `dms config mango-migrate`
- Setup backs up and replaces config.conf, moves binds and rules into dms/
- Start DMS from mango-session.target with systemd or exec-once without
- fix cheatsheet run action and bind precedence, window-rule and display save updates
- Support urgent active tag, greeter loading  /etc/mango, per-screen overviews
2026-10-08 20:50:53 -04:00
Ignacy Pękała b53f7c0633 settings: add support for multiple config files (#3425)
* settings: add support for multiple settings files

PR: #3425

Introduces a drop-in settings files drectory - config.d/.
SettingsStore.toJson preserves settings set in any file (even set to
default values).

* Unload settingsFolderModel if config.d doesn't exist
2026-10-08 18:55:55 -04:00
bbedward 7f2ab8e012 ci: diff PRs with git instead of gh in review 2026-10-08 17:36:31 -04:00
Mario Mohar b55d5b428b launcher: stop treating BAMF_DESKTOP_FILE_HINT as a snap marker (#3729) 2026-10-08 17:10:32 -04:00
Mario Mohar 1dc0790d77 clipboard: skip the clear confirmation when there is nothing to clear (#3730) 2026-10-08 17:08:43 -04:00
bbedward 9073de5286 bar/kblayout: consistent width 2026-10-08 17:07:00 -04:00
dms-ci[bot] e985408a2e nix: sync flake.lock to dank-qml-common submodule 2026-10-08 20:44:38 +00:00
bbedward 5f9f66ca59 shell: fix lints 2026-10-08 16:44:10 -04:00
bbedward b938dd76f0 theme: use blend helpers from qml-common 2026-10-08 16:20:30 -04:00
bbedward 474b723faf keybinds: capture pressed shortcuts in the search fields
settings: add a cheatsheet button to the keyboard shortcuts page

fixes #3631
fixes #3632
2026-10-08 16:03:55 -04:00
bbedward f19632eba5 config/kitty: set remember_window_size no in default dms config
ref #3719
2026-10-08 14:03:30 -04:00
bbedward 2c77b9d477 setup: resolve bind terminal from $TERMINAL before the ghostty default
fixes #3653
2026-10-08 13:57:20 -04:00
bbedward fa6927de3b bump qml-common 2026-10-08 13:00:36 -04:00
bbedward 1a0e4b9979 settings: tweak sidebar shadow style to work better with opacity 2026-10-08 12:57:09 -04:00
bbedward f61a5d1211 lists: restore faded edges everywhere 2026-10-08 12:26:13 -04:00
Mario Mohar 1d120f94bf fix(wallpaper): keep rendering until the scrolling image has decoded (#3725)
In scroll fill mode the parallax layer decodes its own copy of the
wallpaper (parallaxImage, asynchronous). renderActive only waited for
currentWallpaper and nextWallpaper, so after a wallpaper change it could
drop to false and let updatesEnabled run out after the three settle
frames while parallaxImage was still loading. The window then kept
showing the black backing until something else, like a workspace
switch, started rendering again.

Count parallaxImage loading as active too. When it turns Ready,
renderActive flips back and onRenderActiveChanged invalidates, so the
finished image gets its settle frames.

Fixes #3702. Diagnosis and fix by @clethbridgedaniel in the issue.
2026-10-08 11:33:25 -04:00
Alexander 97004ffd6e niri: keep workspace focus in sync with niri (#3713) 2026-10-08 11:06:50 -04:00
bbedward aa5610dd49 matugen: do not dbus-activate fcitx5 when reloading its theme 2026-10-08 10:49:00 -04:00
bbedward 2fd472b89a launcher/notifications: drop view transitions, fade rows via DListView.fadeIn 2026-10-08 10:47:43 -04:00
bbedward c1006f6b34 audio: wait for the sound theme before playing any event sound
ref #3723
2026-10-08 10:04:28 -04:00
bbedward 8b47d3cf70 brightness: map a dp connector's ddc bus as well as its aux bus
ref #3724
port 1.6
2026-10-08 09:34:01 -04:00
bbedward 2bf7f7c556 cups: fix 24.20 compatibility
fixes #3721
2026-10-08 09:22:47 -04:00
bbedward 4dc45185ba notifications: add 2 finger swipe gesture to dismiss on popups
fixes #3716
2026-10-08 09:05:07 -04:00
purian23 c0f08f08cb refactor(island/settings): update notch mode & radius defaults
- Merge island satellites and standard bar goth corner options in shared code
- Update bar settings to collapse inactive sliders
2026-10-08 03:44:42 -04:00
purian23 f9d91cd21f feat(island): implement notch mode variant 2026-10-07 21:55:07 -04:00
bbedward b1dde74e4d settings: improve transition behavior 2026-10-07 19:22:24 -04:00
bbedward f2c06a3cc7 settings: add WheelHandler above Flickable 2026-10-07 16:14:47 -04:00
bbedward ed43b07d5e settings/sidebar: shrink avatar in search a bit and make accent ring off
by default and actually work when disabled
2026-10-07 15:55:58 -04:00
bbedward 4b0dfa67a0 desktop: add right click context menu for actions on compositors where
it doesnt interfere, with a setting to turn it off

Some compositors like kwin/labwc/sway cannot have this without breaking
some built in layering they do.

niri can have this, but for focus follow mouse it may need to be disabled.

fixes #3717
2026-10-07 15:18:39 -04:00
Marlon Kranz 72c23161e7 fix(hyprland): write window rules Hyprland 0.56 accepts, keep DMS windows opaque (#3655)
* fix(hyprland): write window rules Hyprland 0.56 accepts, keep DMS windows opaque

The Lua window-rule writer emitted match keys and effects Hyprland 0.56
does not know: `floating`/`pinned`/`initialised` as match keys and
`noborder`/`norounding` as effects. It now writes `float`/`pin` and
`border_size = 0`/`rounding = 0`; the parser still reads the old
spellings, so existing files load and are rewritten on the next save.
"No Rounding" now suppresses the corner radius so a rule has one
`rounding` key.

The rule editor no longer offers fields the Hyprland writer drops
(Window Opening output/workspace, Size Constraints, the Initialised
match), and Save is disabled while only one half of a move or size
pair is filled. Every window-rule write now reloads the compositor
through one helper, which also covers Hyprland.

DMS windows draw their own translucent background, so any compositor
opacity rule or decoration opacity multiplied on top of it. DMS now
keeps a hidden rule for its own app id: `opaque = true` on Hyprland
(applied after rule and decoration opacity, independent of order, and
leaving the window's own alpha alone) and `opacity 1.0` on niri, which
the store keeps last. It is re-added on start when missing and hidden
from the rules list and the focused-window menu.

Compositor layout gains a Hyprland "Window opacity" slider writing
decoration.active_opacity/inactive_opacity; 100 leaves the user's own
config in charge.

HyprlandService.reloadConfig queues callbacks: Proc keeps one callback
per id, so a reload requested while the layout writer's reload was
pending dropped its callback and left layout generation stuck.

* fix(windowrules): cap corner radius at 20

Hyprland 0.56's Lua window_rule rejects rounding above 20 ("value 24
is more than the maximum of 20") and the rule loses its rounding. The
Hyprland writer now clamps to 20, and the editor slider stops at 20
for every compositor.
2026-10-07 14:09:49 -04:00
Marlon Kranz 7c6e9ed514 fix(screenshot): copy a saved screenshot as a file, not only image bytes (#3667)
dms screenshot put only image/png on the clipboard. Terminals can only
paste text, so pasting a screenshot into a terminal app (e.g. a CLI
that attaches images from a pasted path) did nothing, while a file
copied through the server's clipboard.copyFile pastes its path.

When the screenshot is saved to a file, copy it the same way:
gnome-copied-files, text/uri-list, the path as text/plain, and the
image bytes. The server is asked first so the copy lands in history;
without it, the same offers are served locally. The offer list is
shared with Manager.CopyFile through clipboard.FileOffers.

copyMultiFork now waits for the child to take the selection, like
copyForkCached, so a failed local copy is reported instead of lost.
dms cl copy --download shares this path and now falls back to a local
copy when the server is down or rejects the file. --no-file and PPM
keep the image-only copy.
2026-10-07 14:05:11 -04:00
Spyros Psarras 66e37ff843 network monitor: add hide when idle and compact mode options (#3569)
* network monitor: add hide when idle and compact mode options

* network monitor: keep the idle widget out of center layout spacing
2026-10-07 13:59:21 -04:00
bbedward 6f43ece99f control center: let grid drops land where dropped 2026-10-07 13:45:21 -04:00
bbedward e9f66d6256 input: use WheelInput for touchpad detection, bump dank-qml-common
fixes QT 6.12 change with pixelDelta being present on mouse wheels
2026-10-07 12:28:06 -04:00
bbedward b42fe6643f perf: gate idle work behind feature and capability state 2026-10-07 11:55:29 -04:00
Artem Timofeev 8fedfae1dc fix(power): reconcile refresh rate after resume (#3459) 2026-10-07 11:17:12 -04:00
bbedward a8146d2ca5 bar: shrink flexible widgets before moving anything into overflow 2026-10-07 11:05:08 -04:00
Lars Artmannandbbedward 8e59341c50 feat(notifications): timed per-app popup mutes (1h / 4h) (#3628)
* feat(notifications): timed per-app popup mutes (1h / 4h) with expiry

* fix test assertions

* notifications: let a timed mute expire without taking the rule's other settings

---------

Co-authored-by: bbedward <[email protected]>
2026-10-07 10:25:38 -04:00
purian23 650b731164 feat(island): new workspace switcher activity 2026-10-07 03:20:25 -04:00
bbedward 07740cc2d5 bar: clear sticky reveal on bars without auto-hide
fixes #3714
2026-10-06 23:10:11 -04:00
dms-ci[bot] 7ffd640ef9 nix: sync flake.lock to dank-qml-common submodule 2026-10-07 02:58:34 +00:00
bbedward 98bf56cbcd windows: drop the qt decoration env, add a title bars toggle and go frameless for own chrome
fixes #3679
fixes #3572
2026-10-06 22:57:21 -04:00
bbedward 8299d8fa77 bump qml-common 2026-10-06 21:41:32 -04:00
bbedward fd52e0af59 qs: add DankBar and DankDash compatibility shims 2026-10-06 21:07:11 -04:00
purian23 5eb78f1f12 notifications: restore hover to reveal closure & expansion buttons 2026-10-06 20:04:20 -04:00
bbedward 769e4455f8 settings: change page transition 2026-10-06 19:12:48 -04:00
purian23 54eb0ccaed plugins: stipe dupe plugin previews & add fallback support 2026-10-06 17:57:16 -04:00
bbedward 089dc98166 network: set routing preference only from settings
fixes #3068
2026-10-06 15:02:12 -04:00
bbedward d838d15e69 cc: replace right click action with long press
fixes #3709
2026-10-06 14:44:15 -04:00
Pedro Jose Soto Rivera f886789835 network(networkd): treat bridge, bond, team and vlan links as wired (#3706)
* network(networkd): treat bridge, bond, team and vlan links as wired

When the IP configuration lives on a bridge or link aggregate, the
enslaved member NIC reports opState=enslaved and the aggregate itself
was dropped by isWired() because only Type=ether was accepted, so the
widget showed Disconnected. Accept the same link types NetworkManager
already handles (#1581).

Also treat lxdbr*/incusbr* as virtual so container bridges, now of a
wired type, are not picked as the uplink.

Fixes #3463

* network(networkd): only treat a bridge as wired if it has a physical member

Name prefixes can't cover every container/VM bridge (lxcbr0, waydroid0,
...). Check the bridge's members in /sys/class/net/<br>/brif instead and
only accept it as the uplink when at least one member is not virtual
(veth*, tap*, ...). This replaces the lxdbr*/incusbr* prefixes.
2026-10-06 14:43:57 -04:00
bbedward 9cceff29dd default apps: disambiguate apps with the same name in desktop entry and
show icon
2026-10-06 11:28:35 -04:00
bbedward 507b818a55 config: drop Before=graphical-session.target from the generated hyprland target 2026-10-06 11:05:01 -04:00
bbedward 069fa318d6 island: fix pill jumping sideways when the compact face switches
fixes #3668
2026-10-06 11:02:32 -04:00
bbedward 766708c551 core: bump dankgo 2026-10-06 10:38:01 -04:00
bbedward ebf9c5ba7b lock: keep the ime commit sink off for in-process input method modules
ref #3669
2026-10-06 10:11:16 -04:00
bbedward 5ee570557f cli/notify: add priority options 2026-10-06 10:10:42 -04:00
bbedward ca93837149 lock: allow anchoring widgets to center, x, and y axises 2026-10-06 09:05:57 -04:00
Isa Al Ula 7b144ddac1 app picker: strip desktop suffix when resolving mime handlers (#3710) 2026-10-06 08:25:55 -04:00
purian23 9d938cfed0 settings: collapse sliders when inactive 2026-10-06 03:42:55 -04:00
purian23 1276418f6e settings: compact toggle/slider design updates & interface organization 2026-10-06 03:24:58 -04:00
Aldair Meneses c8d9a8617e bar: add configurable middle and right click actions on empty space (#3582)
Revives PR 2710 using in-process PopoutService toggles per review, extended
with a right click action and a follow-mouse option for anchored popouts.
2026-10-06 02:11:21 -04:00
bbedward 4265ea1620 freebsd: gate audio and battery polling on consumer refs instead of popout bindings 2026-10-05 22:23:34 -04:00
Pedro Jose Soto Rivera ff50b492b1 network(iwd): follow the Wi-Fi device when iwd recreates it (#3707)
When iwd restarts it recreates wlan0 under a new D-Bus path. Only the
Station interface was tracked on InterfacesAdded, so devicePath kept
pointing at the dead object: the PropertiesChanged match for the new
device was never registered, updateState() read Powered from the old
path, and SetWiFiEnabled() failed with "No matching method found".
WiFiEnabled stayed false after handleStationRemoved() until DMS was
restarted.

Handle the Device interface on InterfacesAdded/InterfacesRemoved: move
devicePath and its signal match to the new object and take Name and
Powered from the signal. Process every interface in an
InterfacesRemoved signal instead of stopping at the first match.

Refs #3495
2026-10-05 22:23:01 -04:00
bbedward b81373aae8 auth: use the expressive text field for polkit, network prompts, greeter and settings password fields 2026-10-05 22:09:37 -04:00
Zer0TraceXandclaude[bot] 87a4da9e13 feat(freebsd): Add initial native FreeBSD support (#3559)
* feat(freebsd): add comprehensive native FreeBSD support

* Update BatteryService.qml

* Add battery probe completion and fallback updates

* Add isVideoSource function and update PwObjectTracker filter

* Add isPipewireVideoSource function to AudioService

Added a function to check if a Pipewire node is a video source.

* Refactor video source check to use AudioService

* Remove unnecessary call in BatteryService.qml

* Add call to updateFreeBsdBatteryFallback on completion

* Simplify AudioService by eliminating redundant bindings

Removed unnecessary bindings in the AudioService loader to streamline the loading process.

* Improve error handling in CheckUpdates function

Handle specific exit error for package upgrade check.

* Enhance FreeBSD network detection logic

* Implement FreeBSD Wi-Fi hardware discovery in Initialize

Added FreeBSD Wi-Fi hardware discovery and updated initialization logic to handle cases where no wpa_supplicant control socket is found.

* Implement Ethernet state monitoring in StartMonitoring

Added monitoring for Ethernet state updates in StartMonitoring function.

* Add Ethernet management functions to backend

* Remove unimplemented Ethernet control methods

Removed unimplemented Ethernet control methods from WpaSupplicantBackend.

* Implement SetWiFiEnabled method for WiFi control

Add error handling and state management for WiFi control

* Update core/internal/server/network/backend_wpa_ethernet.go

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Implement Ethernet monitoring in WpaSupplicantBackend

* Refactor startEthernetMonitor and add debounce logic

Refactor the startEthernetMonitor function to improve readability and maintainability. Introduce a debounce mechanism for processing relevant FreeBSD route events.

* Reorder import statements in backend_pkg.go

* Remove unnecessary blank line in backend_wpa_ethernet.go

* Implement FreeBSD audio polling based on UI visibility

Introduce a polling mechanism for FreeBSD audio device updates based on visibility of UI elements.

* Add readonly freeBsdPollingRequested property and optimize polling

Introduce a readonly property to determine if FreeBSD polling is requested based on various UI conditions. Update command execution to prioritize polling efficiency.

* Simplify freeBsdPollingRequested logic

Removed unnecessary conditions from freeBsdPollingRequested property.

* Implement reference counting for battery consumers

Added reference counting to manage battery information consumers and updated related functions for better battery state handling.

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-10-05 22:03:32 -04:00
purian23 2c584c8bf7 launcher: add app editing to spotlight 2026-10-05 21:48:29 -04:00
alvarosth eeea1a633c registries: let the CLI policy turn the registries off (#3703)
* policy: load the CLI policy file from an internal package

The plugin and theme registries need to read the policy too, and
internal packages cannot import package main.

* registries: let the CLI policy turn the registries off

With disable_registries set in /usr/share/dms/cli-policy.json or
/etc/dms/cli-policy.json (the /etc file wins), the plugin and theme
registries start without sources, so nothing is cloned or pulled.
Installed plugins and themes keep working.
2026-10-05 20:41:29 -04:00
bbedward 9ec48c4f13 ui: rename internal widgets with D prefix to match qml-common change 2026-10-05 18:05:07 -04:00
bbedward f8b25bc43c launcher: add an option to disable history
widgets: delete plugin compatibility shims that arrived after 1.6
2026-10-05 16:58:39 -04:00
bbedward ccbf1d38ad widgets: bump qml-common, rename Dank* prefixed widgets to D, add
deprecation notice to plugin compatibility shims

- When qml-common became a thing, we left qs.Widgets shims in place for
  plugin compatibility
- It's annoying to need to maintain them when qml-common has any change,
  add a warning so we can safely deprecate them in the future
- One shot rename the common stuff to D to make it shorter and nicer to
  write
2026-10-05 16:47:19 -04:00
bbedward ba50299b2f dash/wellbeing: size default to other tabs, except overview 2026-10-05 15:20:55 -04:00
bbedward f6d0701505 matugen: more clear failure details 2026-10-05 15:05:25 -04:00
bbedward 4b285323a1 i18n: sync 2026-10-05 14:45:34 -04:00
bbedward 0ac7233c4d settings: one-line sidebar hints, slimmer rows
wellbeing: page through past weeks in the chart
2026-10-05 14:42:19 -04:00
euletheia 2f5a438e1e feat(gamma): add exceptions (fullscreen and excluded apps) 2026-10-05 14:01:12 -04:00
bbedward 9b7c0eddd7 greeter: own widget list, state-driven sync page, clock typography fix 2026-10-05 13:39:49 -04:00
bbedward d11075d154 i18n: sync 2026-10-05 11:27:43 -04:00
bbedward 7eee3dcbe7 window rules: percent sizing for niri column width and height, explain float default
fixes #3687
2026-10-05 09:56:35 -04:00
purian23 d8113d2a02 add(digital wellbeing): ipc targets for tracking status & limits 2026-10-05 03:00:11 -04:00
purian23 74628450fc settings: fix the dot behavior card shadowing its own store 2026-10-05 02:47:03 -04:00
KadirandClaude Opus 5.5 dfa19b9237 settings: hide include setup banners on compositors without includes (#3699)
On compositors with no include support (sway, labwc, ...) the displays page
showed a setup banner whose fix button can never succeed.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-05 00:49:54 -04:00
KadirandClaude Opus 5.5 131906d6a5 app drawer: open the context menu at the click position (#3697)
The drawer's modal adapter lacked alignedX/alignedY/effectiveScreen, so
LauncherContextMenu treated the offsets as 0 and opened the menu near the
top-left of the screen.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-05 00:47:18 -04:00
purian23 f4785763a1 polkit: size the auth dialog from theme tokens & use default icon 2026-10-05 00:39:36 -04:00
purian23 5972dd188b refactor(workspaces): retain pill & dot shapes w/index numbers 2026-10-05 00:22:05 -04:00
dms-ci[bot] 1f8e7a69a0 nix: sync flake.lock to dank-qml-common submodule 2026-10-05 03:00:43 +00:00
bbedward 235297898b core: bump go-udev to 0.10.0
fixes #3692
2026-10-04 23:00:20 -04:00
bbedward 9d3599c51a lock: present the first frame complete 2026-10-04 23:00:20 -04:00
Stephanos Kamprogiannis 002c4eb549 notepad: preserve notes across repeated saves (#3684) 2026-10-04 22:24:36 -04:00
bbedward 076288604e widgets: share the lock editor with the desktop, adjust overlapping
face, share desktop clock widget with lock screen
2026-10-04 22:23:14 -04:00
wowo101andClaude Opus 5.5 e26d0294c8 clipboard: restore copied folders from history (#3695)
A directory's stat size passes the MaxEntrySize check, so CopyFile tried to
read it and every folder restore failed with "is a directory". Skip the read
for directories; the uri-list and plain path offers already cover them.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-04 22:21:49 -04:00
alvarosth 4bd3e091fe lock: take the lock back when the shell restarts into a locked session (#3685)
* lock: take the lock back when the shell restarts into a locked session

Check the state once when the module completes, on locked or lockedHint,
and handle it the way onSessionLocked does, including the wait while the
session is inactive. After a logind Unlock that the shell ignored for a
lock it started, only lockedHint still says locked; and the module loads
asynchronously, so the state can arrive before its Connections exist.

* loginctl: read LockedHint from logind on unlock

logind's Unlock signal does not clear LockedHint, only the locker does.
Forcing it false told a shell restarted in that window that the session
was no longer locked.
2026-10-04 22:17:06 -04:00
Nat Fletcherandclaude[bot] 30dc1122c5 added launcher dmenu mode (#3639)
* added launcher dmenu mode

* fixed prechecker issues

* added unit tests for dms dmenu command

* added workaround for matugen printing error message to stdout

* correct help message: matching is case-*in*sensitive

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* no longer save dmenu selections to history; now properly close dmenu sockets

* increased parity with rofi -dmenu flags; added -i as an ignored flag to match dmenu

* go mod tidied

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-10-04 22:16:40 -04:00
Huỳnh Thiện Lộc d2942a4dbf feat(dock): toggle compositor overview on launcher button right-click (#3694) 2026-10-04 21:45:39 -04:00
apollo79 2b01926e31 settings: add a link to the source code in plugin details (#3690)
* settings: add a link to the source code in plugin details

* style: remove unnecessary whitespace, add empty line

* show link to the plugin source instead of explanation

* fix: check for empty string instead of undefined
2026-10-04 21:36:37 -04:00
bbedward 18610b8972 lock: size the auth box to the field and snap resizes to square 2026-10-04 18:58:11 -04:00
bbedward c2962f8239 lock: give the editor its own grid and make the grid keys reachable 2026-10-04 18:43:45 -04:00
dms-ci[bot] d3936e1742 nix: sync flake.lock to dank-qml-common submodule 2026-10-04 21:50:18 +00:00
bbedward 23e8bb5935 dash: rebuild the calendar event editor on settings rows
notifications: paint the popup and toast surface once
2026-10-04 17:49:25 -04:00
bbedward d7dc8ae5d2 lock: allow customization, including any desktop widget, to be added and
arranged on lock screen
2026-10-04 15:46:56 -04:00
EmiOnFlux 375aa2f492 add portage backend (#3676)
* add portage backend

* portage: add excludes
2026-10-04 15:10:36 -04:00
apollo79 30c7171836 timepicker: align with m3e spec (#3689) 2026-10-04 13:32:30 -04:00
bbedward 27f7a1b67b tests: drop the surface fixtures and the ui source slicing tests 2026-10-04 11:28:34 -04:00
Huỳnh Thiện Lộc d8fa3f609d fix(dock): preserve focused indicator in overview mode (#3680) 2026-10-04 11:08:18 -04:00
Huỳnh Thiện Lộc 4ca2a8ef99 feat(dock): push neighboring icons apart on magnification (#3681)
* feat(dock): push neighboring icons apart on magnification

* fix(dock): cap magnification expansion and disable it in taskbar mode

* fix(dock): scale pushed icon offsets to the capped magnification expansion

* fix(dock): avoid binding loop when reading expansion ratio
2026-10-04 10:44:58 -04:00
arfan c79127c1bb theme: expand vscode syntax and semantic token colors (#3686)
Add missing TextMate scopes (storage, entity namespaces/types) and
semantic tokens (method, struct, interface, enum, typeParameter,
enumMember, namespace), and remap roles to theme-derived hues so
keywords, strings, types and methods read distinctly instead of
flat white.
2026-10-04 10:43:37 -04:00
purian23 b48fce0447 refactor(control center): free the edit button and misc tweaks 2026-10-04 00:55:31 -04:00
purian23 585827e7e4 feat(workspace): add dots workspace indicator styles & compact mode 2026-10-03 20:49:25 -04:00
purian23 d025ccdfc8 refactor(overflow): simplify dbar widgets overflow menu 2026-10-03 18:35:26 -04:00
purian23 184a16c2b4 feat(theme): add support for DMS bold color spec variants 2026-10-03 16:43:38 -04:00
purian23 19abc7dc96 feat(island): state pulses, mic, privacy & duo status glyph activities
- caps lock, power profile, idle inhibitor and charging pulse in the island
- microphone mute and volume as a level transient
- privacy slot while mic, camera or screen share is in use
- duo battery style: arc around the network glyph with signal dots
2026-10-03 15:28:06 -04:00
Marlon Kranz 51f115f11e fix(notifications): edit rules from the menu instead of deleting them (#3670)
* fix(notifications): edit rules from the menu instead of deleting them

A notification rule can set an action and Bypass DND together, but
Unmute and Block in Do Not Disturb spliced the whole rule, dropping the
other setting. They now reset only their own field, and a rule is
removed once it no longer does anything (action and urgency default,
no bypass). Mute and Allow in Do Not Disturb set their field on the
app's existing rule instead of appending one that first-match policy
would shadow.

The menu now only reads and edits enabled, exact rules on the app name
or desktop entry. It used to match any rule whose pattern equaled the
app name, including summary, body, contains and regex rules, and could
delete disabled ones.

Also adds a Copy item to the notification menu on popups and center
cards, copying the summary and body as plain text.

* fix(notifications): let mute apply alongside other rule actions

Rule actions are first-match, so a mute rule behind an app's existing
rule (e.g. No history) never applied while the menu showed Unmute.
Mute is now evaluated like Bypass DND: any matching enabled mute rule
disables the popup, and the other actions stay first-match, so No
history and Mute both apply.

* fix(notifications): skip the sound for dropped and muted notifications

The notification sound played before the check that dismisses a
notification with neither a popup nor a center entry, so dropped
notifications (transient while muted, Popup only plus Mute) still made
a sound. Play it only for notifications that are kept, and not when a
mute rule applies, like Do Not Disturb.
2026-10-03 14:39:48 -04:00
bbedward 27989967b8 media: build the bar lyric line from the word list, not the time-sorted cues 2026-10-03 12:43:31 -04:00
bbedward 2a8368a8a4 media: add album art to bar widget
settings: keep the page still when a rebuilt row drops focus

fixes #3673
2026-10-03 12:23:57 -04:00
bbedward 49d7d6b2fe media: bar lyric line, dbus position re-anchor, keep upcoming lines in view
closes #3652
fixes #3650
2026-10-03 07:07:52 -04:00
bbedward 48de42b57c settings: request activation when the window is already open 2026-10-03 05:39:37 -04:00
bbedward 28c76ddb66 settings: hide profile pic on search query 2026-10-03 05:00:58 -04:00
Marlon Kranz 6a320399c9 fix(lock): show the lock screen on every display when none is selected (#3666)
With two or more screens connected and none of them in the lock screen
display list (a laptop docked at a different desk), every surface was
inactive and no password field rendered, so the only way out was a
TTY. getFilteredScreens only falls back to all screens with
showOnLastDisplay and exactly one screen, which the lock screen picker
never sets. Treat an empty result as all screens, the same as the
default "all" preference.
2026-10-03 04:57:52 -04:00
bbedward 497e80732f settings: restore a max content width 2026-10-03 04:52:55 -04:00
bbedward 57de2083ad settings: keep the shortcut search in view and focused while filtering
fixes #3649
2026-10-03 04:40:13 -04:00
尘世凡间 204aa1735b clipboard: decode file uris and stop failing large file copies (#3598)
EntryToFile and the preview helpers stripped file:// without
percent-decoding, so history entries whose paths contain non-ASCII
characters stat'ed a literal %E5... path and copyEntry failed with
"file not found". CopyFile additionally hard-failed any file larger
than MaxEntrySize (5MB default) right after stat, so even decoded
entries for bigger files could never be restored to the clipboard.

Decode the path in EntryToFile, uriListPreview and
tryReadImageFromURI. In CopyFile only read file content when it fits
the size cap; oversized files still get offered as uri-list /
gnome-copied-files / plain path, and the stored entry keeps the file
URI instead of an empty body.

Keep encode and decode symmetric: every URI this service stores or
offers now goes through encodeFileURI (percent-encoded) and every
read path decodes exactly once through pathFromFileURI, so a literal
%20 in a file name survives the round trip. Regression tests cover
space / literal %20 / # / CJK / oversized names across EntryToFile,
uriListPreview, the image probe and handleCopyEntry.
2026-10-03 03:30:53 -04:00
purian23 d9d9dc21d2 feat(system-update): implement floating window variant for system updates 2026-10-03 02:16:30 -04:00
purian23 0bff4524e2 refactor(software-updates): few button tweaks 2026-10-02 23:07:55 -04:00
purian23 136e1f21cd refactor(notification): restore preview icon placement & apply user theme to dank icon 2026-10-02 22:52:45 -04:00
purian23 4395c97a6a refactor(settings): embed avatar in search field & enhance Software Updates UI 2026-10-02 22:32:26 -04:00
Alexander 1e1abb0206 theme: use the current dms executable for matugen (#3648) 2026-10-02 19:07:41 -04:00
lingdianshiren 300544bc8c bluetooth: identify codecs from the profile description (#3643)
* bluetooth: identify codecs from the profile description

PipeWire describes profiles in the daemon's locale and only names the codec in
that description for the bare profile (LHDC settles on "a2dp-sink"), so the old
English "codec %s" match dropped LHDC profiles on non-English systems. Parsing
the translated shape instead only moves the coupling to word order and
punctuation.

Emit the profile index and raw description from the lua script and let the UI
name the codec by matching the untranslated literal from one codec table.
Switch by index, falling back to the profile name or codec literal, so no
translated text is parsed. Also teach the BlueZ fallback the Savitech ids
(LHDC v5 and v3).

Test: quickshell/tests/bluez-codec.test.mjs

* bluetooth: show unknown codec without masking bluez

* bluetooth: ignore stale codec query results
2026-10-02 19:07:28 -04:00
Huỳnh Thiện Lộc 340f2d179c fix(notepad): hide placeholder during IME composition (#3646) 2026-10-02 18:28:58 -04:00
Huỳnh Thiện Lộc 5318c43f3a feat(screenshot): add Ctrl+C shortcut to copy w/o saving (#3645)
* feat(screenshot): add Ctrl+C shortcut to copy selection without saving

Add Ctrl+C shortcut in interactive region selection mode to immediately
copy the current selection (or hovered snap target) to clipboard without
saving to a file.

- Works during mid-drag, on hovered targets, or on existing selections
- Bypasses file saving and forces clipboard output
- Emits aborted status in JSON mode so external callers (such as Quick
  Capture) exit cleanly without launching an annotation editor
- Exits cleanly without error if triggered with no selection
- Updates HUD items with Ctrl+C copy shortcut
- Hides corner resize handles and maintains crosshair cursor while dragging with Ctrl held

* fix(screenshot): bypass stdout and scroll phase when copyOnly is active

- Disable stdout output when copyOnly is active to ensure the image
  is copied to the clipboard instead of being written to stdout.
- Write 'Copied to clipboard' to stderr if --stdout was originally passed.
- Bypass entering scrollPhase in finishSelection() if copyOnly is set,
  immediately capturing and copying the selected region.
2026-10-02 18:25:59 -04:00
bbedward 850d7a6071 dash: fix metric trends, edit bar focus rings, battery color option, forecast containers
fixes #3658
2026-10-02 18:23:50 -04:00
bbedward 1d789892f7 lock: wake outputs on unlock whichever path powered them off
fixes #3660
2026-10-02 16:59:03 -04:00
bbedward 1492b649e6 qml-common symc 2026-10-02 16:52:07 -04:00
bbedward c559a5250c wallpaper: material presets with seed color and dynamic backdrop 2026-10-02 16:51:34 -04:00
bbedward eda8b26f6e trash: only count while a dock shows the trash button
fixes #3664
2026-10-02 15:48:09 -04:00
bbedward e4ec3582b4 settings: raised content pane and surface tier ladder instead of dividers 2026-10-02 11:53:15 -04:00
purian23 af27f0bac0 refactor(software-updates): remove confirmation modal & add action item prompt support 2026-10-02 00:52:44 -04:00
purian23 e473c92825 feat(overflow): automatic & per/app dbar widget options 2026-10-02 00:34:09 -04:00
purian23 de1c5c99fa feat(settings + keyboard): core clean up & keyboard driven overhaul in settings 2026-10-01 19:34:28 -04:00
bbedward c668ab9fdf cc: place popout menus by the host window's margins 2026-10-01 18:44:12 -04:00
bbedward e61fa45444 config: resolve nested includes against the including file
fixes #3647
2026-10-01 18:06:50 -04:00
bbedward ea6c52b5e8 dash: wallpaper carousel follows the index only, swipe steps it 2026-10-01 17:32:48 -04:00
bbedward b9c8faa60f settings: hold the cups ref while the window is open 2026-10-01 15:31:38 -04:00
bbedward d77c4f6298 wellbeing: add Digital Wellbeing, with a built-in dash plugin 2026-10-01 15:15:37 -04:00
bbedward a348d8bc5e icons: skip the pixmaps fallback when the theme index times out fixes #3642
battery: bridge history gaps, clock-aligned ticks, hover readout
2026-10-01 10:14:38 -04:00
bbedward 0b9d931ea3 brightness: ddcutil-parity ddc probing, bounded rereads
fixes #3616
2026-10-01 09:16:23 -04:00
Huỳnh Thiện Lộc b54fa3a584 feat(dock): expand focused app indicator into animated pill (#3640)
Focused dot expands to 3× width (horizontal dock) or 3× height
(vertical dock) using Theme.shortDuration + Easing.OutCubic.
Applies to both circle and line indicator styles.
2026-10-01 08:52:24 -04:00
Stephanos Kamprogiannis 22fa08f0ff idle: wake monitors after automatic lock (#3633)
Locking rearms the normal idle monitors and can lose their wake transition.
Enable the existing locked-monitor wake listener for idle power-off too.
2026-10-01 08:51:32 -04:00
LOSTDATA 738e19715e battery: pulse the critical bar indicator (#3596)
* battery: pulse the critical bar indicator

* battery: add critical pulse toggle
2026-10-01 02:04:14 -04:00
purian23 a8623e3ccb settings: cleanup pass to update shared surface tier roles 2026-09-30 21:35:40 -04:00
bbedward 5b4faef744 frame: surfaces opened over a stacked modal paint their own chrome
fixes #3638
2026-09-30 20:57:39 -04:00
bbedward c4ea1a1dbd frame: collapse a hidden bar's band to the gutter
fixes #3630
2026-09-30 20:57:39 -04:00
Alexander d84037066e dock: restore cycling through grouped windows (#3637) 2026-09-30 17:56:15 -04:00
bbedward ad25687d30 core: bump dankgo 2026-09-30 16:27:26 -04:00
bbedward 3bbf02f502 settings: some changes ref #3635
cc: add trash icon to drag and crush things
cc: allow anchoring footer items to start or end
2026-09-30 16:00:48 -04:00
bbedward 9e7f35de0c settings: index the compositor page in settings search
fixes #3634
2026-09-30 14:38:19 -04:00
bbedward 8e9970efee notifications: more androidy physics
fixes #3636
2026-09-30 14:02:48 -04:00
bbedward adccc05397 cc: fixed footer or header, customizable with any widgets, blah blah 2026-09-30 12:46:53 -04:00
purian23 73759d5752 refactor(control center): redesign update #8 2026-09-30 00:15:05 -04:00
bbedward db903801c3 osd: more position options, hookup sheets to some
fixes #3629
2026-09-29 23:31:28 -04:00
bbedward 219a95d869 notifications: edit rules in a dialog like window rules
fixes #3615
2026-09-29 18:08:35 -04:00
bbedward d5a684ce52 settings: use wallpaper icon for wallpaper & colors 2026-09-29 17:50:59 -04:00
bbedward 5cfda4ebe7 notifications: no selection ring on modal open clipboard: match the notification header and add a settings butto
settings/interface: explicit window radius match option
cc: restore dim in edit mode

fixes #3623
2026-09-29 17:44:20 -04:00
bbedward f025a8d7b4 island: keep control center and dash size in edit mode
widgets: show keyboard hint boxes as keycaps

fixes #3624
2026-09-29 17:29:05 -04:00
Huỳnh Thiện Lộc ea70863fd7 feat(dock): add customizable proximity icon magnification (#3625)
* feat(dock): add proximity icon magnification with customizable curves

* fix(dock): restrict proximity magnification to dock and add translator context
2026-09-29 17:07:22 -04:00
Huỳnh Thiện Lộc 34e0ed2e90 feat(screenshot): snap to active popouts & modals in region mode (#3618)
* feat(screenshot): snap to active popouts and modals in region mode

Allow region screenshot selector to detect and snap to visible shell
popouts and modals on hover and click or keyboard confirmation.

- PopoutManager.qml: add getActiveSurfaces() collecting visible
  DankPopout and DankModal global bounding boxes
- DMSShellIPC.qml: add screenshot.getSurfaces IPC endpoint
- core: fetch snap targets from shell IPC during region screenshot init
- selector: highlight snap targets on hover with surface name and dimensions
- selector: click or Enter/Space on hovered target locks selection to surface bounds
- selector: allow hover snap over pre-selection (SelectionState.fromPreSelect)
- selector: reset selection on degenerate (1x1) click release for quick re-selection
- unit tests for display name formatting, hover detection, and render bounds

* fix(screenshot): correct snap off-by-one and drag-away cancellation

- region_input: fix drag-away snap cancellation by setting hasSelection=true
  and clearing preSelect so freeform dragging renders and is not discarded
- region_input: add snapToTarget helper subtracting 1 device pixel (devPxX/Y)
  from currentX/currentY to prevent inclusive coordinate off-by-one (420x580 -> 420x580)
- region_render: compute hover bounds with w = round(width * scaleX) and
  x2 = x1 + w - 1 to eliminate +1 device pixel in render bounds
- region_snap_test: assert exact 420x580 bounds for both hover and snapped states

* style(screenshot): format region_snap_test.go with golangci-lint-fmt
2026-09-29 17:06:19 -04:00
arfan c1818dee2c fix(theme-vscode): update editorSuggestWidget colors for improved visibility (#3599)
* fix(theme-vscode): update editorSuggestWidget colors for improved visibility in dark, default, and light themes

* fix(theme-vscode): update button.hoverBackground to use primary_fixed colors for consistency
2026-09-29 17:06:06 -04:00
bbedward 765ffe8564 plugins: hide the list under the detail overlay
plugins: block activation when the startup check cannot run

ref #3626
2026-09-29 16:57:24 -04:00
Mario Mohar 53b8de2714 fix(keybinds): warn when a bind's key sits above the layout's first level (#3591)
A compositor resolves a bind's keysym on the first level of the active
layout. Layouts that put punctuation higher up therefore cannot reach it
at all: de:neo has bracketleft on layer 4 and plain de on AltGr, so
Mod+bracketleft is a bind that can never fire. The editor recorded and
stored it anyway and gave no sign that anything was wrong (#3585).

`dms keybinds keymap` now reports what the seat's xkb keymap puts on the
first level of every physical key, and the editor says so instead of
letting a dead bind through.

The keymap a compositor hands out writes its keysyms as hex, so only the
range the keymap package can name back is usable for the comparison.
That is the ASCII range, which is where the problem lives, and the
command reports the vocabulary alongside the keys so the caller can tell
"the layout cannot reach this" from "we could not name it" and stay
quiet in the second case.
2026-09-29 16:57:13 -04:00
Hatta 3081941592 lock: lock before sleep when the command skips logind (#3587)
* lock: lock before sleep when the command skips logind

Custom hibernate never emits PrepareForSleep, so the session stayed unlocked. Wait for the compositor lock on every sleep path, and release the delay inhibitor if that lock finished first.

* lock: release the sleep inhibitor only after lockerReady

* lock: read the sleep inhibitor under its mutex in the race test
2026-09-29 16:50:13 -04:00
arfan c205358b79 feat(settings-appdefault): add office and archive manager categories (#3580)
Add word processor, spreadsheet, presentation and archive manager
selectors to default apps, fix calendar lookup to query text/calendar
first so Dank Calendar is detected, and expand image mime coverage
with tiff/heic/heif/jxl.
2026-09-29 16:42:49 -04:00
Matt Jiang ce8db05cea core: add user bin dirs to the shell PATH (#3575) 2026-09-29 16:42:36 -04:00
dion 13034cb85c audio: add mono audio toggle with support detection (#3539) 2026-09-29 16:38:22 -04:00
bbedward f482642880 sysupdate: check only on demand, run at low priority, fix the settings page
scrollbar: interface style toggle and no scrollbars in the dash
dash: never allow scrollbars
cc: widget sheet cards, header close button and content-sized dialogs
2026-09-29 16:30:04 -04:00
bbedward aced05157b cc: redesign 7 with a persistent footer and individually tiled buttons 2026-09-29 14:52:40 -04:00
bbedward 2fb0cfee60 delete bad test 2026-09-29 11:24:52 -04:00
bbedward 182e08b1a4 dash: soft primary container, per-widget options and weather tones 2026-09-29 11:22:55 -04:00
bbedward 00952d4d51 settings: window rule editor on the shortcut editor's dialog and cards
fixes #3608
2026-09-29 11:11:23 -04:00
purian23 35faa3130c feat(island): add clipboard support & align launcher sizing options 2026-09-29 03:30:58 -04:00
purian23 97ec64ff97 refactor(cc / blur): restore card background on quick controls & unify blur popouts on Dash/CC 2026-09-29 01:39:58 -04:00
purian23 3fb27d93de fix(hubHint): filter hidden children & test fix 2026-09-28 21:02:59 -04:00
dms-ci[bot] 8173dd336e nix: sync flake.lock to dank-qml-common submodule 2026-09-28 23:46:52 +00:00
purian23 eda77f7816 feat(settings): dms system software updates & versioning support 2026-09-28 19:45:07 -04:00
bbedward 33bc0a0dff cc: network popup opens at the top
fixes #3605
2026-09-28 16:59:42 -04:00
bbedward e372c48009 settings/cc: network and brightness pages on the shared row pattern
fixes #3602
fixes #3603
fixes #3610
2026-09-28 16:52:51 -04:00
bbedward f9de6b86ea theme: unify every selected list fill on selectedContainer 2026-09-28 16:05:12 -04:00
bbedward ffe2da7e99 avatar: material person glyph instead of an app icon lookup
fixes #3600
2026-09-28 14:43:04 -04:00
bbedward 1142390724 settings: add + item name on every creation fab
settings: stop repeating the page title as the first card label
translations: index untitled cards by their page, not a nested row

fixes #3604
fixes #3609
2026-09-28 14:38:43 -04:00
bbedward 4ca4e2586e cc: change header design and sliders
fixes #3606
2026-09-28 14:26:42 -04:00
bbedward a609b5f999 cc: bg off by default in header 2026-09-27 23:37:09 -04:00
bbedward f6bb4b1727 control center: merge user card and quick actions into one pinned header tile 2026-09-27 23:26:14 -04:00
dms-ci[bot] c45579746b nix: sync flake.lock to dank-qml-common submodule 2026-09-28 00:56:46 +00:00
purian23 64b4d0d235 feat(control center): configurable user card & power control action tiles
- Quick actions are reorderable with button color roles and optional power accents
- Updated dank-qml-common
2026-09-27 20:55:54 -04:00
Feng Yu a4e500bc00 dankbar: bind clickthrough mask regions to section items (#3595) 2026-09-27 18:24:21 -04:00
bbedward d5c96a1756 keybinds: resolve the main modifier per compositor and report it on the cheatsheet 2026-09-27 16:19:06 -04:00
Alexander 734314d09f bar: allow popouts from bars revealed in overview (#3592) 2026-09-27 16:13:03 -04:00
bbedward e4fb86be91 cc: prevent edit mode from leaving screen boundaries 2026-09-27 11:57:53 -04:00
Huỳnh Thiện Lộc 77a6cdfc00 fix(dock): prune orphaned dock surfaces and invalidate frame on deletion (#3577) 2026-09-27 11:50:27 -04:00
Matt Jiang f27627ead2 powermenu: reboot into a chosen EFI boot entry (#3568)
* powermenu: derive shortcuts and extra actions from one source

The power menu matched keys with a switch that repeated the key hints
getPowerActionData already returns, plus a second key list for release.
Look the pressed key up against the hints of the visible actions instead,
so every action with a hint gets its shortcut and the two cannot drift.

Side effects:
- Ctrl+letter no longer triggers actions. Only Ctrl+P was excluded
  before, so Ctrl+R still rebooted. Ctrl combinations are navigation.
- U now triggers switch user when it is shown. Its hint already said U.
- Releasing a letter with no visible action no longer cancels a hold.

SessionService now owns the custom:N action ids as customPowerActions
and extraPowerActions, which the power menu and the launcher both use.
Actions resolve by index into that list instead of slicing the prefix.

* powermenu: add actions that reboot into a chosen efi boot entry

Settings → Power & Sleep gets a "Reboot to another OS" card that lists
the firmware's active EFI entries through efibootmgr. Each saved entry
becomes a "Reboot to <name>" action in the power menu and the launcher,
bound to 1-9 in the order it was added.

Picking one lists the entries again, and stops with a toast if the id no
longer carries the saved label. Otherwise it sets BootNext through pkexec
and reboots through SessionService, so custom reboot commands still apply.

BootEntryService reports why entries are unavailable: not booted in UEFI
mode, efibootmgr missing, or efibootmgr failing with its stderr. Saved
entries stay listed in settings whatever the status, so a synced config
on another machine can still remove them.

efibootmgr parsing and the picker's filtering live in BootEntries.js,
covered by tests/boot-entries.test.mjs.

* powermenu: ignore special keys in action shortcuts
2026-09-27 11:47:05 -04:00
arfan 814620a934 fix(theme): update focus highlight colors for list and quick input components (#3579) 2026-09-27 11:46:44 -04:00
Huỳnh Thiện Lộc 2a5e9d59df docs: update Ko-fi support link in README (#3578) 2026-09-27 11:46:09 -04:00
Huỳnh Thiện Lộc 980d37c9da fix(bar): update center section layout synchronously on child size changes (#3588) 2026-09-27 11:45:02 -04:00
Huỳnh Thiện Lộc 0717908d53 fix(island): avoid bottom clipping in media expanded sheet (#3593) 2026-09-27 11:44:08 -04:00
bbedward 4b00c047da launcher: keep the shift+enter paste process alive after the launcher closes
fixes #3586
2026-09-27 11:41:55 -04:00
purian23 5623476126 feat(island): host the island as a widget in any bar configuration
- The island is now a bar widget so any bar can host one alongside the island layout and dot
- Routing got simpler: pick which shortcuts open here, the rest follow the shared rules
- IPC close/hide take down every surface showing the activity
- Qt 6.11 pointer prune workaround so the expanded island stays clickable
2026-09-27 02:02:30 -04:00
bbedward 9dc5b2c5af settings: some loader restructure and restyle keybinds
island: fix satellite opacity and blur
2026-09-26 16:25:34 -04:00
bbedward 09e7ea0221 lyrics: share one mpris controller via LyricsService, accept plain sources 2026-09-26 14:34:32 -04:00
Huỳnh Thiện Lộc 1fc4537894 fix(sysmon): performance tab transparency flicker (#3576)
* fix(sysmon): prevent transparency flicker when switching to performance tab

* fix(sysmon): preload tabs on modal open and ensure immediate floating surface resolution
2026-09-25 22:45:51 -04:00
bbedward e796f9d1ed i18n: sync 2026-09-25 21:33:56 -04:00
bbedward a48f41b749 theme: switch to dankgo material utilities fork
settings: label some non-obvious thing swith more details

fixes #3555
fixes #3553
2026-09-25 21:31:34 -04:00
bbedward 3247476368 settings: rank search results by page level and whole-word matches 2026-09-25 18:54:53 -04:00
bbedward 3b1b63c93f notepad: confirm before loading files over 5 MiB
closes #3574
2026-09-25 18:36:42 -04:00
dms-ci[bot] 909c0055ea nix: sync flake.lock to dank-qml-common submodule 2026-09-25 21:44:05 +00:00
bbedward c7ab8993e5 media: add empty state and contained lyrics loader 2026-09-25 17:43:41 -04:00
Marcus Ramberg 05377e690a fix: scope bluez dbus match to bluezService (#3573)
Ensure the watcher only seees bluez messages
2026-09-25 16:44:28 -04:00
dms-ci[bot] 65dbb9fc58 nix: update vendorHash for go.mod changes 2026-09-25 19:03:39 +00:00
bbedward cc45f8b445 settings: add reusable FAB component and use it in more places 2026-09-25 15:02:45 -04:00
Marcus Ramberg 8da739a3ea feat(cellular): expose signal strength and conn type (#3533)
network manager doesn't expose this so talk to modem manager over dbus
2026-09-25 15:02:23 -04:00
bbedward 3b529b3a97 lyrics: add kugou and YT music providers 2026-09-25 14:02:27 -04:00
LemonMon 60342b7416 fix: stabilize tray item key to prevent position jumps on property changes (#3386)
* fix: keep tray order stable when tooltipTitle changes dynamically

Revert the earlier simplistic key approach: keep getTrayItemKey()
unchanged (id::tooltipTitle) so duplicate-id disambiguation (#1542)
is preserved and persisted id::tooltipTitle entries keep matching.

Instead make persisted-key lookups tolerant of dynamic tooltipTitle:
resolveOrderIndex() in sortByPreferredOrder() does exact match first,
falling back to prefix matching an id::* entry only when the id has a
single instance. findSavedTrayKey()/isTrayIdHidden() replace direct
SessionData.isHiddenTrayId() calls and showTrayId() now receives the
actual saved key.

Prefix fallback is gated by trayItemCountForId() to single-instance
ids only, so multiple items sharing an id always match exactly and
hiding/ordering one never affects the others.

* fix: bail on ambiguous saved tray keys in fallback

* tray: use occurrence index as stable key, add one-time session migration

* tray: pin stable key per item instance so it never flips when siblings come and go

* tray: assign unique per-id sequential keys to avoid collision after sibling exit

* tray: reuse free key slots so a restarted singleton keeps its plain id
2026-09-25 11:10:40 -04:00
Vladimir KosteleiandClaude Opus 5 d7074a53eb fix: fingerprint lid open (#3448)
* lock: keep the fingerprint reader usable across suspend

The reader stopped responding after the lid was closed and opened, after a long
idle period, and after suspend.

The shell held a PAM session for as long as the screen was locked, so a suspend
always landed on a running verify. fprintd then suspends the device under the
verify, pam_fprintd's ReleaseDevice fails with "device is still busy", and the
claim is never released, so every later Claim fails until fprintd restarts. The
reader is now released when the monitors blank and armed again when they come
back, so a suspend has nothing left to wedge.

The PAM profile drops to max-tries=1 and sets an explicit timeout. pam_fprintd
only counts a wrong finger, so one finished session is now one wrong scan and
maxFprintTries means the number of scans it names rather than five times that.
A timeout expiry arrives as PamResult.Error, exactly like a device fault, so a
session that ran at least as long as the configured timeout is treated as a
planned renewal instead of spending the error budget and painting an error.

A suspend that still lands while the screen is on wedges as before. Recovery now
waits out fprintd's idle timeout instead of climbing to it, which brings the
reader back about 37 seconds after resume instead of about 98.

SessionService gains lidOpened from UPower, because logind can ignore the lid on
AC and resume alone is not enough. The lock PAM state machine is fixed alongside:
PamResult.Failed was dropped, a pending retry outlived the contexts that owned
it, and an availability check reset the wrong-scan counter.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q5LQHivuucTLXwLkvZMfcn

* lock: time the fingerprint session from the verify, not the claim

pam_fprintd's timeout runs from the verify, but the session age was measured
from start(), which folds the claim into it. A slow claim plus a verify that
failed before its own timeout could add up past the timeout and be taken for a
planned renewal, skipping the backoff. A session that never reached a verify had
the same problem.

The age now starts at the message pam_fprintd sends when the verify begins, so a
session that never verified can never qualify as an expiry, and a slow claim no
longer ages a fault. The message is translated and quickshell clears it before
completed, so only its timing is used, never its text.

That message arrives after pam_fprintd has already armed its deadline, because
VerifyStart is asynchronous, so the age read here runs short of the real timeout
and a slack makes it up. The bias is deliberate. A fault misread as a timeout
costs one extra retry, while a timeout misread as a fault parks the reader behind
a backoff that grows to a minute, which on an idle lock screen would leave it
unusable most of the time.

Also corrects the comment that claimed a timeout is reported without a message.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q5LQHivuucTLXwLkvZMfcn

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-09-25 11:02:51 -04:00
Nat Fletcher 7c2140dd60 theme: add toggle to skip matugen at startup when nothing changed (#3270) 2026-09-25 10:56:20 -04:00
bbedward 3c14a41624 cc: remove slider wheel opt out from details pages 2026-09-25 10:50:31 -04:00
dms-ci[bot] b1b6a3d686 nix: sync flake.lock to dank-qml-common submodule 2026-09-25 14:28:05 +00:00
bbedward 361e9da26a matugen: use a contrasting color for vscode light theme constants
fixes #3566
2026-09-25 10:27:39 -04:00
bbedward 890166e384 cc: apply some non-compositor blur to details pages based on settings
Can't do real compositor blur since they arent real subsurfaces
2026-09-25 10:27:39 -04:00
bbedward 6cb8a3b67a launcher: restore focus ring to spotlight and respect global setting 2026-09-25 10:27:39 -04:00
Tássio Guerreiro Antunes Virgínio 10e8086874 trayrecovery: recover SNI items orphaned at first boot (#3570)
Autostart apps (e.g. KeePassXC) that call RegisterStatusNotifierItem
before the watcher owns its name fail silently and never re-register.
Until now the recovery scan only ran on suspend/resume or watcher
owner change, so first-boot orphans were never rescued.

Schedule a one-time delayed scan when the watcher first appears.
recoverTrayItems() already deduplicates via registeredConnIDs, so this
cannot double-register apps that registered normally.
2026-09-25 09:36:01 -04:00
bbedward 9a1c622492 blur: clip regions to the window so hyprland stops blurring whole surfaces
fixes #3480
2026-09-24 22:00:28 -04:00
bbedward 82fd310732 lyrics: restore loaded lyrics when the media tab is recreated
fixes #3563
2026-09-24 21:25:12 -04:00
bbedward ec6407fb1e cc: drop the detail dialog scroller so pages scroll to their end
fixes #3562
2026-09-24 21:03:26 -04:00
bbedward e76d989656 hyprland: use focusgrab in overview
fixes #3486
2026-09-24 20:09:33 -04:00
bbedward 7975136c10 popout: keep hover popouts open after a transient menu closes
fixes #3492
2026-09-24 18:13:20 -04:00
bbedward 8508121f4a bar: open hidden and disabled widgets over ipc
fixes #3550
2026-09-24 14:57:18 -04:00
bbedward b97a8828eb launcher: keep spotlight open when the category dropdown opens on hyprland
fixes #3556
2026-09-24 13:02:52 -04:00
bbedward ace13d7c18 popout: never round the body below its content height
fixes #3558
2026-09-24 12:07:40 -04:00
bbedward 4554c9c894 cc: show detail pages as a dialog style instead of page 2026-09-24 11:33:35 -04:00
bbedward 4949c2fbbe settings: fix some inconsistencies
dash: no header for disabled tabs
2026-09-24 10:00:25 -04:00
dms-ci[bot] 1c61c26d46 nix: sync flake.lock to dank-qml-common submodule 2026-09-24 01:51:11 +00:00
bbedward e3b5cde72e update qml-common 2026-09-23 21:50:14 -04:00
dms-ci[bot] d750c173ba nix: update vendorHash for go.mod changes 2026-09-24 01:28:14 +00:00
bbedward 6e9bbc15a6 files: swith to diles shared components 2026-09-23 21:27:14 -04:00
bbedward 878aa69fac lyrics: keep following through same-song refreshes 2026-09-23 20:16:42 -04:00
bbedward 145b9bbc18 launcher: even out spacing 2026-09-23 18:13:37 -04:00
bbedward c5029444ff lyrics: fix fade 2026-09-23 16:41:41 -04:00
bbedward 4b926be08f launcher: design adjustments
shell: cut animation frame cost and cold open stalls
dock: restore 1.6-like spacing behaviors

fixes #3537
2026-09-23 16:35:18 -04:00
bbedward 4d31c84188 bump qml-common 2026-09-23 14:39:18 -04:00
bbedward eb0b56e578 i18n: sync 2026-09-23 14:18:09 -04:00
Thomas KrollandClaude Opus 5.5 eb0cb80884 fix(workspace): ignore duplicate submit in rename modal (#3549)
Pressing Enter in the name field fires both the text field's accepted
signal and the dialog's own Return handler, so submitAndClose() runs
twice. The first call renames and hides the window, which clears the
field; the second call then renames to an empty string and unsets the
workspace name. The OK button only fires once, so it was unaffected.

Ignore submits once the window is hidden.


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

Signed-off-by: Thomas Kroll <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-23 14:05:16 -04:00
dms-ci[bot] 7e8c556ab6 nix: sync flake.lock to dank-qml-common submodule 2026-09-23 16:59:34 +00:00
bbedward ccd6da3b43 media: add loading indicator between lyrics changes, add no-smoothing
option, adjust surface colors and theme tokens
2026-09-23 12:58:08 -04:00
bbedward f97e7f7fe9 audio: drop input ports and hidden sinks from switchable outputs 2026-09-23 11:41:35 -04:00
bbedward 6ab3755405 bar: keep the full-width center fixed and cap the focused app before it 2026-09-23 11:00:27 -04:00
bbedward fb973951e3 screenshot: configure surfaces before requesting screen captures 2026-09-23 11:00:27 -04:00
f22ad2c333 feat(theme): inject external color palettes into matugen (#3509)
* matugen: support multiple injected palettes with settings ui

Run user-configured external commands on each theme build and merge their
JSON output into matugen's import data under a namespace, so templates can
reference e.g. {{mypalette.color0}} alongside dank16. Multiple palettes are
supported, each under its own namespace. Command output may be nested JSON
(pywal/wallust shape). A settings page manages the list; a legacy
single-object config file is still read.

* settings: map PaletteInjectTab in search index

* matugen: bound palette-inject command with timeout and WaitDelay

* fix palette column spacing (review suggested

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* remove wallpaper kind guard (review suggestion)

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* fixup! remove wallpaper kind guard (review suggestion)

---------

Co-authored-by: bbedward <[email protected]>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-09-23 10:26:47 -04:00
bbedward 4bdfcfc0cf settings: adjust window rules and clock options 2026-09-23 09:42:28 -04:00
Huỳnh Thiện Lộc 9d1884fb19 feat(keybinds): redesign cheatsheet with category navigation (#3534)
* feat(keybinds): redesign cheatsheet with category navigation, M3 styling, and click-to-execute

- Add category sidebar with Material Symbols icons and badge counts
- Move search field to top of sidebar with Ctrl+F shortcut
- Implement 2-column masonry layout for keybind items
- Deduplicate duplicate actions and stack multi-key combos vertically
- Collapse consecutive workspace keybindings (1-9) into range entries
- Add media & hardware icons for volume, mic, brightness, and PrtSc keycaps
- Preserve Niri window filter properties and distinguish same-app Alt-Tab binds
- Enable click-to-execute for keybind actions across supported compositors

* fix(keybinds): preserve niri action properties and disable execution on range rows

* fix(keybinds): convert niri action properties to CLI flags and disable IPC execution for window switcher binds

* fix(keybinds): pass skip-confirmation as boolean switch in niri executeAction

* fix(keybinds): handle niri spawn-sh actions in executeAction
2026-09-23 09:09:28 -04:00
purian23 80060ee925 fix(debian): obs preinstall & ID checks for debian testing builds 2026-09-22 23:29:34 -04:00
bbedward c66ca00062 settings/bar: move name back to its own pill 2026-09-22 22:24:45 -04:00
bbedward 62098c8add rename test 2026-09-22 22:14:35 -04:00
bbedward 4cb2be5fa4 bar: adjust settings UI 2026-09-22 22:12:34 -04:00
purian23 edf936973f fix(pointer): update mouse pointer on bar widgets 2026-09-22 21:44:52 -04:00
purian23 a96dea2019 settings: re-org & unify bar sections into updated sub-sections 2026-09-22 20:41:24 -04:00
purian23 7ba4bccf6c feat(dankdot): operation DMS freedom & island anywere modes
- A new minimal and fun island instance (dot) that gets out of your way to be tossed anywhere on your screen
- Dankisland freedom is here too! It can now also be used in anywhere mode and used horizontally or vertically as you see fit!
2026-09-22 20:38:45 -04:00
bbedward 860542314e settings: re-org launcehr tab built-in plugins, make desktop widgets
sub-pages to match the pattern of bar widgets
2026-09-22 18:11:12 -04:00
bbedward 89484b5d85 compositor: initial support for umbriel
tests: remove a lot of them

fixes #3214
2026-09-22 16:43:41 -04:00
bbedward 27434cf016 ci: fix review job 2026-09-22 14:52:16 -04:00
bbedward abd51287f3 settings: align nested rows and page widths to shared spacing 2026-09-22 14:19:41 -04:00
bbedward 17d52d3c88 media: rebrand to Bento
popouts: instantly set empty input region on exit
2026-09-22 13:44:12 -04:00
bbedward 4eb163538c media: change player style
theme: restore layer outline option
launcher/theme: tweaks and fix icon service
2026-09-22 09:00:34 -04:00
bbedward 2d1b4f905d bar/goth: fix inverse radius override 2026-09-21 22:51:49 -04:00
bbedward e9498716f9 dankbar: trace goth wings and adjacent-bar seams in the border outline
fixes #3502
2026-09-21 22:14:42 -04:00
bbedward 8538b303e3 dankbar: trace goth wings and adjacent bar seams
fixes #3496
2026-09-21 21:32:47 -04:00
bbedward 75b8cfff4f i18n: keep translations when the selected locale resolves to the same file
fixes #3508
2026-09-21 20:46:29 -04:00
bbedward b41f0f2c4e launcher: open files through the mime handler so terminal apps get a terminal
fixes #3518
2026-09-21 19:01:22 -04:00
bbedward f3af870d93 matugen: fix kitty cursor color
fixes #3520
2026-09-21 18:11:39 -04:00
bbedward a84e84c39b layout: let an overview-only bar share the row of a bar that hides in the overview
fixes #3527
2026-09-21 18:09:25 -04:00
bbedward 74e146a1e8 theme: add surface role color overrides and a per-bar background color 2026-09-21 17:57:12 -04:00
bbedward 8266e078e7 dock: show an overview-only dock on niri again
fixes #3528
2026-09-21 17:09:41 -04:00
bbedward c9032926d7 settings: load all pages asynchronously 2026-09-21 16:47:09 -04:00
bbedward bb20528f68 popouts: grow surfaces at their origin
cc: slide grid with the detail page
settings: tweak appearance header style
settings: tweak sidebar profile style
2026-09-21 15:47:01 -04:00
14Do 17ecaad8dd sysupdate: skip the obsoleted packages in dnf output (#3525)
dnf lists the packages an upgrade obsoletes indented beneath it, under a
separate heading. Those rows are removals carrying their installed
version, so they were parsed as upgrades from a version to itself and
counted as pending. Skip indented rows. The heading is translated, so
matching it would only work in English.
2026-09-21 15:27:20 -04:00
Matt Jiang cc8a6053c5 feat(mux): add Herdr backend (#3531)
* mux: add herdr backend

* mux: remove herdr service tests

* mux: include herdr in settings subtitle

* mux: add translator context to the multiplexers subtitle

* mux: drive the service from a backend table

Each backend (tmux, zellij, herdr) is one entry in MuxBackends.js with
its commands and parser, so the service no longer branches on muxType
in eight places and the settings dropdown lists the table keys. One
availability check runs for the selected backend and re-runs when the
backend changes. The herdr parser reads the real {sessions:[...]}
shape only.
2026-09-21 15:26:56 -04:00
bbedward e3b4efc498 notifications: make compact mode more compact 2026-09-21 13:34:21 -04:00
bbedward a3f1a3d620 feat(bar): add support for fractional sizes, fit to contents 2026-09-21 13:27:14 -04:00
bbedward 0bd3e85150 settings: fix window rules overflow, unify popout border settings
cards: fix regression that created borders
matugen: run on a low priority worker and seeds dms-colors before other
templates
2026-09-21 11:47:52 -04:00
14Do 1c683e932f sysupdate: collapse dnf rows duplicated across repos (#3524)
dnf prints one row per (package, repo), so a package offered at the same
version by two enabled repos is listed twice and inflates the pending
count. Key the parse on (name.arch, version) and keep the first row.
2026-09-21 09:40:39 -04:00
Huỳnh Thiện Lộc 21cd0a70ce feat(bar): show edit window rule when rule exists (#3516)
* feat(bar): show edit window rule in focused app popout when rule exists

Check existing DMS-managed window rules when the focused window popout
opens. If a matching rule exists for the focused window, show
'Edit window rule' and open the rule in edit mode via showEdit().
Otherwise, keep showing 'Add window rule' for new rule creation.

* fix(bar): address PR review feedback for focused window rules

- Clear matchingRule up front in checkMatchingRule() to prevent showing previous window's rule while async query is in flight
- Reuse existing catalog term 'Edit Window Rule' to leverage existing translations
2026-09-21 09:38:12 -04:00
neofion cb037e2543 keyboard layout: add user-editable label overrides for compact mode (#3503)
* keyboard layout: add user-editable label overrides for compact mode

* keyboard layout: add translator context and fix search index for label overrides
2026-09-21 09:36:56 -04:00
bbedward 567c14f6d9 processlist: design changes 2026-09-20 22:40:40 -04:00
SatoshoeandClaude Opus 5 dacffb16b3 i18n: pick the DankCommon translation for the chosen locale (#3505)
* i18n: pick the DankCommon translation for the chosen locale

updateLocale() switches the shell translation, but the DankCommon one was
picked once at startup from the system locale and never changed. With DMS set
to English on a German system, plugin strings that fall back to I18n.tr()
(Suspend, Reboot, ...) stayed German. _pickCommonTranslation() now uses the
resolved locale and runs again from useLocale(); for English, where no common
file exists, the table is cleared.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* i18n: clear the DankCommon table when falling back to English

_fallbackToEnglish() left the common table as it was picked for the previous
locale. Setting Language to "System default" on a system without a shell
translation (an English one, cs_CZ, ...) goes through that path, so the shell
switched to English while DankCommon strings stayed in the old language - the
same mismatch this branch fixes for the useLocale() path.

The function now resolves to "en" before re-picking, which clears the table
because no English common file exists, and it also covers the case where the
selected translation file is missing or fails to parse.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-09-20 21:18:53 -04:00
bbedward 64ba6163e2 settings: proper color swatches everywhere 2026-09-20 21:18:13 -04:00
bbedward 4d6de9cf96 settings/wallpaper: more restyle to appearance header 2026-09-20 21:09:12 -04:00
bbedward d9d9e6b9cf settings: rebuild wallpaper page around a single hero thumbnail
cc/edit: fix janky resize problems
2026-09-20 20:42:16 -04:00
bbedward 9b6c9a4b36 control center: hold the tile grid height while a tile is dragged or resized
fixes #3515
2026-09-20 19:27:13 -04:00
bbedward 2486adb226 popouts: reserve edit handle padding without fullscreen surfaces
fixes #3512
2026-09-20 17:43:09 -04:00
Lucas 427da94090 pre-commit: add nix-vendor-hash pre-commit hook (#3521)
* pre-commit: add nix-vendor-hash pre-commit hook

* nix: remove deprecation warning
2026-09-20 17:27:45 -04:00
bbedward b56a53c03a settings: preserve navigation state and add notification test button 2026-09-20 15:28:18 -04:00
purian23 c65cda2273 mango: discover layouts and fix monitor targeting and visibility
Closes #2924
2026-09-20 15:18:54 -04:00
dms-ci[bot] 03a3ef28d2 nix: update vendorHash for go.mod changes 2026-09-20 16:39:10 +00:00
bbedward 5d120d5a66 dgop: sample on a niced worker thread and gate widget modules on their toggles
fixes #3107
2026-09-20 12:36:44 -04:00
bbedward 879e8f5791 overview: keep hyprland window thumbnails inside the active workspace ring
fixes #3499
2026-09-20 12:13:11 -04:00
Huỳnh Thiện Lộc 0c36f17896 docs: credit Reverier-Xu/Ori-fcitx5 for fcitx5 theme inspiration (#3510) 2026-09-20 11:48:00 -04:00
Huỳnh Thiện Lộc 367f9f1e28 fix(dash): close add widget dropdown when exiting edit mode (#3511) 2026-09-20 10:51:50 -04:00
purian23 df71c65478 feat(workspaces): add lines & card indicator styles 2026-09-20 03:24:09 -04:00
purian23 c1feab857a refactor(bar widgets): show new widget style in all bar modes 2026-09-20 03:21:51 -04:00
purian23 b84fa410ce fix(modals): adjust focus handling in ClipboardHistoryModal & DankModalHost 2026-09-20 02:30:34 -04:00
dms-ci[bot] 5d65deeb37 nix: sync flake.lock to dank-qml-common submodule 2026-09-20 06:23:51 +00:00
purian23 a68344df60 fix(surfaces): bar follows global opacity & misc ajustments/fixes
- Standard bars respond to the global surface opacity again
- Island dash stays open when an edit-mode menu is used
- Weather forecast chip and island level groove follow the foreground opacity
- Control center tile icons follow blur and foreground settings like the slider icons
- Restored the layer outline on control center tiles and slider rows
- Hover popouts setting is visible for island bars
- Bumped dank-qml-common
2026-09-20 02:23:03 -04:00
purian23 5409c8738a fix(island): update dash editor clipping the handles 2026-09-19 22:58:00 -04:00
bbedward 60e79909c0 launcher: inset spotlight results and clip them to the frame radius
fixes #3498
2026-09-19 22:12:29 -04:00
SatoshoeandClaude Opus 5 84594e6449 settings: skip unchanged saves so external edits are not swallowed (#3504)
saveSettings() set _selfWrite before setText(). When the serialized
settings were identical to the file, nothing was written, no
fileChanged followed, and the flag stayed set. The next external edit
of settings.json was then taken for our own write and never reloaded.

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-09-19 21:46:34 -04:00
Aman9das c2c423e68e Add positron as a vscode fork (https://positron.posit.co/) (#3507) 2026-09-19 21:40:58 -04:00
dms-ci[bot] 2b55d81bb6 nix: update vendorHash for go.mod changes 2026-09-20 00:58:38 +00:00
bbedward 19b4593bfe dash: replace the tab bar with an m3 navigation bar and inline edit controls
wallpaper: allow SVG wallpapers, with a workaround for a matugen panic I
encountered on passing it an SVG - fixes #1350
2026-09-19 20:56:05 -04:00
dms-ci[bot] 67e5f24761 nix: sync flake.lock to dank-qml-common submodule 2026-09-19 21:08:53 +00:00
bbedward a5087735b5 theme: add fixed corner radius mode and rescale radius strength 2026-09-19 17:07:37 -04:00
bbedward aea35afdc7 settings: fix gating sound tab on sounds being available, other stuff 2026-09-19 16:22:59 -04:00
bbedward 4c65efc40b bar/media: fix icon scaling 2026-09-19 16:08:07 -04:00
bbedward f102a83d9f matugen: fix kde color scheme templates ignoring the accent 2026-09-19 15:59:53 -04:00
bbedward f8f1bfaaaa grids(cc+dash): make edit mode less opinioned, fix some minor bugs
fixes #3490
fixes #3489
2026-09-19 15:49:42 -04:00
Connor Welsh b5415caa7f bar: stop hidden pills from catching clicks (#3488) 2026-09-19 12:55:38 -04:00
Isaiah Centifanto c2dcee49a8 widgets: release hidden slideout input mask (#3500) 2026-09-19 12:54:57 -04:00
dms-ci[bot] 617bfcaca9 nix: update vendorHash for go.mod changes 2026-09-19 09:37:12 +00:00
purian23 77e60313ef feat(control center): add fractional step sizes to pill widgets 2026-09-19 01:52:24 -04:00
purian23 79abcdfd7b feat(theme): get expressive ~ new bold color palette options 2026-09-19 00:54:53 -04:00
purian23 2447ebec4f fix(bar): refresh frame mode geometry when changing positions 2026-09-18 23:42:02 -04:00
bbedward 3d1873c46e settings: remove leftover gates from interfaces tab 2026-09-18 18:14:27 -04:00
bbedward 10e9c070df settings: frame and island follow the bar 2026-09-18 17:59:06 -04:00
bbedward 7faed137f2 dash/controlCenter: keep the panel size in edit mode and move the resize ring outside 2026-09-18 17:30:18 -04:00
dms-ci[bot] 81d63410ed nix: update vendorHash for go.mod changes 2026-09-18 21:21:29 +00:00
dms-ci[bot] 0293891f41 nix: sync flake.lock to dank-qml-common submodule 2026-09-18 21:19:47 +00:00
bbedward ed77fea62e fix dankgo ref (i use replace= for local testing and accidentally pushed
it)
2026-09-18 17:18:57 -04:00
bbedward a8e8a22e43 core: use trash and desktop from dankgo 2026-09-18 17:07:32 -04:00
purian23 a3a9fe13eb feat(Hyprland): add native special workspace scratchpad support
Closes #1000
2026-09-18 16:21:54 -04:00
bbedward f0040ffcba dash/clock: fix styling when date is enabled
misc other tiny changes to lyrics and a bugfix in CC
2026-09-18 16:12:16 -04:00
arya satwika d036a3eba2 theme: improved readability on buttons (#3481) 2026-09-18 15:22:20 -04:00
bbedward 0784bdb87e workspaces: keep the hyprland slot for a workspace whose monitor is pending 2026-09-18 14:40:49 -04:00
bbedward 014c69b09a settings: tile color restyle and re-org, use materials own shift for
them towards theme
controlCenter: make a real grid, add numerous more tile varieties, add
icon size option
surfaces: fix elevation levels in general, and shift them down
settings: restyles and re-org
sliders: scale down universally
2026-09-18 14:32:55 -04:00
purian23 e5d8d03114 feat(workspaces): ability to adjust minimum workspace counts
- This adds native persistent slots per compositor such as Hyprland, Mango & Sway
- Not all compositors record preloading workspaces so those options return as minimum placeholders
2026-09-18 13:26:22 -04:00
bbedward 292a9595b3 bar: fix auto hide 2026-09-17 23:25:28 -04:00
dms-ci[bot] a534c37fcc nix: sync flake.lock to dank-qml-common submodule 2026-09-18 03:21:57 +00:00
bbedward f6c53993ac theme: stop painting selections with unreadable primaryContainer 2026-09-17 23:21:12 -04:00
bbedward c2c36d7e21 settings: softer onErrorContainer for sidebar red items 2026-09-17 22:13:22 -04:00
bbedward de8d16bba5 processlist: clip graphs inside their container 2026-09-17 21:32:18 -04:00
purian23 29463bbf27 feat(hyprland): add native tiling layout comp settings 2026-09-17 21:30:41 -04:00
bbedward c4b1f4b880 power menu: m3e restyle for list mode 2026-09-17 21:26:43 -04:00
bbedward bffeb252ee powermenu: fix blur/transparency token not applying 2026-09-17 19:46:47 -04:00
bbedward 39d42f850d bump qml-common
fixes #3475
2026-09-17 19:40:29 -04:00
bbedward eec0209415 settings: keep dashboard option cards alive across option changes
fixes #3476
2026-09-17 19:38:10 -04:00
bbedward 8334ed3c19 launcher: keep the hyprland focus grab when the category dropdown opens
and remove sticky header divider

fixes #3472, #3471
2026-09-17 19:32:39 -04:00
bbedward 1bb13d8bcf settings: keep the motion preview hint under the pill
fixes #3469
2026-09-17 19:15:29 -04:00
bbedward c5a76d9e46 island: make control center and dash panels resizable hosts
fixes #3474, #3470, #3468
2026-09-17 19:12:19 -04:00
bbedward a4b575c41a weather: clamp max size of humidity effect 2026-09-17 18:57:07 -04:00
bbedward 72326ec541 workspaces: read hyprland slot records for pill labels, icons and urgent state
fixes #3467
2026-09-17 18:54:36 -04:00
bbedward 4df80fa057 settings: keep page history when opening plugin settings from bar widgets
fixes #3466
2026-09-17 18:49:35 -04:00
dms-ci[bot] dcc7164b77 nix: update vendorHash for go.mod changes 2026-09-17 21:18:39 +00:00
bbedward b47e1d9bf1 i18n: sync 2026-09-17 17:15:39 -04:00
bbedward 8e9cd3f4a5 Squashed commit of the following:
commit f9bcdfdce1ab4a028cd9ccc08bc5fc6748e2d9e8
Author: bbedward <[email protected]>
Date:   Thu Sep 17 16:37:31 2026 -0400

    wallpaper: regenerate colors when the active light/dark wallpaper changes

commit 0b2dd37971a7b360f26b1a302297335963282313
Author: bbedward <[email protected]>
Date:   Thu Sep 17 16:16:03 2026 -0400

    lyrics: keep the active line in place when rows above resize, remember the lyrics view

commit db56722c7af9cb50a9c779a39e12a54dc5a607a6
Author: bbedward <[email protected]>
Date:   Thu Sep 17 16:04:33 2026 -0400

    lyrics: emphasize vocals per part instead of per group

commit 934e0676d9d63f6a7cb314c693ac256c1b56e057
Author: bbedward <[email protected]>
Date:   Thu Sep 17 15:59:56 2026 -0400

    lyrics: drop focus from overlapping lines when their vocals end

commit 3fe092446b1f27d7acc1a0a2a2ecc5d19f455275
Author: bbedward <[email protected]>
Date:   Thu Sep 17 15:17:28 2026 -0400

    media: add lyrics attribution, unison provider, shrink album art
    slightly when its low res

commit f54b3e2c1dc589451f449218d4985758b88757b4
Author: bbedward <[email protected]>
Date:   Thu Sep 17 14:35:52 2026 -0400

    media/zurvan: style changes

commit 9ea5aecfba4c638e2f08ab288706d2fb3143ea0b
Author: bbedward <[email protected]>
Date:   Thu Sep 17 14:02:03 2026 -0400

    rebase fixes

commit b2b04d12d2e9118346d0e5950b348aa222d58e50
Author: bbedward <[email protected]>
Date:   Thu Sep 17 13:46:03 2026 -0400

    i18n: overhaul
    battery popout: redesign
    clipboard: fix style of pinned button and remove number sidebar

commit 3057940bb28a29de22f90f5630a2c68113707d16
Author: bbedward <[email protected]>
Date:   Thu Sep 17 11:48:16 2026 -0400

    displayconfig: show valid fractional scales only, fix custom setter

commit 2bbe9e4b4c4278d1aca8632d650feba912a1c0af
Author: bbedward <[email protected]>
Date:   Thu Sep 17 10:47:43 2026 -0400

    media/zurvan: restyle

commit 33299ef45261c4f0a7e8e663c1ed2f0996e3ac78
Author: purian23 <[email protected]>
Date:   Thu Sep 17 02:50:08 2026 -0400

    refactor(clipboard): add click to open image previews & misc adjustments

commit 9809aad3c466bf4974b887a37a806039e58b47ed
Author: purian23 <[email protected]>
Date:   Thu Sep 17 02:35:20 2026 -0400

    add(media): dynamic sizing

    - tests: start the fixture bus with dbus-broker or dbus-daemon

commit 75a20eeb5a1299af1b0dd0d99cb374b4c5c8f15f
Author: purian23 <[email protected]>
Date:   Thu Sep 17 00:30:04 2026 -0400

    fix(dash): position bar-triggered popout once

commit 1536691beb0bf2cd58fa3e1b2106eeeb9317a39c
Author: purian23 <[email protected]>
Date:   Wed Sep 16 23:58:50 2026 -0400

    dash & cc: operation freedom

commit f3a2ae13bffbbdb92134af23138dc9a253f69e09
Author: bbedward <[email protected]>
Date:   Wed Sep 16 23:12:58 2026 -0400

    lyrics: keep track identity independent of mpris length

commit a59dd84908486f23e07172a6fca4c76eed27d31f
Author: bbedward <[email protected]>
Date:   Wed Sep 16 22:41:44 2026 -0400

    more zurvan changes

commit fda890225576e6fc740946486c5c05cdc58ebd77
Author: bbedward <[email protected]>
Date:   Wed Sep 16 22:10:55 2026 -0400

    media: more style changes

commit 2170f055e3700466f87c0f6a072cb67e5e610574
Author: bbedward <[email protected]>
Date:   Wed Sep 16 20:00:36 2026 -0400

    zurvan style tweaks

commit 10795b2f2c41fcd1c24f5625324ff0ea7dd3e4ae
Author: bbedward <[email protected]>
Date:   Wed Sep 16 18:32:48 2026 -0400

    media: adjust styling of zurvan player

commit 363eb10f7f53681d8f36fc6e9648205c9d5879d2
Author: bbedward <[email protected]>
Date:   Wed Sep 16 17:30:05 2026 -0400

    lyrics: dont classify 401s as failures

commit 0267c7110e14285fe4a611dbc78aee93e77e6ec8
Author: bbedward <[email protected]>
Date:   Wed Sep 16 17:07:34 2026 -0400

    media: create the dank lyrics engine with multiple providers and
    priority support
    media: add zurvan style player and make it default

commit 0aae835377cc96c5ec8383650df914058f604a42
Author: bbedward <[email protected]>
Date:   Wed Sep 16 10:22:36 2026 -0400

    remove dumb test

commit 8610121b1cb20b4b4b557170b7df52af39f4efff
Author: bbedward <[email protected]>
Date:   Wed Sep 16 10:16:00 2026 -0400

    notifications: redesign card header

commit 868b73740a575d51577fa3aaff5abc1ccd53043c
Author: bbedward <[email protected]>
Date:   Wed Sep 16 09:24:13 2026 -0400

    clipboard: tweak image preview style

commit 6de9a1ba7c306269889227b892703dc822049ac9
Author: purian23 <[email protected]>
Date:   Tue Sep 15 22:52:40 2026 -0400

    clipboard: new size variants & image previews

commit 05909190a2fb93a3a2c68fc711c07c3ffc09a529
Author: bbedward <[email protected]>
Date:   Tue Sep 15 19:14:41 2026 -0400

    qs: drop the delegates the layout and output migrations left behind

commit 6ca48caf7c2141e7e5f55a7d4634e0803a90c6b9
Author: bbedward <[email protected]>
Date:   Tue Sep 15 19:03:03 2026 -0400

    config: one implementation of the compositor fragment mechanics

commit 9f024c001e0c661a19c35b92bb36a6a9758f9d4d
Author: bbedward <[email protected]>
Date:   Tue Sep 15 18:34:03 2026 -0400

    compositor: name capabilities instead of checking identity in the ui

commit 90067f14ed38e0cbf919e35cbf333f62240bc585
Author: bbedward <[email protected]>
Date:   Tue Sep 15 18:19:57 2026 -0400

    settings: read every default from the spec

commit 7f7ae95ed3093c5df7cb4bf5d23b718497a769ff
Author: bbedward <[email protected]>
Date:   Tue Sep 15 15:47:13 2026 -0400

    notifications: watch screen sharing from the service side

commit a470ed448cbddb849922e80afdd2b13eb85f08f9
Author: bbedward <[email protected]>
Date:   Tue Sep 15 15:15:38 2026 -0400

    rebase fix

commit dc8f05fa0ca4650a40e32fb59614ca1b7df75f38
Author: bbedward <[email protected]>
Date:   Tue Sep 15 10:57:38 2026 -0400

    dash: keep cards still when edit mode opens

commit bc2001359613fd9b47ba0000a8f8f081d412a166
Author: bbedward <[email protected]>
Date:   Tue Sep 15 10:16:48 2026 -0400

    settings: own the full spec again and merge the shared greeter keys
    settings: drop sidebar hints that repeat their own label
    cc/dash: change edit interface design slightly

commit ee963f54b259eced7ec23701194539441b264db0
Author: purian23 <[email protected]>
Date:   Mon Sep 14 23:43:56 2026 -0400

    settings: update island notification handling to use bar context

commit 4d6f9fde4401001625f0352f97821ce5034a7e1c
Author: bbedward <[email protected]>
Date:   Mon Sep 14 23:21:28 2026 -0400

    greeter: read the shared settings schema instead of a duplicated one

commit 70e4bdaec84efa3d68438ef235e46762cee38101
Author: bbedward <[email protected]>
Date:   Mon Sep 14 22:42:31 2026 -0400

    osd, toast, launcher: start open motion after onFrameSwapped signal

commit ddfd28b7be6dd4b0940512897d835107dbab1f23
Author: bbedward <[email protected]>
Date:   Mon Sep 14 21:12:37 2026 -0400

    notifications: wait for onFrameSwapped signal before showing
    notification popup

    Lets the layer-surface map before rendering the body, resulting in
    better animation appearance

commit 24efe6f8a18b36bb42c6b399086ffa5c5e2aa9c5
Author: purian23 <[email protected]>
Date:   Mon Sep 14 18:55:46 2026 -0400

    notifications: adjust animation durations

    - Mapped the layer-shell surface
    - Fixed group chevron/dismiss button

commit 52389ecf5cc586e912c26a345004814e37f4048a
Author: bbedward <[email protected]>
Date:   Mon Sep 14 18:10:33 2026 -0400

    dash: spacing tweak of tabbart

commit 0285a70c1b07d09558c450c1eeb925a5c903069f
Author: bbedward <[email protected]>
Date:   Mon Sep 14 17:55:43 2026 -0400

    blah

commit a1abc7a8df94032b01b51189b214aa21543f7218
Author: bbedward <[email protected]>
Date:   Mon Sep 14 17:40:10 2026 -0400

    shrink window CSD height

commit db20f7e9bb93c7dce503e410e82e15e7a03b1e7d
Author: bbedward <[email protected]>
Date:   Mon Sep 14 17:16:16 2026 -0400

    windows: center titles

commit 7268712bdf58c5c1903606f24a392d64655a4ad5
Author: purian23 <[email protected]>
Date:   Mon Sep 14 16:45:44 2026 -0400

    notifications: updated entry/exit paths

    - Added popout option in island mode
    - Fixed cards getting stuck & out of place when multiples arise at once

commit 3a3f0b3061bd0a7ce895152ec5a056600e41e7e8
Author: bbedward <[email protected]>
Date:   Mon Sep 14 16:34:36 2026 -0400

    battery: make cc variant consistent

commit ee569ddef3bf2875d42347d18b57f493f492b243
Author: bbedward <[email protected]>
Date:   Mon Sep 14 16:15:42 2026 -0400

    battery: remove bottom text footer thing

commit 817c4e46eac52c95d91f418e3ddbafdab0888b5c
Author: purian23 <[email protected]>
Date:   Mon Sep 14 15:34:18 2026 -0400

    settings: rework interface style and bar hub layouts

    - Separate global surface styling from per-surface overrides on Interface style
    - Move bar management into the hub header and render island settings inline
    - Cut redirect banners down to one per page and point them at the right row

commit 03a4e45d65aec83c927c7419a4a0f3f252e0e955
Author: bbedward <[email protected]>
Date:   Mon Sep 14 14:52:20 2026 -0400

    launcher: fix inconsistency in niri overview variant

commit e74246d058a5db215200606eb9f9c662706d8689
Author: bbedward <[email protected]>
Date:   Mon Sep 14 14:21:10 2026 -0400

    plugins: eval symlinks in repo lookup

commit 7dd2cddebf4819da51ff7da792560372f5d9a03e
Author: bbedward <[email protected]>
Date:   Mon Sep 14 09:09:46 2026 -0400

    update common

commit a8c1f77fe2d952b1ec33e622b2951e5382860c9e
Author: purian23 <[email protected]>
Date:   Mon Sep 14 02:05:09 2026 -0400

    island: fix search input focus on launcher

commit 2d1f8bf5338ca5e0a4a58f0f18b54b92dfe5e5ee
Author: purian23 <[email protected]>
Date:   Mon Sep 14 01:29:05 2026 -0400

    tooltips: add further label & accessibility parity

commit 3ebfe85d57553b4803b52d5e5ea417b8abec5bcf
Author: bbedward <[email protected]>
Date:   Sun Sep 13 23:28:29 2026 -0400

    ui: drop tooltips from obvious icon buttons, keep labels on Accessible.name

commit a8e7a072b2bad394c5c5d9fd60eaff5240ce37d4
Author: bbedward <[email protected]>
Date:   Sun Sep 13 22:55:28 2026 -0400

    widgets: dim filled action buttons when disabled

commit 0dc58825344ebfc5479d2a3bc75b5d2ae014131a
Author: bbedward <[email protected]>
Date:   Sun Sep 13 22:45:34 2026 -0400

    settings: one compositor include workflow for outputs, layout, input, window rules, cursor and binds

commit 9fc8c8e53c18de18b33ccdfe1e1abed84e01b219
Author: purian23 <[email protected]>
Date:   Sun Sep 13 22:27:28 2026 -0400

    theme: add focus ring settings & replace text fields w/search fields aligned to m3e specs

commit 8359fea68230172b1c10df08831efdf944fbbd64
Author: bbedward <[email protected]>
Date:   Sun Sep 13 20:37:49 2026 -0400

    auth: share one pam stack parser between the greeter, lock probe and polkit

commit bf05136e78aada16e41a7d0b7164a7f543d65760
Author: bbedward <[email protected]>
Date:   Sun Sep 13 20:24:01 2026 -0400

    tests: replace golden files and dump-only fixtures with hand-written assertions

commit f2cd6a877b561a5078e7b6cbad8348e9165158ca
Author: bbedward <[email protected]>
Date:   Sun Sep 13 19:15:02 2026 -0400

    display: split DisplayService into brightness, night mode and refresh rate services

commit fa7ce5fe836e4f00646c80eb3cb50ecc004f4524
Author: bbedward <[email protected]>
Date:   Sun Sep 13 18:19:26 2026 -0400

    battery: m3e redesign

commit 04576106158c009374d9564d18852cf7ea1f9284
Author: bbedward <[email protected]>
Date:   Sun Sep 13 17:21:32 2026 -0400

    shell: one window model and one keyboard layout source
    hosts: put change handlers on the observed object instead of proxy properties

commit e4750c767b27d4650b95d42930410ad090d6ca87
Author: bbedward <[email protected]>
Date:   Sun Sep 13 15:39:10 2026 -0400

    shell: replace connections with bound property handlers in per-item and per-screen components

commit 8efb05dc014f0d09b36d6112b03e6f4e1f60d1d2
Author: bbedward <[email protected]>
Date:   Sun Sep 13 13:43:10 2026 -0400

    display config: move output parsing, identifiers and geometry into a pure model

commit e9b8174812b2da316eeae42e3989f2f269ef5649
Author: bbedward <[email protected]>
Date:   Sun Sep 13 12:52:46 2026 -0400

    slideout: build content on first open

commit b899f307650cf5cc646de90ac666b6e1731bbc4f
Author: bbedward <[email protected]>
Date:   Sun Sep 13 12:28:11 2026 -0400

    services: stop using weakmap for workspace, window and term caches

commit b90e9a4c2a72744685f79cf613ab80660721389f
Author: bbedward <[email protected]>
Date:   Sun Sep 13 10:35:48 2026 -0400

    workspaces: one model with per-compositor adapters, no compositor branches in widgets
    shell: stop hot reload while the session lock is held

commit e200e17e96e06b4fd7e0dd596b50dcbbc254494a
Author: purian23 <[email protected]>
Date:   Sun Sep 13 02:36:41 2026 -0400

    dock: mix widgets between pinned apps

    - Pinned apps become individual units in edit mode with a saved per-dock order, while running windows stay one unit in compositor order.
    - Fix the Window.window Connections crash
    - Add right-click-to-edit empty dock space option and dock edit / editFor ipc commands

commit cad3ba78727695419a4fe8a96c90146bbf5e6120
Author: bbedward <[email protected]>
Date:   Sun Sep 13 00:51:35 2026 -0400

    launcher: create plugin instances only when a query needs them

commit aed25c3c75d0c24a17a940c162a6956a372c4e0f
Author: bbedward <[email protected]>
Date:   Sat Sep 12 23:45:19 2026 -0400

    settings: rebuild about, printer, wifi, display and window rules pages on settings blocks
    popouts: drop inconsistent headers

commit 3a20c2c9528fe1fed3464085492af3bebdcdf809
Author: bbedward <[email protected]>
Date:   Sat Sep 12 17:41:18 2026 -0400

    launcher: consolidate host code and logic

commit 4e53b470d5beaffa54687c8164e492ee41472e49
Author: bbedward <[email protected]>
Date:   Sat Sep 12 15:37:44 2026 -0400

    surfaces: collapse connected/standalone popout and modal hosts
    typography: make font weight work and follow m3 weights
    settings: fix reset to default shifting rows

commit ce71b5dc3ca0e96ee11b906b1ea3399d3d7d93ac
Author: bbedward <[email protected]>
Date:   Sat Sep 12 15:02:25 2026 -0400

    dock: move widgets, launcher, apps and trash as units in edit mode

commit 6ebaef52d885744a45f3db7d48c3b6d0924993b2
Author: bbedward <[email protected]>
Date:   Sat Sep 12 13:24:31 2026 -0400

    plugins: add examples for dash and dock

commit 067cabc95991f5676a19746d3cad0700eb525082
Author: bbedward <[email protected]>
Date:   Sat Sep 12 12:25:59 2026 -0400

    settings: fix v18 migration

commit 55d07f3bba352972c768acdd7b6705f80fac4e0f
Author: bbedward <[email protected]>
Date:   Sat Sep 12 12:09:56 2026 -0400

    ci: fix fails

commit 2e81531dd6980f0331af80fe5beca1d695c58f06
Author: bbedward <[email protected]>
Date:   Sat Sep 12 11:49:39 2026 -0400

    surfaces: add windowRadius token to make surfaces actually follow
    settings and match the compositor windows
    surfaces: change default radius (Strength 50) from 12->16
    settings: add result to default for everything
    settings: further re-organization and re-structuring position of things
    settings: remove dead keys
    dock: remove hardcoded border and respect border setting
    bar: add a third style and color options
    ci: hookup QML testing

commit a2fd8abd37e0ff1ef67af9bfa4e7679e7aa338bd
Author: bbedward <[email protected]>
Date:   Sat Sep 12 09:20:00 2026 -0400

    dash: size surface width to the width of the largest tab

commit 93394549a2cb3f7647b564ba71621e113c83e106
Author: bbedward <[email protected]>
Date:   Sat Sep 12 00:11:15 2026 -0400

    bar/workspaces: restore old style, respect color choices, make pills
    default  > segments

commit 71f8351a3b11f8d06e45dd28db4d501e54a27f9a
Author: purian23 <[email protected]>
Date:   Fri Sep 11 23:48:04 2026 -0400

    dbar: initial M3E remaster pass on shared chrome w/dock & island

commit 1711716073f660afd519ab0af88d3c8177f1a8c8
Author: bbedward <[email protected]>
Date:   Fri Sep 11 21:44:31 2026 -0400

    dash: only show focus ring when KB navigating

commit 6037de06471e7fa80ec36088e9e86cbf2e4d3941
Author: bbedward <[email protected]>
Date:   Fri Sep 11 21:41:53 2026 -0400

    launcher: scope plugin and clipboard refreshes, paint the keystroke before plugin search

commit 78c9ab990b7665e76762dadd65d1a1d23275a405
Author: bbedward <[email protected]>
Date:   Fri Sep 11 21:16:36 2026 -0400

    launcher: one open-session reset, faster keystroke settle

commit 01b15051363715bfb2bfe1d905b4b573fb4e49ac
Author: bbedward <[email protected]>
Date:   Fri Sep 11 20:47:45 2026 -0400

    dash: fix kb navigation  on overview

commit 27465074e1fa395a844cc3b804a362ee7a4e4f70
Author: bbedward <[email protected]>
Date:   Fri Sep 11 16:58:53 2026 -0400

    dash: replace material shapes on resource cards with a ring gauge

commit 5e41b85a1df69885c57a41ab44a43ef38b0c5560
Author: bbedward <[email protected]>
Date:   Fri Sep 11 16:33:48 2026 -0400

    qml: share bases for registry browsers, fade windows and plugin list settings

    Fixes fade window focus, list setting load write-back and the missing Theme.onError

commit be3542ab8fc1949a256d6498b5c4a0379b86e023
Author: bbedward <[email protected]>
Date:   Fri Sep 11 16:11:15 2026 -0400

    popout: fix pixel jumps

commit 2ad0e9e2f5b12f13f0acc9733c29c4c42d640df7
Author: bbedward <[email protected]>
Date:   Fri Sep 11 15:52:52 2026 -0400

    popout: fade the surface fill with the content

commit 4b11e21b1a09895f67ca1b6b858fe772500e4a2c
Author: bbedward <[email protected]>
Date:   Fri Sep 11 15:13:14 2026 -0400

    desktop widgets: read and write saved geometry through one path

commit 21ed01bbb20627d4d68bbb704b9f2bb50b45c911
Author: bbedward <[email protected]>
Date:   Fri Sep 11 15:10:02 2026 -0400

    surfaces: bind modal and popout backends to their handle

commit 362859d810b768621061f0429c39b17ff3b00184
Author: bbedward <[email protected]>
Date:   Fri Sep 11 14:30:27 2026 -0400

    notifications: move the popup context menu onto DankContextMenu

commit 8942b622f5929e854fbcf145e7f34f4637904731
Author: bbedward <[email protected]>
Date:   Fri Sep 11 14:20:49 2026 -0400

    colorpicker: design changes
    consolidate context menus

commit 29be554d488aefd9a8f6e4e5f5d094147e3b214d
Author: bbedward <[email protected]>
Date:   Fri Sep 11 12:48:02 2026 -0400

    dash: add analog clock option

commit 43748726983a466e19e4b642399ea040d4095de9
Author: bbedward <[email protected]>
Date:   Fri Sep 11 12:12:51 2026 -0400

    popouts: fix spacing inconsistencies
    dash: redesign overview media card
    osd: more redesign
    dash/overview: remove row cap and make it dynamic

commit 14a3a315d94336cac6e8eb77e08965503717d780
Author: bbedward <[email protected]>
Date:   Fri Sep 11 09:23:53 2026 -0400

    shell: dedupe bar ipc handlers, weather resets, color options, launcher icons

commit 9f2b6d4bbe6f5954a33b5853ecea0ef0ac4b483e
Author: purian23 <[email protected]>
Date:   Thu Sep 10 23:52:39 2026 -0400

    dock: add spacing option & update defaults

commit 3e6cb55488a42ade23728bcf4d8608efab2e6e0f
Author: bbedward <[email protected]>
Date:   Thu Sep 10 23:29:07 2026 -0400

    session: collapse the monitor cycling setters onto one helper

commit 613e756d8bf88b3c2e6ae7edcede4c680da72456
Author: bbedward <[email protected]>
Date:   Thu Sep 10 21:56:38 2026 -0400

    dock: share the hover bounce animation across dock buttons

commit 8d8f801874ddf5dcc3b299405de458183e758c7c
Author: bbedward <[email protected]>
Date:   Thu Sep 10 21:46:44 2026 -0400

    settings: share one picker window for bar and desktop widgets

commit e3c90efbce4d7d11adf0cceefb8e9758e7d9099c
Author: bbedward <[email protected]>
Date:   Thu Sep 10 21:24:10 2026 -0400

    network: merge wired and wifi info modals

commit 43f75e6a3dfc988f3adc8c7d659fff5529ed6db8
Author: purian23 <[email protected]>
Date:   Thu Sep 10 21:22:06 2026 -0400

    island: unify new dashboard dynamic sizing & activities

    - New wallpaper carousel and Colum/Row options
    - Fixed island controller overlap in bar widget settings
    - Fixed island widgets click to hide activity
    - Expanded island IPC cyle targets

commit 805611efca91bea029894436dad9baa6dd492461
Author: bbedward <[email protected]>
Date:   Thu Sep 10 19:40:27 2026 -0400

    bar: share context menu window and tooltip offset across widgets

    also fix Media art glitches and things

commit 17a08c4c7e56312d0b3a7732978223f569828aaf
Author: bbedward <[email protected]>
Date:   Thu Sep 10 19:00:48 2026 -0400

    osd/media: respect follow album accent option

commit 6a753a7ab6304568f178bc3ec0910a9132020dd2
Author: bbedward <[email protected]>
Date:   Thu Sep 10 18:57:15 2026 -0400

    theme: fix surface elevation

commit af4f44eba649923bfb0928423a24fc6d115518c4
Author: bbedward <[email protected]>
Date:   Thu Sep 10 18:45:09 2026 -0400

    m3e: redesign OSDs, Forms, etc.

commit 1d2add180a661157b3c06a1221edae21bdbcb765
Author: bbedward <[email protected]>
Date:   Thu Sep 10 17:19:07 2026 -0400

    rebase fixes

commit ac39a7745cfe88bcfd3826f64cfd55ed41b18b3e
Author: bbedward <[email protected]>
Date:   Thu Sep 10 16:51:16 2026 -0400

    tray: share one item delegate and icon for both bar orientations

commit 642eeb0f4b5d0a912fc44c134fc10adf8a5b5291
Author: bbedward <[email protected]>
Date:   Thu Sep 10 16:33:36 2026 -0400

    bar: share one delegate for running apps in both orientations

commit 7f69e42f3bef9025d411acfc768deb3865338922
Author: bbedward <[email protected]>
Date:   Thu Sep 10 15:56:33 2026 -0400

    bar: share one monitor pill for cpu, ram, temp and disk widgets

commit f5507dc3b2df6929521e87cd820eb45a3e3077ad
Author: bbedward <[email protected]>
Date:   Thu Sep 10 14:03:00 2026 -0400

    island: reuse widget model, surface palette and layout metrics in hosts

commit aceac03ba0c9a2b0aa1a47f1b62a380fc785ec6d
Author: bbedward <[email protected]>
Date:   Thu Sep 10 13:36:47 2026 -0400

    osd: share one level osd for volume, brightness, mic and media

commit 1b4a68d10c5b59f3ecf3e7c44974a2cb83964246
Author: bbedward <[email protected]>
Date:   Thu Sep 10 13:12:30 2026 -0400

    shell: consolidate connected surface state into one slot store

commit d7edc23cf14a10cd275fdf3888d24482d43a15f3
Author: bbedward <[email protected]>
Date:   Thu Sep 10 12:19:30 2026 -0400

    shell: restore centered dash popout for center-section widgets

commit d2f9405de9e7ade7db402826cfb0520249c58a2b
Author: bbedward <[email protected]>
Date:   Thu Sep 10 12:11:29 2026 -0400

    dash/media: always show placeholder player when no players are playing

commit 55535082f53163914440e1e858aa78246bc4e40a
Author: bbedward <[email protected]>
Date:   Thu Sep 10 12:08:10 2026 -0400

    dock: fix sizing, exclusive zone, edit mode and multi-instance settings

commit c82cc738ca2d20e7a4363b7abb86ad6d9e7bb172
Author: bbedward <[email protected]>
Date:   Thu Sep 10 11:08:40 2026 -0400

    settings/plugins: redesign page

commit 9c283e0f6c4b7d812362124fdd36fc15327ef03c
Author: bbedward <[email protected]>
Date:   Wed Sep 9 20:44:26 2026 -0400

    shell: route widget instances and support same-edge layouts

commit 405a3b397c264688b2271d6acec881a4aea4864f
Author: bbedward <[email protected]>
Date:   Wed Sep 9 18:05:37 2026 -0400

    shell: share bar content and clock presentation

commit 65a6d2df8e870af259b1542c896fce60be4f6cb6
Author: bbedward <[email protected]>
Date:   Wed Sep 9 15:49:23 2026 -0400

    shell: centralize layout ownership

commit 585aeb4e1cba737cc4007fc214ac1e109d3e0f19
Author: purian23 <[email protected]>
Date:   Wed Sep 9 18:05:13 2026 -0400

    dock: initial m3e remaster

commit a2e646f2fb364f042fb134e74e9014386b80971b
Author: bbedward <[email protected]>
Date:   Wed Sep 9 14:58:38 2026 -0400

    plugins: consolidate upgrade path

commit 139253504c5f62d522fbc6109689c11ccaec7493
Author: bbedward <[email protected]>
Date:   Wed Sep 9 14:43:28 2026 -0400

    settings: use icons on all outlined text fields

commit a9c1e569b8428e4f3748da345200e8a62c90ba20
Author: bbedward <[email protected]>
Date:   Wed Sep 9 14:32:33 2026 -0400

    launcher: fix KB navigation focus and modes

commit ac64eb52d360322a0e952844b41b58532d5ae32f
Author: bbedward <[email protected]>
Date:   Wed Sep 9 14:05:10 2026 -0400

    m3e: remove inconsistent widget usage, adjust SystemUpdater rredesign,
    fix window decoration inconsistencies

commit 0eac1d3e22cb879c7b7e9205fba0ed9c17b89dcd
Author: bbedward <[email protected]>
Date:   Wed Sep 9 13:25:44 2026 -0400

    server: consolidate ipc routing and subscription forwarding

commit ed289e5c2c867eb2d6e1319d9517885e6e80d1a3
Author: bbedward <[email protected]>
Date:   Wed Sep 9 12:56:29 2026 -0400

    plugins: redesign management and browsers with m3 expressive

commit 7f3a526e2d38c24edbb5d51c4c9c550a0bc93752
Author: bbedward <[email protected]>
Date:   Wed Sep 9 11:00:07 2026 -0400

    settings: fix inconsistent ripple effect radius

commit 8f1fb8a11e92b1811808325c136199fef10d4a53
Author: bbedward <[email protected]>
Date:   Wed Sep 9 10:26:39 2026 -0400

    aqueous: fix variant terms

commit 2ba861947092cb657d983702c842e88ffc6fb0fa
Author: bbedward <[email protected]>
Date:   Wed Sep 9 10:01:50 2026 -0400

    running apps: fix delegate bug

commit b37c925e85cb6902bbb3005d92a5b522e3648116
Author: bbedward <[email protected]>
Date:   Wed Sep 9 00:18:57 2026 -0400

    bump dml-common

commit 5e83c40c56c624df74306efc839d707ec8d6aa14
Author: bbedward <[email protected]>
Date:   Wed Sep 9 00:12:16 2026 -0400

    settings: incorporate user & accounts tab, consolidate security, add M3E
    textfield

commit 278370c7909226ca42f0a53cbd9ca2bd5f2fb4e7
Author: bbedward <[email protected]>
Date:   Tue Sep 8 18:17:25 2026 -0400

    m3e: unify shell surfaces and refresh process monitoring

commit e2d5860d3952da717ba9073e857133f2538a70b0
Author: bbedward <[email protected]>
Date:   Tue Sep 8 15:15:17 2026 -0400

    notifications: fix expansion timing and simplify the header

commit d8f29c221474c2884ec482d26c827de09d581539
Author: bbedward <[email protected]>
Date:   Tue Sep 8 14:53:11 2026 -0400

    icon: install app icon under the conventional name (app id)

commit ebeebb70fd4c7538ef5ea867116d9fa7118eee27
Author: bbedward <[email protected]>
Date:   Tue Sep 8 14:47:09 2026 -0400

    notifications: refine card styling and separate app icons from image previews

commit d6133d93ed314ca02a9a3b634872ee32484a0bb3
Author: bbedward <[email protected]>
Date:   Tue Sep 8 14:27:18 2026 -0400

    settings: unify expressive rows and drag and drop

commit 0a4d14752a2a1e8dd60b3056fd006d02d818d6a4
Author: bbedward <[email protected]>
Date:   Tue Sep 8 12:32:16 2026 -0400

    settings: use native action button hover state

commit dc8c4564cc964ab50ed28a3e9e4ca7b13ebfbcb1
Author: bbedward <[email protected]>
Date:   Tue Sep 8 09:55:42 2026 -0400

    shell: refine weather, slider controls, and tooltips

commit bec5c6ad0119e22a3cb69975d135d3b72654f77e
Author: bbedward <[email protected]>
Date:   Mon Sep 7 18:43:39 2026 -0400

    theme: tweak token values

commit 8519cbc4bbb396a8b6779764ff0f5a0945156dcf
Author: bbedward <[email protected]>
Date:   Mon Sep 7 13:43:53 2026 -0400

    dankdash: fix grid sizing, controls, notifications, and popout blur

commit f2405f2925aa63eba480cff4ddc55119818f8057
Author: bbedward <[email protected]>
Date:   Mon Sep 7 10:38:07 2026 -0400

    dash: redesign media player and migrate its appearance options

commit cb57ce04fbd50920a17d9a8ad33e4d20fc856021
Author: bbedward <[email protected]>
Date:   Mon Sep 7 09:07:01 2026 -0400

    lock: add lock m3e redesign

commit e89909e3743993dad1bdf17d57ab17f1122dfa3c
Author: bbedward <[email protected]>
Date:   Sun Sep 6 21:24:48 2026 -0400

    qs: more loaders, less repetition

commit 288335fef67b523f51e22bb234c81bd8b88e7e2c
Author: bbedward <[email protected]>
Date:   Sun Sep 6 20:16:41 2026 -0400

    launcher: initial m3e redesign

commit 5b7fc75c473491bf086b65a41416325697ae330c
Author: purian23 <[email protected]>
Date:   Sun Sep 6 19:50:14 2026 -0400

    feat(motion): new fluid animation variant

    - Refactored Popouts/Modals for performance passes

commit 0fc4f51f4d0b1d297c1886b19be8062d279c505b
Author: bbedward <[email protected]>
Date:   Sun Sep 6 15:36:22 2026 -0400

    notifications: tweak animnation stacking behavior on popups

commit 4369233794e3bebd890a2b830c6450d0a65eb31a
Author: bbedward <[email protected]>
Date:   Sun Sep 6 15:15:55 2026 -0400

    notifications: initial M3E redesign

commit eb67a51779c543038ad885ecd406bfe21aecd1e2
Author: bbedward <[email protected]>
Date:   Sat Sep 5 15:10:37 2026 -0400

    ddash: exhaustive overhaul of even more pieces of the system

commit 14554a39effd677206f7a3c4b5944d2ae47c2b03
Author: bbedward <[email protected]>
Date:   Sat Sep 5 12:47:06 2026 -0400

    settings: rebase fixes

commit d6e0bb604bdb15ed94d77f9e1d85a26381ed6836
Author: bbedward <[email protected]>
Date:   Sat Sep 5 10:18:38 2026 -0400

    ddash: initial rewrite

commit 29903114d0d8849b25c6f6e9161765b0ad03da49
Author: bbedward <[email protected]>
Date:   Fri Sep 4 16:48:23 2026 -0400

    cc: tweak s lider size

commit 8d14a07864972e4b705e77b371d21f5fdb7c4c07
Author: bbedward <[email protected]>
Date:   Fri Sep 4 16:44:53 2026 -0400

    cc: initial M3E re-design

commit 976f968967166c61cec423f3bb3f7a28a5a9ac76
Author: bbedward <[email protected]>
Date:   Fri Sep 4 15:00:35 2026 -0400

    settings: continue to iterate

commit a99a1c5ad183e34435a20dd90f19ed84099a1070
Author: bbedward <[email protected]>
Date:   Fri Sep 4 09:44:55 2026 -0400

    stash m3e work
2026-09-17 16:51:27 -04:00
bbedward 9d5114d295 bluetooth: stop publishing mpris snapshot on position ticks 2026-09-17 13:17:23 -04:00
Maximilian Schmidt 8c3d3c653c bluetooth: route headset controls to the active player (#3273) 2026-09-17 13:14:09 -04:00
bbedward 0d81d0fc2e launcher: keep open when a dropdown menu opens on hyprland
fixes #3451
2026-09-17 10:03:30 -04:00
bbedward 36bb62ac66 niri: fix workspace updates never applying
fixes #3462
port 1.6
2026-09-17 09:50:44 -04:00
bbedward 49a639a358 qt: write real newlines in new qt5ct/qt6ct config
fixes #3457
port 1.6
2026-09-17 09:46:13 -04:00
Marlon Kranz 579d6f7b5c fix(frame): keep launcher edge-hover on the Top layer (#3453)
Opening the launcher by hovering the frame edge requested the Overlay
layer via CompositorService.framePeerSurfacesUseOverlayForScreen(),
which resolves to whether the frame is configured for the screen and
is true in normal use. DankLauncherV2ModalConnected's
frameOwnsConnectedChrome requires the Top layer, so the overlay
request silently dropped connected-chrome anchoring: the launcher
opened dead-centered with a different animation instead of emerging
from the configured frame side.

The keybind/bar-button path already opens on the Top layer by passing
no overlay argument. The edge-hover trigger only runs in connected
frame mode to begin with (gated by usesConnectedFrameChromeForScreen
in this file's own eligible check), so there's no case where it needs
standalone/overlay behavior. Match the keybind path.
2026-09-17 09:37:39 -04:00
Domen Kožar 1bf35426ea tray: use available width for menu labels (#3456) 2026-09-17 09:37:08 -04:00
Alexander 81d5962650 dock: fix overview visibility on niri (#3460) 2026-09-17 09:36:46 -04:00
bbedward 641d65cbd9 dank16: use the real primary_container and fix light-mode derivation
fixes #3405
fixes #3406
ref #3408
port 1.6
2026-09-16 10:05:30 -04:00
dms-ci[bot] 8c975630ef nix: update vendorHash for go.mod changes 2026-09-16 13:30:12 +00:00
bbedward c9506bc847 core: bump dankgop
port 1.6
2026-09-16 09:27:35 -04:00
Mattias 6ecdd6d45d privacy: stop counting screen captures as camera use (#3428)
Apps capturing the screen through PipeWire, such as OBS, create a live
video input stream, which the camera check counted as camera use. Skip
streams tagged with the Screen media role.
2026-09-15 15:01:06 -04:00
447997b9ec launcher: add vpn connections as a search provider (#3391)
* launcher: add vpn connections as a search provider

Adds a dms_vpn built-in launcher plugin that lists every configured VPN
profile and toggles it on enter. Connected rows show a lock icon and a
Connected badge, in-progress rows show Connecting, everything else shows
a crossed-out key.

Profiles come from the network state the shell already fetches at
startup, so there is no new timer, process or request. The provider
returns nothing when the backend is not NetworkManager.

It ships without a trigger. A trigger prefix replaces the whole result
set, so claiming "vpn" would have hidden every application whenever
someone typed it to find their VPN client. Matching on the vpn keyword
instead keeps apps and profiles in the same list.

Co-Authored-By: Claude Opus 5 <[email protected]>

* launcher: hide vpn profiles on the home view without a trigger

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* launcher: list vpn profiles when browsing the plugin

The empty query guard also applied when opening VPN from the Browse
section, so the plugin view came up empty. The plugin filter path now
opts out of the guard, while the home view still hides the profiles.

Co-Authored-By: Claude Opus 5 <[email protected]>

* launcher: stop matching vpn profiles on connect and disconnect

Keywords match by substring, so "connect" also matched "disconnect" and
listed connected profiles too. Picking one of those disconnected it.

Co-Authored-By: Claude Opus 5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-09-15 12:41:14 -04:00
MattiasandClaude Opus 5 2c136b6c10 notifications: auto-enable do not disturb while screen sharing (#3316)
Turn on do not disturb while a screen share is active so notification
popups and their contents stay off the shared screen. The unread dot and
the existing dnd bypasses are unaffected.

Detection reuses the screen sharing privacy indicator: niri cast ipc,
otherwise pipewire and xdg-desktop-portal. Neither source says when it
has finished reporting, so a share reading as gone is only acted on once
it stays gone for four seconds. Engaging stays immediate, and turning the
setting off releases right away since that is a user action rather than a
detection guess.

Turning dnd off by hand during a share is remembered against the sources
that were sharing at the time, so it holds for that share and across a
shell reload. A share started after that one has ended gets a fresh
decision.

Co-authored-by: Claude Opus 5 <[email protected]>
2026-09-15 12:37:24 -04:00
bbedward 6e9a657132 evdev: open input devices read-only
go-evdev default is O_RDWR but we dont need to open evdev devices RW
2026-09-15 12:35:18 -04:00
bbedward 1ef38adddf i18n: sync 2026-09-15 12:01:42 -04:00
Minghao YUandbbedward e809733e3a feat(display): per-output ICC color profiles and color temperature (#3388)
* feat(display): per-output ICC color profiles and color temperature

Rebuild of #3388 on current master: the previous branch was cut from an
older base and reverted unrelated fixes in the gamma stack and the display
settings UI.

ICC profiles:
- core/internal/icc: ICC v2/v4 parser (TRC, LUT8/16 and parametric curves,
  vcgt, description/version/color space) plus gamma ramp generation, with
  synthetic coverage and optional vendor-profile tests (ICC_TEST_DIR).
- wayland manager: load iccProfiles from the compositor's DMS config dir,
  build the ramp from the profile when one is set (falling back to the
  temperature ramp when it is missing or unparsable), and re-apply on
  output hotplug and after resume.
- ICC ramps apply while the night light schedule is disabled; outputs
  without a profile keep an identity ramp.
- desc/mluc offsets are validated before slicing, so a malformed profile
  returns an error instead of panicking the daemon.

Per-output temperature:
- outputTemps in the same config; SetOutputTemp accepts 1000-10000K and the
  Display Config slider exposes 3000-10000K, which matters for displays
  calibrated at a higher white point (e.g. a 7000K profile).

Gamma control lifecycle:
- needsControls() now keeps the controls alive for configured ICC profiles
  and per-output temperatures, so toggling the night light off no longer
  destroys the controls and drops the ramps.
- Re-apply paths clear the per-output dedup state instead of adding a
  manager-level guard, which would have suppressed contrast-only writes and
  the post-resume forced resend (#1235), and left a recreated control
  without a ramp.
- Monitor sleep/disconnect still re-establishes controls when an output
  comes back.

IPC / CLI / UI:
- wayland.icc.{getStatus,apply,remove,listOutputs,setTemp,getTemps}
- dms icc list | info <file> | apply <output> <file> | remove <output> | status
- Display Config output card gains a Color Profile row (browse/apply/remove
  with description, version, color space and active state) and a per-output
  color temperature slider, backed by Services/ICCService.qml.

Config path:
- DMSConfigDir() follows the compositor layout (niri/dms, hypr/dms,
  mango/dms) via the shared compositor detection and utils.XDGConfigHome(),
  replacing the hardcoded niri path in both the manager and `dms icc`.

Tests: TZ=UTC go test ./... (60 packages ok), including red/green coverage
for the icc parser bounds checks and needsControls().

* feat(icc): add `dms icc set-temp` and show per-output temps in `status`

The per-output temperature override was reachable from the Display Config
slider and from `wayland.icc.setTemp` over IPC, but not from the CLI.

- `dms icc set-temp <output> <kelvin>` (alias `setTemp`) sets a per-output
  override, 1000-10000K, and 0 clears it so the output follows the night
  light schedule again. Range checking matches the daemon so an invalid
  value fails in the CLI instead of over IPC.
- `dms icc status` gains a Temp column (the override in K, or `schedule`),
  so an override can be verified without opening the settings UI.

Verified against a running daemon: `dms icc status` reports 7000K for the
three overridden outputs and `schedule` for the remaining one.

* feat(icc): treat the per-output temperature as the profile's reference white point

The per-output temperature was documented as the white point a profile was
produced at, but an output with a profile ignored it entirely: the ramp was
generated from the profile and the temperature only applied to outputs
without one (or as the fallback when a profile failed to parse). A display
calibrated at 7000K therefore got the profile ramp, and enabling the night
light had no effect on it at all.

- `ProfileRampWithTemp` composes the profile ramp with the ratio between the
  target temperature ramp and the reference ramp, so the temperature is the
  white point the profile describes and the night light shifts relative to
  it.
- `applyGamma` passes the night light temperature as the target, and
  `noTempTarget` when the schedule is disabled, which leaves profiled
  outputs at their reference white point and drives plain outputs with the
  neutral ramp.
- The re-apply sentinel stays 0, so it cannot collide with an applied
  temperature.

Covered by TestProfileRampWithTemp (reference == target is a no-op, warmer
and cooler targets move the expected channels).

Also: `dms icc set-temp` help text describes the reference-white-point
semantics.

* fix(icc): load the configured profiles even when controls already exist

Restoring the upstream "output returned, re-establishing controls" branch
made the registry handler establish the gamma controls before the startup
post runs, and that post returned early on `m.controlsInitialized`, so the
configured ICC profiles and per-output temperatures were never attached:
every output reported "(none)" and the night light ran with defaults until
the user re-applied a profile by hand.

- `initializeControlsAndICC` (the startup post) loads the configuration
  first and only creates the controls when they are missing.
- The loading is split into `loadConfiguredICC` / `applyConfiguredICCForOutput`
  so it can also run per output.
- A hotplugged output now gets its configured profile and temperature as soon
  as its name is known (name handler) or when its control is created, which
  also makes the "re-applies on hotplug" claim in the description true.
- After attaching, the output's dedup state is cleared so the ramp is written.

Covered by TestManager_LoadConfiguredICCWhenControlsAlreadyExist and
TestManager_AttachConfiguredICCForNamedOutput.

* fix(icc): make the per-output temperature an actual target

The per-output value was treated as the white point a profile was produced
at, which meant a display with a profile never changed when the user set a
temperature: the profile ramp was applied as measured and the value only
mattered when a night light target existed. Per-display temperatures were
therefore unusable on profiled outputs while the night light was disabled,
which is the normal state for users who want each monitor at its own value.

- `effectiveTempTarget` decides per output: a per-output override wins over
  the night light schedule, the schedule applies when there is no override,
  and `noTempTarget` means neither. `0` is "no override", not 0K.
- `applyGamma` composes the override on top of the profile ramp, treating the
  profile as measured at 6500K (D65), and drives outputs without a profile
  with the same target so both paths agree.
- `dms icc set-temp` help text and the Display Config slider description go
  back to describing an independent per-output temperature.

Covered by TestEffectiveTempTarget (override wins, schedule fallback, none)
and the updated TestProfileRampWithTemp (7000K cools, 5000K warms relative to
the 6500K reference).

* feat(icc): show the profile details in Display Config

The profile row only showed the description, so there was no way to tell
which profile a display actually has applied (whitepoint, curve type, file
provenance) without leaving the settings page.

- `ICCStatus` carries the descriptive metadata: class, tone-curve kind (plus
  gamma or table size), vcgt channels/entries, white point chromaticity with
  a derived CCT and standard illuminant name, file size and mtime.
- `internal/icc` gains `WhitePointXY`, `WhitePointCCT` (McCamy),
  `WhitePointName` (D50/D65) and `TRCKind`, all covered by tests.
- Display Config's Color Profile row gets an info button that opens
  `ICCProfileInfoModal` with those fields and the profile path, alongside
  the existing browse/remove actions.
- `dms icc info` reports the white point as name/xy/CCT instead of raw XYZ.

* fix(icc): keep the daemon, the state snapshot and the temp slider honest

A malformed profile could still take the daemon down, the ICC getters ran off
the wayland actor goroutine, the pushed state hid a removal, and the
per-output temperature slider dropped wheel changes.

- `internal/icc`: the tag table was sized with `uint32` arithmetic, so
  `tagCount*12` wrapped (`0x15555556*12 == 8`) and a 140-byte profile passed
  the bounds check, allocated ~4.3 GB and then panicked. `ParseFile` runs on
  the wayland actor goroutine, which has no `recover()`, so a bad profile
  persisted in `wayland.json` took the daemon down at startup. The count is now
  compared against the bytes that are actually left before anything is sized
  with it.
- The manager publishes an ICC snapshot (`iccStatus`/`iccTemps`) whenever the
  actor changes an output's profile or temperature, and `GetICCStatus`/
  `GetOutputTemps` serve that snapshot instead of ranging `m.outputs` and
  reading fields the actor owns. That removes the data race reported by
  `go test -race` for `dms icc status` running against a hotplug, and keeps the
  per-profile `os.Stat` off the night light animation path, where it ran on
  every `updateStateFromSchedule` (including each 25 K step).
- `State.ICCProfiles`/`OutputTemps` drop `omitempty`, so removing the last
  profile pushes `{}` instead of omitting the key: the QML truthiness guard
  then clears `ICCService.status` instead of leaving the removed profile on
  screen with a green "active" dot.
- `removeOutputByRegistryName` (extracted from the registry remove handler)
  deletes the `outputNames`/`outputRegNames` entries along with the output, so
  a disconnected monitor stops being listed by `dms icc status`/`listOutputs`,
  and a rebound `wl_output` reusing the released object ID cannot attach the
  previous monitor's profile from the stale name.
- The per-output temperature slider sets `wheelEnabled: false` like the other
  drag-finished sliders: `DankSlider`'s wheel handler assigns `value` and emits
  `sliderValueChanged` without `sliderDragFinished`, so scrolling dropped the
  change, froze the label on the scrolled value and destroyed the value binding.

Covered by TestParseBytesTagTableBounds (allocates 4,299,161,600 bytes at
parser.go:138 and dies with an unrecovered fatal error on the previous
commit), TestManager_ICCStatusServesPublishedSnapshot,
TestManager_RemoveOutputByRegistryNamePrunesNames and
TestManager_ICCStatusConcurrentPublishAndRead; a temporary copy of the getter
test against the previous commit reports DATA RACE and passes here.

* fix(icc): keep the attached profile when a gamma control is re-created

`setupOutputControls` stored a fresh `outputState` for every output it touched,
and `createControls` calls it whenever the controls are rebuilt, so recreating a
control dropped the output's `iccPath`/`iccProfile`/`outputTemp` — configured
state that does not belong to the control. The path is dormant today because
`destroyControls` only runs when nothing is configured (`needsControls()` covers
`ICCProfiles`/`OutputTemps`), but the snapshot published after `createControls`
would then report whatever the rebuilt state holds instead of the profile the
display is configured with.

`controlStateFor` reuses the state of a known output and resets only what belongs
to the new control: the ramp bookkeeping and the failure budget (rampSize,
failed, retryCount, lastFailTime, lastTemp, lastGamma, lastContrast). A control
that has not reported gamma_size yet has to receive its ramp again, while the
profile and the per-output temperature survive. An output seen for the first
time still starts from a clean state.

Covered by TestManager_ControlStateReuseKeepsAttachedICC.

* fix(icc): keep the config a real snapshot and make the per-output temperature apply

Third review round.

- `Config` is copied by value all over the manager, but `ICCProfiles` and
  `OutputTemps` are maps, so every copy aliased the ones the manager writes under
  `configMutex`. `updateStateFromSchedule` stored such a copy in `m.state` and it
  was marshalled on the connection writer goroutine while `ApplyICC` stored a
  profile from the IPC goroutine (`fatal error: concurrent map iteration and map
  write`, which no `recover()` catches); `SaveConfig` marshalled its copy after
  the unlock, and `loadConfiguredICC`/`attachConfiguredICC` indexed the maps after
  releasing the lock. Every copy that outlives the lock is cloned now
  (`cloneConfig`/`configSnapshot`), so a value copy is a snapshot again.
- `SetOutputTemp` never created the gamma controls. On a default install (night
  light off, gamma 1.0, contrast 1.0, no profile, no override) they do not exist,
  so the `Range` over `m.outputs` matched nothing: the value was written to
  `wayland.json`, no ramp was applied, and the settings slider snapped back to
  "Default" until a daemon restart. It now persists first, creates the controls
  through `ensureOutputControls` (shared with `ApplyICC`), and pushes the state
  from the actor, and the published temperatures report the configured value even
  before an output is known.
- A TRC is not a video card gamma ramp: `rTRC`/`gTRC`/`bTRC` describe the
  display's own transfer function, so a gamma-2.2 profile was written as
  `t^(1/2.2)` — mid gray 0.5 became 47909/65535 — washing the display out instead
  of correcting it. `ApplyICC` refuses a profile without `vcgt`, the attach path
  skips one with a warning, and `GenerateGammaRamp` returns an error instead of
  falling back to the TRC or to an identity ramp. TRC stays profile metadata
  (`dms icc info`, the settings dialog).
- `icc: applied ICC ramp` was logged at Info on every 25 K animation step; it is
  Debug now, which is where a per-step message belongs.
- `I18n.tr("No information available")` in the profile info modal passes its
  translator context, like every other string this PR adds.

Covered by TestManager_ConfigSnapshotOwnsItsMaps,
TestManager_ConfigSnapshotConcurrentMarshal (the previous commit dies with
`fatal error: concurrent map iteration and map write`),
TestManager_SetOutputTempWithoutControlsPublishesValue (the previous commit
reports 0 for the value it just stored), TestGenerateGammaRampRequiresVCGT, and
the updated TestProfileRampWithTemp, whose fixture now carries an identity vcgt
table instead of relying on an empty profile.

* fix(icc): push the state when only the ICC part changed

Found while sweeping the same class of problem: `stateChanged` compared only the
schedule fields, so a change that touches nothing else — applying or removing a
profile, `dms icc set-temp` — was treated as "no change" and the notifier never
pushed it. The settings page then only caught up because the UI path calls
`requestStatus()` afterwards, and the CLI path did not catch up at all, which is
the symptom the omitempty fix was meant to remove.

`iccStateChanged` compares the ICC profile status and the per-output
temperatures, so those changes reach the subscribers on their own. Covered by
the new TestStateChanged cases (profile applied, profile removed, profile
description changed, output temp changed, unchanged ICC state); the four
change cases report `stateChanged() = false` on the previous commit.

* fix(icc): keep the night light state, prune names without a control, use a 0 sentinel

Fifth review round.

- `SaveICCConfig` replaces `SaveConfig` on the three ICC paths (apply, remove,
  set-temp). Writing the daemon's whole config persisted the night light fields
  it happens to hold in memory: with the night light on, applying a profile
  stored `"Enabled": true`, and since the shell only ever sends
  `wayland.gamma.setEnabled` when the mode is on, turning it off afterwards (an
  in-memory change on the daemon side, no write) left that `true` on disk — after
  log out/in the daemon came up driving the schedule while the UI showed night
  mode off, with no way to resync short of a toggle. The ICC save now reads the
  file and rewrites only `iccProfiles`/`outputTemps`, so the fields the shell
  owns stay as they are.
- `removeOutputByRegistryName` resolved the output from `m.outputs`, which only
  has entries once the gamma controls exist. On a default install (night light
  off, no profile, no override) no control is ever created, so a disconnected
  monitor kept its `outputNames`/`outputRegNames` entries for the life of the
  daemon: `dms icc status`/`listOutputs` listed monitors that were gone, and both
  maps grew with every dock/undock cycle. The object ID is resolved from
  `m.outputRegNames` (populated for every `wl_output`), the name entries are
  dropped before the output state is looked up, and a state that is known without
  a registry name still falls back to matching its own.
- The Display Config temperature row used `7000` as its fallback for an output
  with no override. `publishICCState` only copies entries with `temp != 0`, so
  the value is absent rather than `0` for an unconfigured output: the card claimed
  "7000K" while the daemon drove the night light or neutral, and a user who
  deliberately set 7000K got the "no override" colouring because the highlight
  tested `!== 7000`. The sentinel is `0` now, which is what the `Default` label
  branches and the "is overridden" tests were written against.

Covered by TestSaveICCConfigKeepsNightLightFields (the previous commit brings the
night light back on after a restart), TestManager_RemoveOutputByRegistryNameWithoutControl
(the previous commit keeps an unplugged monitor listed) and the existing
TestManager_RemoveOutputByRegistryNamePrunesNames.

* fix(icc): validate the CLI arguments and the IPC parameters of the ICC commands

Sweep of the two surfaces the review has not looked at yet, so the failures are
found here instead of in the next round.

- `dms icc apply` copied the profile into the config directory before asking the
  daemon, and only the *parse* was checked locally. A profile the daemon refuses
  (no `vcgt` table, non-RGB color space) was therefore left in the profile
  directory for `dms icc list` to keep offering, and an IPC failure left the copy
  behind as well. The command now checks the same rules the daemon enforces
  before copying anything, and removes the copy it just made when the request
  fails (a file that was already there is left alone).
- `dms icc list` marks a profile without a `vcgt` table, so the rule that makes
  `apply` refuse it is visible where the profiles are listed.
- `ApplyICC`/`RemoveICC`/`SetOutputTemp` reach the manager over IPC with whatever
  the caller sent: an empty (or whitespace-padded) output name used to be
  persisted as an entry in `wayland.json` that no `wl_output` can ever match, and
  a relative profile path was stored to be resolved again on the next start, from
  a different working directory. Both are rejected now, before anything is
  written.

Verified with the built CLI against the previous binary: with a profile that has
no vcgt table, `dms icc apply` now exits before touching the config directory
(`residual=[]`) where the previous one copied the file and went on to the IPC
(`residual=[nogamma.icc]`); with a valid profile and no daemon reachable, the
previous one leaves `vcgt.icc` behind and this one does not. Covered by
TestManager_RejectsInvalidICCArguments.

* fix(icc): apply the user gamma and contrast on profiled outputs

* fix(icc): recreate failed controls without the night light and allow clearing a per-output temp

* test(icc): cover the recreated control and the cleared temperature override

Follow-up on d55d3d1f, which fixed both points without tests:

- TestManager_RecreateOutputControlFollowsNeedsControls pins the predicate: with
  the night light off and nothing configured there is nothing to recreate, and
  with a configured profile or a per-output temperature the control has to be
  recreated. Both configured cases return nil on 9226251c.
- TestManager_SetOutputTempZeroClearsTheOverride pins what the reset button in
  Display Config does end to end: the stored value and the published entry both
  disappear, so the row reads "Default" and the output follows the schedule.

Also keeps the reset button's slot instead of revealing it on demand. The
temperature row shares its column with the slider below it, so the height change
between "Default" (no button) and an override moved the slider 14 px under the
pointer while it was being dragged; the button is now always laid out and only
its opacity and enabled state follow the override, which also lets the label
width drop the conditional.

* fix(icc): replicate a one-channel vcgt and keep the temp slider in sync

* fix(icc): release idle gamma controls and push the output list

* fix(icc): dedupe ramps on the effective target

* fix(icc): never overwrite a same-named profile in the config dir

---------

Co-authored-by: bbedward <[email protected]>
2026-09-15 11:45:35 -04:00
bbedward c94ce9b49f ci: raise the claude review turn cap 2026-09-15 10:43:57 -04:00
bbedward 534003209c i18n: sync 2026-09-15 10:14:38 -04:00
Timofey 05c807b7cd hyprland: use focusedMonitor instead of focusedWorkspace.monitor (#3424)
Hyprland's workspace refactor (hyprwm/Hyprland#16140) broke
Hyprland.focusedWorkspace.monitor as a way to find the focused screen
(quickshell-mirror/quickshell#1119). Quickshell already exposes
Hyprland.focusedMonitor directly, so read the monitor from there
2026-09-14 14:31:40 -04:00
Giacomo Di Clerico 218f7f752f tailscale: handle profile switches and bus updates reactively (#3427)
* tailscale: handle profile switches and bus updates reactively

* tailscale: address review feedback on test coverage and dead code
2026-09-14 14:26:05 -04:00
gibbert 6af2b1fbc3 fix(audio): avoid false negative when some event sounds can't be found (#3437) 2026-09-14 13:14:07 -04:00
bbedward ef86e0c54f i18n: separate plugin terms to separate POEdit project 2026-09-14 13:03:33 -04:00
deepfriedret 0133962936 fix(media widget): prevent title from truncating with scroll song title on (#3436) 2026-09-14 09:22:52 -04:00
Mario Mohar 6b50696b8a fix(battery): report when no charge threshold exists instead of faking success (#3385)
* fix(battery): report when no charge threshold exists instead of faking success

* fix(battery): hide the charge limit controls when there is no battery

Follow-up to the review on #3385: the new explanation showed on
desktops and VMs with no battery at all, where it reads as a fault
report about hardware the machine was never going to have.

Both the explanation and the apply button now require
BatteryService.batteryAvailable. Hiding the button there is the same
point the pull request makes elsewhere: offering a control that cannot
work is what produced the false success in the first place. A machine
without a battery has no charge threshold to write to, so the limit was
never applicable.

BatteryService.batteryAvailable is already the gate used twice in this
file for the same reason.
2026-09-12 12:09:42 -04:00
Harsh Kumar d7b5812561 display config: read live layout from hyprland instead of the cached wlr snapshot (#3418) 2026-09-12 09:59:55 -04:00
Huỳnh Thiện Lộc 4141089e63 fix(clipboard): preserve pinned state when editing entries (#3412)
* fix(clipboard): preserve pinned state when editing entries

* fix(clipboard): address PR review feedback on editEntry

* fix(clipboard): validate empty text and entry size in EditEntry

* fix(clipboard): gate saving entry on full text load completion

* fix(clipboard): add user feedback and error fallback for entry loading

* fix(clipboard): keep entry and gate save with error toast on load failure

* fix(clipboard): reject non-text edits and add translator contexts

* fix(clipboard): support alt text mime types in EditEntry and UI guards

* fix(clipboard): pre-fetch entry before opening editor and allow retry on load failure
2026-09-12 09:24:23 -04:00
phoropter b4f6da25ee Update vesktop.css (#3410)
Fix text being the same color as mention badge background.
2026-09-11 22:18:22 -04:00
bbedward f864dc2233 CONTRIBUTING: add generative AI policy 2026-09-11 10:48:13 -04:00
Huỳnh Thiện Lộc 13192370f5 feat(screenshot): add flag to control HUD scale and visibility (#3398)
* feat(screenshot): add --hud flag to control HUD scale and visibility

* fix(screenshot): clamp HUD scale to 1-4 and clarify flag description
2026-09-11 10:05:11 -04:00
bbedward 09ddd6da82 doctor: add aqueous and scroll
port 1.6
2026-09-11 09:47:05 -04:00
Kaushik Rawat b3e146b310 Island: Added option to choose the control center icon to be either Battery gauge or Connectivity (#3342)
* Island: Added option to choose the control center icon to be either
Battery gauge or Connectivity

* Add Control Center to settings search index

* island: keep connectivity dim opacity off the blink target

DankBlink animates opacity and forces it back to 1.0 when it stops,
which broke the icon's binding and left the disabled state fully opaque.
2026-09-11 01:17:06 -04:00
bbedward 4d5bac28c9 frame: close hairline gap at the screen edge
ref #3389
2026-09-10 17:04:32 -04:00
MattiasandClaude Opus 5 5e873170ee network: support fortinet vpn with saml and password auth (#3384)
Adds Fortinet gateway support to the NetworkManager openconnect path.
A SAML gateway authenticates through the browser, with the loopback
redirect handled in core and the session traded for the cookie
openconnect needs. Certificate trust is confirmed once and stored as
the digest openfortivpn uses.

openfortivpn config files can be imported as profiles. A saml-login
line selects the browser login, anything else imports as a password
profile carrying whatever credentials the file had.

Co-authored-by: Claude Opus 5 <[email protected]>
2026-09-10 16:35:08 -04:00
David Jiménez af866ba661 system-update: make popout reopen after terminal upgrade optional (#3307)
When a backend runs the upgrade in a terminal (paru/yay or a custom
command), the popout unconditionally reopened itself once the terminal
closed. Add an updaterReopenAfterUpgrade setting (default: on, keeping
current behavior) so users can opt out and keep the popout closed after
the terminal exits.
2026-09-10 10:36:02 -04:00
iperegkefalos 990fa17841 feat: add touchpad click method in settings (#3356)
* feat: add touchpad click method

* i18n: add translator context for click-method terms
2026-09-10 10:28:21 -04:00
Mattias 8c0f34232b idle inhibitor: add duration presets and timed expiry (#3344)
* idle inhibitor: add duration presets and timed expiry

Right-click the bar widget or expand the Control Center pill to keep the
system awake for a set duration rather than only indefinitely. The deadline
is persisted so it survives a shell restart, and is re-checked on resume
because timers stop across suspend.

Adds `dms ipc call inhibit enableFor <minutes>`, and `inhibit status` now
reports the remaining time.

* idle inhibitor: add translator context to new duration strings
2026-09-10 10:27:45 -04:00
Huỳnh Thiện Lộc 3adfa918c1 feat(launcher): add flatpak uninstall option in context menu (#3340)
* feat(launcher): add flatpak uninstall option to context menu

* fix(launcher): reuse existing catalog terms for uninstall toasts
2026-09-10 10:12:25 -04:00
Mario Mohar cf74c10561 fix(settings): size the plugin update spinner to fit its row (#3383) 2026-09-10 10:10:35 -04:00
purian23 60675ecb39 refactor(island): add island satellite hover option
fixes #3296
port 1.6
2026-09-09 23:36:26 -04:00
purian23 ce930f003d distro: meta file stash to fix debian sid 2026-09-09 22:13:13 -04:00
Huỳnh Thiện Lộc 65fb97d247 refactor(systemtray): overflow menu & context menu to Dpopout (#3239)
* refactor(systemtray): convert overflow menu and context menu to DankPopout

* fix(systemtray): correct window-local anchor position mapping and enable contentHandlesKeys for tray menu keyboard navigation

* fix(systemtray): force active focus on tray menu content

* fix(systemtray): address inline PR review feedback

- Force active focus on open for key navigation
- Unregister menu only if matching active instance
- Map anchor trigger position relative to screen origin

* fix(systemtray): gate onMenuOpenChanged on useOverflowPopup

- Prevent registering overflowPopout in inline expansion mode

* fix(systemtray): correct context menu positioning in overflow popout

- Handle screen global coordinate mapping for both connected and standalone popout modes

* fix(systemtray): handle per-screen backend and vertical bar anchor Y offset

- Use overflowPopout.useConnectedBackend for per-screen discriminator

- Add vertical anchor Y centering offset in showForTrayItem

* fix(systemtray): center overflow popout on vertical bar caret

- Add vertical orientation anchor Y offset in _openOverflowAt

* fix(systemtray): clear submenu entryStack on tray menu open

- Expose entryStack alias on trayMenuContentRoot

- Clear entryStack on trayMenuPopout onOpened to avoid submenu state leakage
2026-09-09 15:00:11 -04:00
Connor Welsh ba2b4a4d5d outputs: only re-enable displays the cycle itself turned off (#3377)
The cycle fallback fired from a never-cycled state. With one display
enabled and another marked off in settings, a DPMS standby that drops
the DisplayPort link looked like an unplug, and DMS sent an On to the
disabled display over niri IPC. It stayed on until the next config
reload and the workspaces round tripped through it.

Track each output's enabled state the first time a cycle touches it,
drop the entry once it is back at that state, and only rescue outputs
that were on before the cycle disabled them. Outputs that were already
off are never enabled automatically.
2026-09-09 13:02:21 -04:00
Vladimir 4b04af121f outputs: cycle connected displays on niri (#3362) 2026-09-09 10:20:38 -04:00
Zoey Bauer 8fe918ff24 add support for shelly (#3367)
* add support for shelly

* fixed terminal failure

* updated to have it not allow adding ignore aur

* updated error strings

* Fixed label

port 1.6
2026-09-08 11:50:02 -04:00
bbedward 2bf2754ad2 systray: dont wrap context menu title
port 1.6
2026-09-08 10:18:55 -04:00
Huỳnh Thiện Lộc 0f058468a5 fix(screenshot): force full-damage commit on cursor toggle (#3365)
* fix(screenshot): force full-damage commit on cursor toggle with compositor v4

* test(screenshot): drop tautological cursor-mismatch test
2026-09-08 08:59:46 -04:00
BBandZoey b9365610c8 feat: support for Aqueous Compositor (#3357)
* updated dank to support aqueous

* further updates

* aqueous support work

* updated to support workspace options better in aqueous

* added blur to dms shell for aqueous

* added keybinding update

* remove unneeded blur

* clean up per claud review

* swapped to ipc

* aqueous: move IPC integration to QML, delete test fixtures (since we
dont have a QML test structure even setup), delete in-repo docs

* fix staticcheck

* aqueous: consolidate more aqueous specific stuff

* add aqueous logo

---------

Co-authored-by: Zoey <[email protected]>
2026-09-07 22:28:52 -04:00
dms-ci[bot] f16fc1d42e nix: update vendorHash for go.mod changes 2026-09-08 01:21:15 +00:00
bbedward 8232144bb7 core: bump dankgo
fixes #3361
port 1.6
2026-09-07 21:18:49 -04:00
bbedward fb07a91108 workspace: keep hit area out of adjacent spacers
fixes #3360
port 1.6
2026-09-07 21:09:18 -04:00
bbedward 24e17a5957 hyprland: stop overwriting the config backup during lua migration
fixes #3359
port 1.6
2026-09-07 21:01:59 -04:00
bbedward 17d4ea6f88 frame: draw panel backgrounds when fullscreen hides the frame
port 1.6
2026-09-07 20:45:09 -04:00
bbedward 8c42700a4c i18n: sync, change plugin workflow
port 1.6
2026-09-07 20:44:46 -04:00
bbedward 9bf8174984 i18n: sync
port 1.6
2026-09-07 13:50:47 -04:00
bbedward 0f56536142 fix(launcher): stop injectnig plugins into app categories
port 1.6
2026-09-07 10:36:21 -04:00
bbedward de27853c46 frame: keep overlay backgrounds with their content
fixes #3350
port 1.6
2026-09-07 10:29:03 -04:00
bbedward 0890db93db display: watch the session store for active profile changes
port 1.6
2026-09-07 09:55:22 -04:00
Huỳnh Thiện Lộc a9fce46fec feat(screenshot): add touch & stylus support for region capture (#3353)
* feat(screenshot): add touch and stylus input support for region capture

* fix(screenshot): guard against finishSelection on touch up during scroll phase

port 1.6
2026-09-07 09:11:23 -04:00
purian23 d0f47bb316 refactor(osd): add compositor agnostic workspace OSD toasts 2026-09-06 23:08:20 -04:00
bbedward c1e53dbc33 i18n: make sync script auto-pull plugins for translation
port 1.6
2026-09-06 17:04:18 -04:00
bbedward 62ad4c2cb7 bar: refresh blur region after widget layout settles
ref #3348
port 1.6
2026-09-06 13:53:54 -04:00
bbedward c76c29139f launcher: stop rebuilding the category map per app and index i18n misses
fixes #3345
port 1.6
2026-09-06 13:39:51 -04:00
bbedward 654c6dce8f controlcenter: release dgop refs while the popout is closed
ref #3349
2026-09-06 13:31:20 -04:00
bbedward 8188f53da5 audio: add output card selection
port 1.6
2026-09-06 13:14:21 -04:00
Huỳnh Thiện Lộc f3d9d73db1 feat(osd): add workspace switch OSD for niri (#1756) (#3337)
* feat(osd): add workspace switch OSD for niri (#1756)

* fix(settings): remove redundant description from workspace osd row

* fix(osd): address review feedback on workspace tracking and i18n context
2026-09-06 01:08:42 -04:00
Kaushik Rawat f9eb2381b0 Added ability to play/pause media and mute audio by middle clicking when (#3332)
using the island mode
2026-09-05 18:04:44 -04:00
1994 changed files with 303245 additions and 271675 deletions
+2 -2
View File
@@ -8,7 +8,7 @@ Each skill is a directory with a `SKILL.md` entrypoint, optional reference docs,
| Skill | Description | | Skill | Description |
|-------|-------------| |-------|-------------|
| [dms-plugin-dev](dms-plugin-dev/) | Develop plugins for DankMaterialShell - covers all 4 plugin types (widget, daemon, launcher, desktop), manifest creation, QML components, settings UI, data persistence, theme integration, and PopoutService usage. | | [dms-plugin-dev](dms-plugin-dev/) | Develop plugins for Dux Shell - covers all 4 plugin types (widget, daemon, launcher, desktop), manifest creation, QML components, settings UI, data persistence, theme integration, and PopoutService usage. |
## Installation ## Installation
@@ -28,7 +28,7 @@ ln -s ../../.agents/skills/dms-plugin-dev .claude/skills/dms-plugin-dev
**Personal** (all your projects): **Personal** (all your projects):
```bash ```bash
ln -s /path/to/DankMaterialShell/.agents/skills/dms-plugin-dev ~/.claude/skills/dms-plugin-dev ln -s /path/to/dms/.agents/skills/dms-plugin-dev ~/.claude/skills/dms-plugin-dev
``` ```
After linking, the skill appears in Claude Code's `/` menu as `/dms-plugin-dev`, and Claude loads it automatically when you ask about DMS plugin development. After linking, the skill appears in Claude Code's `/` menu as `/dms-plugin-dev`, and Claude loads it automatically when you ask about DMS plugin development.
+110 -30
View File
@@ -1,65 +1,71 @@
--- ---
name: dms-plugin-dev name: dms-plugin-dev
description: > description: >
Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on Develop plugins for Dux Shell (DMS), a QML-based Linux desktop shell built on
Quickshell. Supports five plugin types: widget (bar + Control Center), daemon (background Quickshell. Supports widget (bar, dock + Control Center), daemon (background service),
service), launcher (search + actions), desktop (draggable desktop widgets), and composite launcher (search + actions), desktop (draggable desktop widgets), dash (dash tab),
(multi-surface). Covers manifest creation, QML component development, startup checks, dashCard (dash overview card), and composite (multi-surface) plugins. Covers manifest
creation, QML component development, startup checks,
settings UI, data persistence, theme integration, PopoutService usage, IPC runtime settings UI, data persistence, theme integration, PopoutService usage, IPC runtime
discovery, and external command execution. Use when the user wants to create, modify, discovery, and external command execution. Use when the user wants to create, modify,
or debug a DMS plugin, or asks about the DMS plugin API. or debug a DMS plugin, or asks about the DMS plugin API.
compatibility: Designed for Claude Code (or similar products) compatibility: Designed for Claude Code (or similar products)
metadata: metadata:
author: DankMaterialShell author: Dux Shell
version: "1.1" version: "1.2"
domain: qml-desktop-development domain: qml-desktop-development
framework: DankMaterialShell framework: Dux Shell
languages: qml, javascript languages: qml, javascript
allowed-tools: Bash Read Write Edit allowed-tools: Bash Read Write Edit
--- ---
# DankMaterialShell Plugin Development # Dux Shell Plugin Development
## Overview ## Overview
DMS plugins extend the desktop shell with custom widgets, background services, launcher DMS plugins extend the desktop shell with bar and dock widgets, background services, launcher
integrations, and desktop widgets. Plugins are QML components discovered from integrations, desktop widgets, and dash tabs and cards. Plugins are QML components discovered from
`~/.config/DankMaterialShell/plugins/`. `~/.config/dms/plugins/`.
**Minimum plugin structure:** **Minimum plugin structure:**
``` ```
~/.config/DankMaterialShell/plugins/YourPlugin/ ~/.config/dms/plugins/YourPlugin/
plugin.json # Required: manifest with metadata plugin.json # Required: manifest with metadata
YourComponent.qml # Required: main QML component YourComponent.qml # Required: main QML component
YourSettings.qml # Optional: settings UI YourSettings.qml # Optional: settings UI
*.js # Optional: JavaScript utilities *.js # Optional: JavaScript utilities
``` ```
**Plugin registry:** Community plugins are available at https://plugins.danklinux.com/ **Plugin registry:** Community plugins are available at https://plugins.duxlabs.dev/
**Five plugin types:** **Plugin types:**
| Type | Purpose | Base Component | Bar pills | CC integration | | Type | Purpose | Base Component | Bar/dock pills | CC integration |
|-------------|--------------------------------|----------------------------|-----------|----------------| |-------------|--------------------------------|----------------------------|----------------|----------------|
| `widget` | Bar widget + popout | `PluginComponent` | Yes | Yes | | `widget` | Bar or dock widget + popout | `PluginComponent` | Yes | Yes |
| `daemon` | Background service | `PluginComponent` (no UI) | No | Optional | | `daemon` | Background service | `PluginComponent` (no UI) | No | Optional |
| `launcher` | Searchable items in launcher | `Item` | No | No | | `launcher` | Searchable items in launcher | `Item` | No | No |
| `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No | | `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No |
| `composite` | Multi-surface plugin | One component per surface | Optional | Optional | | `dash` | Tab in the dash popout | `DashTabComponent` | No | No |
| `dashCard` | Card in the dash overview grid | `DashCardComponent` | No | No |
| `composite` | Multi-surface plugin | One component per surface | Optional | Optional |
## Step 1: Determine Plugin Type ## Step 1: Determine Plugin Type
Choose the type based on what the plugin does: Choose the type based on what the plugin does:
- **Shows in the bar?** - Use `widget`. Displays a pill in DankBar, optionally opens a popout, - **Shows in the bar or a dock?** - Use `widget`. Displays a pill in DankBar or a dock (same
optionally integrates with Control Center. component), optionally opens a popout or an attached dock panel, optionally integrates with
Control Center.
- **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper - **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper
changes, notifications, battery level, etc.). changes, notifications, battery level, etc.).
- **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher - **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher
with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji). with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).
- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the - **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the
desktop layer. desktop layer.
- **Full page in the dash?** - Use `dash`. A tab in the dash popout, id `plugin_<pluginId>`.
- **Small tile on the dash overview?** - Use `dashCard`. A resizable card in the overview grid.
- **Needs multiple surfaces?** - Use `composite`. A single plugin that registers any combination - **Needs multiple surfaces?** - Use `composite`. A single plugin that registers any combination
of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own
QML component file. QML component file.
@@ -126,6 +132,11 @@ Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md
} }
``` ```
**Standalone dash tab or card:** same single file form, `"type": "dash"` or `"type": "dashCard"`
plus `"component"`. A tab and a card together (or with a bar widget) use `components` with the
keys `dash` and `dashCard`. Both accept an optional `dash` block for labels, card size and
options. See [dash-plugin-guide.md](references/dash-plugin-guide.md).
**Key rules:** **Key rules:**
- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$` - `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`
- `version` must be semver (e.g., `1.0.0`) - `version` must be semver (e.g., `1.0.0`)
@@ -280,6 +291,62 @@ PluginComponent {
See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution. See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
### Dash Tab
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
import qs.Modules.DankDash
DashTabComponent {
id: root
property date now: new Date()
implicitHeight: DashMetrics.tabMinHeight
Timer {
interval: 1000
repeat: true
running: root.live
onTriggered: root.now = new Date()
}
StyledText {
anchors.centerIn: parent
text: root.now.toLocaleTimeString(I18n.locale())
color: Theme.surfaceText
}
}
```
### Dash Card
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
DashCardComponent {
id: root
tone: options.tone ?? ""
StyledText {
anchors.centerIn: parent
text: I18n.trFor("myPlugin", "Hello")
color: root.contentColor
}
}
```
Gate timers and animations on `live`; the dash content stays alive after the popout closes.
Cards must lay out for every size in their manifest range. Use the card colors (`accentColor`,
`contentColor`, `mutedColor`) so tones work. See [dash-plugin-guide.md](references/dash-plugin-guide.md).
### Composite ### Composite
For composite plugins, create a separate QML file per surface. Each surface uses the same For composite plugins, create a separate QML file per surface. Each surface uses the same
@@ -295,8 +362,7 @@ MyCompositePlugin/
Settings.qml # Shared settings for all surfaces Settings.qml # Shared settings for all surfaces
``` ```
Use `pluginService.pluginHasSurface(pluginId, "widget")` to check whether a specific surface `pluginService.availablePlugins[pluginId].surfaces` lists the surfaces a plugin registered.
is registered for a plugin at runtime.
## Step 4: Add Startup Check (Optional) ## Step 4: Add Startup Check (Optional)
@@ -414,7 +480,7 @@ Always use `Theme.*` properties from `qs.Common` - never hardcode colors or size
- Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge` - Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge`
- Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32) - Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32)
**Common widgets from `qs.Widgets`:** `StyledText`, `StyledRect`, `DankIcon`, `DankButton`, `DankToggle`, `DankTextField`, `DankSlider`, `DankGridView`, `CachingImage`. **Common widgets from `qs.DCommon.Widgets`:** `StyledText`, `StyledRect`, `DIcon`, `DButton`, `DToggle`, `DTextField`, `DSlider`, `DGridView`, `CachingImage`. The old `Dank*` names in `qs.Widgets` are deprecated.
See [theme-reference.md](references/theme-reference.md) for the complete property list. See [theme-reference.md](references/theme-reference.md) for the complete property list.
@@ -431,7 +497,6 @@ PluginComponent {
PopoutComponent { PopoutComponent {
headerText: "My Plugin" headerText: "My Plugin"
detailsText: "Optional subtitle" detailsText: "Optional subtitle"
showCloseButton: true
Column { Column {
width: parent.width width: parent.width
@@ -450,7 +515,7 @@ PluginComponent {
} }
``` ```
**PopoutComponent properties:** `headerText`, `detailsText`, `showCloseButton`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly). **PopoutComponent properties:** `headerText`, `detailsText`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly).
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing` Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
@@ -550,7 +615,7 @@ Lookup order: plugin file, then the global DMS catalog, then the English term. `
1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json) 1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)
2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml` 2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`
3. Open Settings > Plugins > Scan for Plugins 3. Open Settings > Plugins > Scan for Plugins
4. Enable your plugin and add it to the DankBar layout 4. Enable your plugin and add it to the DankBar layout, a dock, or the dash (tabs show up on their own; add cards from the dash edit mode)
**Runtime plugin discovery via IPC:** **Runtime plugin discovery via IPC:**
@@ -588,6 +653,10 @@ Plugin IDs are validated against `^[a-zA-Z0-9_\-:]{1,64}$`.
11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies` 11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`
12. **Providing both `component` and `components`** - Use one or the other, not both 12. **Providing both `component` and `components`** - Use one or the other, not both
13. **Missing `trigger` on composite with launcher surface** - Still required when `components` has a `launcher` key 13. **Missing `trigger` on composite with launcher surface** - Still required when `components` has a `launcher` key
14. **Using `"type": "widget"` for a dash tab** - Dash surfaces need `"type": "dash"` / `"dashCard"` or a `components` key
15. **Timers in dash tabs or cards that ignore `live`** - The dash content outlives the popout; gate on `live`
16. **Reading `dash.options` from a bar widget** - `options` only reach dash surfaces; share values through `settings` and `pluginData`
17. **Raw theme colors in a dash card** - Use `accentColor` / `contentColor` / `mutedColor` so the card follows its tone
## Quick Reference: Imports ## Quick Reference: Imports
@@ -611,6 +680,16 @@ import QtQuick
import qs.Common import qs.Common
``` ```
**Dash tab / card:**
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
import qs.Modules.DankDash // DashMetrics, DashWidgetGrid
import qs.Modules.DankDash.Overview // Card, for tiles inside a tab
```
**For clipboard/exec:** `import Quickshell` **For clipboard/exec:** `import Quickshell`
**For processes:** `import Quickshell.Io` **For processes:** `import Quickshell.Io`
**For networking:** `import Quickshell.Networking` **For networking:** `import Quickshell.Networking`
@@ -629,7 +708,8 @@ import qs.Common
Load these on demand for detailed API documentation: Load these on demand for detailed API documentation:
- [plugin-manifest-reference.md](references/plugin-manifest-reference.md) - Complete plugin.json field reference and JSON schema - [plugin-manifest-reference.md](references/plugin-manifest-reference.md) - Complete plugin.json field reference and JSON schema
- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar pills, popouts, click actions, CC integration - [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar and dock pills, attached dock panels, popouts, click actions, CC integration
- [dash-plugin-guide.md](references/dash-plugin-guide.md) - DashTabComponent, DashCardComponent, DashWidgetGrid, dash options, card sizing
- [launcher-plugin-guide.md](references/launcher-plugin-guide.md) - getItems/executeItem, triggers, icon types, context menus, tile view - [launcher-plugin-guide.md](references/launcher-plugin-guide.md) - getItems/executeItem, triggers, icon types, context menus, tile view
- [desktop-plugin-guide.md](references/desktop-plugin-guide.md) - DesktopPluginComponent, sizing, edit mode, position persistence - [desktop-plugin-guide.md](references/desktop-plugin-guide.md) - DesktopPluginComponent, sizing, edit mode, position persistence
- [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution - [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution
@@ -1,8 +1,8 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://danklinux.com/schemas/plugin.json", "$id": "https://duxlabs.dev/schemas/plugin.json",
"title": "DankMaterialShell Plugin Manifest", "title": "Dux Shell Plugin Manifest",
"description": "Schema for DankMaterialShell plugin.json manifest files", "description": "Schema for Dux Shell plugin.json manifest files",
"type": "object", "type": "object",
"required": [ "required": [
"id", "id",
@@ -42,7 +42,7 @@
"type": { "type": {
"type": "string", "type": "string",
"description": "Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.", "description": "Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.",
"enum": ["widget", "daemon", "launcher", "desktop", "composite"] "enum": ["widget", "daemon", "launcher", "desktop", "dash", "dashCard", "composite"]
}, },
"capabilities": { "capabilities": {
"type": "array", "type": "array",
@@ -80,11 +80,83 @@
"type": "string", "type": "string",
"description": "Launcher provider component (requires 'trigger')", "description": "Launcher provider component (requires 'trigger')",
"pattern": "^\\./.*\\.qml$" "pattern": "^\\./.*\\.qml$"
},
"dash": {
"type": "string",
"description": "Dashboard tab component (DashTabComponent)",
"pattern": "^\\./.*\\.qml$"
},
"dashCard": {
"type": "string",
"description": "Dashboard overview card component (DashCardComponent)",
"pattern": "^\\./.*\\.qml$"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
"minProperties": 1 "minProperties": 1
}, },
"dash": {
"type": "object",
"description": "Presentation for the dashboard surfaces. Falls back to 'icon' and 'name'.",
"properties": {
"icon": {
"type": "string",
"description": "Material icon for the dashboard tab"
},
"title": {
"type": "string",
"description": "Tab label"
},
"card": {
"type": "object",
"description": "Overview card presentation and default size in grid units. Widths are clamped to the user's column count (3 to 8).",
"properties": {
"icon": { "type": "string" },
"title": { "type": "string" },
"w": { "type": "integer", "minimum": 1, "maximum": 8 },
"h": { "type": "integer", "minimum": 1 },
"minW": { "type": "integer", "minimum": 1, "maximum": 8 },
"minH": { "type": "integer", "minimum": 1 },
"maxW": { "type": "integer", "minimum": 1, "maximum": 8 },
"maxH": { "type": "integer", "minimum": 1 }
},
"additionalProperties": false
},
"options": {
"type": "array",
"description": "User options shown in the dashboard options sheet and Settings > Dashboard; values reach the tab and card as `options.<key>`",
"items": {
"type": "object",
"required": ["key", "type"],
"properties": {
"key": { "type": "string", "not": { "const": "widgets" }, "description": "Option key, read as options.<key>. 'widgets' is reserved for DashWidgetGrid layouts" },
"text": { "type": "string", "description": "Row label, translated through the plugin catalog" },
"description": { "type": "string", "description": "Secondary text under a toggle row, translated through the plugin catalog" },
"type": { "type": "string", "enum": ["toggle", "choice", "number"] },
"def": { "description": "Default value: boolean for toggle, choice value for choice, number for number" },
"choices": {
"type": "array",
"items": {
"type": "object",
"required": ["value"],
"properties": {
"value": { "type": "string" },
"text": { "type": "string" }
},
"additionalProperties": false
}
},
"min": { "type": "number" },
"max": { "type": "number" },
"step": { "type": "number", "exclusiveMinimum": 0 },
"unit": { "type": "string" }
},
"additionalProperties": false
}
}
},
"additionalProperties": false
},
"trigger": { "trigger": {
"type": "string", "type": "string",
"description": "Trigger string for launcher activation (required for launcher type)" "description": "Trigger string for launcher activation (required for launcher type)"
@@ -0,0 +1,33 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
import qs.Modules.DankDash
DashCardComponent {
id: root
readonly property bool wide: width >= DashMetrics.gridRowUnit * 2
tone: options.tone ?? ""
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "extension"
size: Theme.iconSizeLarge
color: root.accentColor
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
visible: root.wide
text: I18n.trFor("myDashCard", "Hello")
font.pixelSize: Theme.fontSizeLarge
color: root.contentColor
}
}
}
@@ -0,0 +1,27 @@
{
"id": "myDashCard",
"name": "My Dash Card",
"description": "A custom dash overview card",
"version": "1.0.0",
"author": "Your Name",
"type": "dashCard",
"capabilities": ["dash-card"],
"component": "./Card.qml",
"icon": "extension",
"dash": {
"card": { "w": 2, "h": 1, "minW": 1, "minH": 1, "maxW": 3, "maxH": 2 },
"options": [
{
"key": "tone",
"text": "Tone",
"type": "choice",
"def": "",
"choices": [
{ "value": "", "text": "Surface" },
{ "value": "primary", "text": "Primary" }
]
}
]
},
"requires_dms": ">=1.7.0"
}
@@ -0,0 +1,37 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
import qs.Modules.DankDash
DashTabComponent {
id: root
focusTarget: button
implicitHeight: Math.max(DashMetrics.tabMinHeight, content.implicitHeight + Theme.spacingL * 2)
function restoreFocus() {
button.forceActiveFocus(Qt.OtherFocusReason);
}
Column {
id: content
anchors.centerIn: parent
spacing: Theme.spacingM
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
visible: !root.options.compact
text: I18n.trFor("myDashTab", "Hello from a dash tab")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
}
DankButton {
id: button
anchors.horizontalCenter: parent.horizontalCenter
text: I18n.trFor("myDashTab", "Open overview")
onClicked: root.tabRequested("overview")
}
}
}
@@ -0,0 +1,18 @@
{
"id": "myDashTab",
"name": "My Dash Tab",
"description": "A custom dash tab",
"version": "1.0.0",
"author": "Your Name",
"type": "dash",
"capabilities": ["dash-tab"],
"component": "./Tab.qml",
"icon": "extension",
"dash": {
"title": "My Tab",
"options": [
{ "key": "compact", "text": "Compact layout", "type": "toggle", "def": false }
]
},
"requires_dms": ">=1.7.0"
}
@@ -59,7 +59,6 @@ PluginComponent {
// popoutContent: Component { // popoutContent: Component {
// PopoutComponent { // PopoutComponent {
// headerText: "My Widget" // headerText: "My Widget"
// showCloseButton: true
// //
// Column { // Column {
// width: parent.width // width: parent.width
@@ -43,7 +43,7 @@ PluginSettings {
pluginId: "exampleVariants" pluginId: "exampleVariants"
// Variant creation UI // Variant creation UI
DankButton { DButton {
text: "Add New Instance" text: "Add New Instance"
onClicked: { onClicked: {
var id = "variant_" + Date.now() var id = "variant_" + Date.now()
@@ -0,0 +1,179 @@
# Dash Plugin Guide
The dash popout (`dms ipc call dash open`) holds tabs and an overview grid of cards. A plugin can add a tab (`dash` surface), a card (`dashCard` surface), or both. Built-in tabs and cards use the same registry (`quickshell/Modules/DankDash/DashRegistry.qml`).
Requires DMS 1.7 or later: set `"requires_dms": ">=1.7.0"`.
## Picking the manifest form
| Plugin provides | Manifest |
|-----------------|----------|
| Tab only | `"type": "dash"`, `"component": "./Tab.qml"` |
| Card only | `"type": "dashCard"`, `"component": "./Card.qml"` |
| Tab and card | `"type": "composite"`, `"components": { "dash": ..., "dashCard": ... }` |
| Dash plus bar/dock widget, daemon, ... | `"type": "composite"` with every surface in `components` |
Add the optional `dash` block for labels, card size and options (see plugin-manifest-reference.md, Dash Block).
## Rules that trip people up
- Only enabled plugins appear in the dash. There is no "disabled" placeholder.
- A plugin tab shows in the tab bar as soon as the plugin loads. Cards are never placed automatically; users add them in edit mode (three-dot menu, Edit, Add widget).
- Tab id is `plugin_<pluginId>`. Use it with `dms ipc call dash open plugin_<pluginId>` and `popoutService.toggleDankDash("plugin_" + pluginId, x, y, width, section, screen)`.
- A tab is created when it becomes current and destroyed when the user leaves it. The dash content stays alive after the popout closes, so gate every Timer, animation and service ref on `live`.
- Widths are grid columns (user picks 3 to 8, default 6). Rows are 96 px (`DashMetrics.gridRowUnit`). Cards must lay out for every size in `minW..maxW` x `minH..maxH`.
- `dash.options` values reach only the dash surfaces as `options.<key>`. A bar widget or daemon in the same plugin cannot read them; use a `settings` component and `pluginData` for values every surface needs.
- `widgets` is a reserved option key.
- Use `I18n.trFor("<plugin id literal>", "...")` for every string.
## Tab: DashTabComponent
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.DCommon.Widgets
import qs.Modules.Plugins
import qs.Modules.DankDash
DashTabComponent {
id: root
focusTarget: button
implicitHeight: Math.max(DashMetrics.tabMinHeight, content.implicitHeight + Theme.spacingL * 2)
menuActions: [
{
label: I18n.trFor("myPlugin", "Refresh"),
iconName: "refresh",
action: () => root.refresh()
}
]
function refresh() {
}
function restoreFocus() {
button.forceActiveFocus(Qt.OtherFocusReason);
}
function handleKeyEvent(event) {
if (event.key !== Qt.Key_R)
return false;
refresh();
return true;
}
Timer {
interval: 1000
repeat: true
running: root.live
onTriggered: root.refresh()
}
Column {
id: content
anchors.centerIn: parent
spacing: Theme.spacingM
StyledText {
text: root.options.compact ? "" : I18n.trFor("myPlugin", "Hello")
color: Theme.surfaceText
}
DButton {
id: button
text: I18n.trFor("myPlugin", "Refresh")
onClicked: root.refresh()
}
}
}
```
Injected: `pluginId`, `pluginService`, `popoutService`, `entryId`, `dashHost` (`dashVisible`, `requestTab(id)`, `editMode`), `editMode`, `live`. Also `targetScreen`, `active` and `rowBudget` when the tab declares them.
Base provides: `options`, `pluginData`, `getData(key, def)`, `setData(key, value)`.
Optional: `implicitHeight`, `focusTarget`, `restoreFocus()`, `handleKeyEvent(event)` (return true to consume; Ctrl+Tab is reserved), `blocksTabNavigation`, `menuActions`, `signal tabRequested(string id)`, `signal navFocusRequested`.
For tiles inside a tab, use `Card` from `qs.Modules.DankDash.Overview` so colors, radius and tones match the overview.
### Editable widgets in a tab
Assign a `DashWidgetGrid` (from `qs.Modules.DankDash`) to `widgetGrid` to get Edit, Add widget, Reset and Clear All:
```qml
DashTabComponent {
id: tab
widgetGrid: grid
implicitHeight: grid.implicitHeight
focusTarget: grid.focusTarget
DashWidgetGrid {
id: grid
width: parent.width
entryId: tab.entryId
live: tab.live
editMode: tab.editMode
definitions: [
{ id: "main", text: I18n.trFor("myPlugin", "Main"), icon: "star", component: mainWidget, w: 2, h: 2, minW: 1, minH: 1, maxW: 4, maxH: 3 }
]
}
Component {
id: mainWidget
Card {}
}
}
```
The grid is 4 columns (2 when narrower than `Theme.smallBreakpoint`). `enabled: false` on a definition leaves it in the Add menu. The layout is saved in `dashOptions[entryId].widgets`.
## Card: DashCardComponent
```qml
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
import qs.Modules.DankDash
DashCardComponent {
id: root
readonly property bool wide: width >= DashMetrics.gridRowUnit * 2
readonly property bool tall: height >= DashMetrics.gridRowUnit * 2
tone: options.tone ?? ""
SystemClock {
id: clock
enabled: root.live
precision: SystemClock.Minutes
}
StyledText {
anchors.centerIn: parent
text: clock.date.toLocaleTimeString(I18n.locale(), "hh:mm")
font.pixelSize: root.tall ? Theme.fontSizeDisplay : Theme.fontSizeXLarge
color: root.accentColor
}
}
```
Base (`Card`) provides: `pad`, `title`, `tone` (`""`, `"primary"`, `"secondary"`, `"tertiary"`), `containerColor`, `contentColor`, `accentColor`, `onAccentColor`, `mutedColor`, `chipColor`, `tinted`, `clickable`, `signal clicked`, `options`, `pluginData`, `getData`, `setData`.
Injected: `pluginId`, `pluginService`, `popoutService`, `entryId`; bound: `live` (overview visible), `interactive` (false in edit mode).
Clicks: when the plugin also has a tab, a click on a `clickable` card opens it. Set `opensTab: false` to handle the click yourself. Non-clickable cards are skipped by keyboard navigation unless `focusTarget` points at a child control.
Use the card colors, never raw `Theme.primary` or `Theme.surfaceText`, so tones work.
## Testing
```bash
dms ipc call plugins reload myPlugin
dms ipc call dash open plugin_myPlugin
```
Working examples in `quickshell/PLUGINS/`: `DashTabExample` (standalone tab), `DashCardExample` (standalone card), `DashCounterExample` (tab + card + bar/dock widget).
@@ -174,7 +174,7 @@ Retrieve a plugin's installation directory at runtime:
var dir = pluginService.getPluginPath(pluginId) var dir = pluginService.getPluginPath(pluginId)
``` ```
Returns the absolute path to the plugin's directory (e.g., `~/.config/DankMaterialShell/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location. Returns the absolute path to the plugin's directory (e.g., `~/.config/dms/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.
## Important Notes ## Important Notes
@@ -9,7 +9,7 @@
| `description` | string | Short description (shown in UI) | Non-empty | | `description` | string | Short description (shown in UI) | Non-empty |
| `version` | string | Semantic version | Pattern `^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$` | | `version` | string | Semantic version | Pattern `^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$` |
| `author` | string | Creator name or email | Non-empty | | `author` | string | Creator name or email | Non-empty |
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `composite` | | `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `dash`, `dashCard`, `composite` |
| `capabilities` | array | Plugin capabilities | At least 1 string item | | `capabilities` | array | Plugin capabilities | At least 1 string item |
One of `component` or `components` is required (not both): One of `component` or `components` is required (not both):
@@ -17,7 +17,7 @@ One of `component` or `components` is required (not both):
| Field | Type | Description | Validation | | Field | Type | Description | Validation |
|-------|------|-------------|------------| |-------|------|-------------|------------|
| `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` | | `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` |
| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher` | | `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard` |
## Conditional Requirements ## Conditional Requirements
@@ -38,6 +38,7 @@ One of `component` or `components` is required (not both):
| `requires` | array | Deprecated alias for `dependencies` | | `requires` | array | Deprecated alias for `dependencies` |
| `permissions` | array | Required permissions | | `permissions` | array | Required permissions |
| `trigger` | string | Launcher trigger string (required for launcher type) | | `trigger` | string | Launcher trigger string (required for launcher type) |
| `dash` | object | Dash tab label, overview card size range and dash options. See Dash Block below. |
## Permissions ## Permissions
@@ -59,6 +60,8 @@ Capabilities are free-form strings that describe what the plugin does. Common va
- `monitoring` - system/service monitoring - `monitoring` - system/service monitoring
- `launcher` - launcher search provider - `launcher` - launcher search provider
- `desktop-widget` - desktop background widget - `desktop-widget` - desktop background widget
- `dash-tab` - dash tab
- `dash-card` - dash overview card
- `ai` - AI/LLM integration - `ai` - AI/LLM integration
- `slideout` - uses slideout panel - `slideout` - uses slideout panel
@@ -119,7 +122,68 @@ The `components` field maps surface names to QML paths, allowing a single plugin
} }
``` ```
Valid surface keys: `widget`, `desktop`, `daemon`, `launcher`. Provide any subset. Each surface is loaded independently in the appropriate registry. Valid surface keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard`. Provide any subset. Each surface is loaded independently in the appropriate registry.
## Dash Block
Plugins with a `dash` or `dashCard` surface can describe them in a `dash` object. Everything is optional.
```json
"dash": {
"icon": "timer",
"title": "Stopwatch",
"card": { "title": "Progress", "icon": "hourglass_bottom", "w": 2, "h": 1, "minW": 1, "minH": 1, "maxW": 4, "maxH": 2 },
"options": [
{ "key": "compact", "text": "Compact layout", "type": "toggle", "def": false, "description": "Hide the secondary line" },
{ "key": "style", "text": "Style", "type": "choice", "def": "bars", "choices": [{ "value": "bars", "text": "Bars" }, { "value": "dots", "text": "Dots" }] },
{ "key": "limit", "text": "Items", "type": "number", "def": 5, "min": 1, "max": 20, "step": 1 }
]
}
```
| Field | Description |
|-------|-------------|
| `icon`, `title` | Tab icon and label. Fall back to the top-level `icon` and `name` |
| `card.title`, `card.icon` | Card name and icon in the Add widget menu. Fall back to the tab label and icon |
| `card.w`, `card.h` | Default size in grid cells. Columns are 3 to 8 (default 6), rows are 96 px |
| `card.minW` .. `card.maxH` | Resize range. Widths clamp to the user's column count; heights have no fixed limit |
| `options` | Rows in the dash options sheet and Settings > Dashboard. Read as `options.<key>` in the tab and card. Types: `toggle`, `choice`, `number`. `widgets` is a reserved key |
## Dash Examples
Standalone tab and standalone card use the single file form:
```json
{
"id": "dashTabExample",
"name": "Stopwatch Example",
"description": "A standalone dash tab",
"version": "1.0.0",
"author": "Developer Name",
"type": "dash",
"capabilities": ["dash-tab"],
"component": "./StopwatchTab.qml",
"dash": { "title": "Stopwatch" },
"requires_dms": ">=1.7.0"
}
```
```json
{
"id": "dashCardExample",
"name": "Progress Example",
"description": "A standalone overview card",
"version": "1.0.0",
"author": "Developer Name",
"type": "dashCard",
"capabilities": ["dash-card"],
"component": "./ProgressCard.qml",
"dash": { "card": { "w": 2, "h": 1, "maxW": 4, "maxH": 2 } },
"requires_dms": ">=1.7.0"
}
```
A tab plus a card, or dash surfaces plus a bar widget, use `components` with `"type": "composite"`.
## Complete Example ## Complete Example
@@ -83,11 +83,12 @@ Theme.popupBackground() // Popup background with proper opacity
```qml ```qml
import qs.Widgets import qs.Widgets
import qs.DCommon.Widgets
Row { Row {
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DIcon {
name: "icon_name" name: "icon_name"
color: Theme.onSurface color: Theme.onSurface
font.pixelSize: Theme.iconSize font.pixelSize: Theme.iconSize
@@ -151,7 +152,7 @@ StyledRect {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DIcon {
name: "star" name: "star"
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: Theme.iconSize font.pixelSize: Theme.iconSize
@@ -191,17 +192,17 @@ font.pixelSize: 14 // Use Theme.fontSizeMedium
|--------|-------------| |--------|-------------|
| `StyledText` | Themed text with proper color defaults | | `StyledText` | Themed text with proper color defaults |
| `StyledRect` | Themed rectangle | | `StyledRect` | Themed rectangle |
| `DankIcon` | Material Symbols icon renderer | | `DIcon` | Material Symbols icon renderer |
| `DankNFIcon` | Nerd Font icon renderer | | `DankNFIcon` | Nerd Font icon renderer |
| `DankButton` | Themed button | | `DButton` | Themed button |
| `DankToggle` | Toggle switch | | `DToggle` | Toggle switch |
| `DankTextField` | Text input field | | `DTextField` | Text input field |
| `DankSlider` | Slider control | | `DSlider` | Slider control |
| `DankDropdown` | Dropdown menu | | `DDropdown` | Dropdown menu |
| `DankGridView` | Grid layout view | | `DGridView` | Grid layout view |
| `DankListView` | List layout view | | `DListView` | List layout view |
| `DankFlickable` | Scrollable container | | `DFlickable` | Scrollable container |
| `DankTabBar` | Tab bar navigation | | `DTabBar` | Tab bar navigation |
| `DankCollapsibleSection` | Collapsible content section | | `DankCollapsibleSection` | Collapsible content section |
| `DankTooltip` | Hover tooltip | | `DankTooltip` | Hover tooltip |
| `DankNumberStepper` | Number +/- control | | `DankNumberStepper` | Number +/- control |
@@ -1,6 +1,6 @@
# Widget Plugin Guide # Widget Plugin Guide
Widgets are bar plugins that display pills in DankBar, optionally open popouts, and can integrate with the Control Center. Widgets display pills in DankBar and in docks (same component, no extra code), optionally open popouts, and can integrate with the Control Center.
## Base Component ## Base Component
@@ -37,6 +37,8 @@ These are automatically set by the plugin host:
| `pluginId` | string | This plugin's ID | | `pluginId` | string | This plugin's ID |
| `pluginService` | object | PluginService reference | | `pluginService` | object | PluginService reference |
| `pluginData` | object | Reactive plugin settings data | | `pluginData` | object | Reactive plugin settings data |
| `surfaceContext` | object | The host surface: `kind` (`"bar"` or `"dock"`), `id`, `screen`, `isVertical`, `thickness`, `live`, `editMode`, `inlineExpansion` |
| `widgetInstanceId` | string | This occurrence; the same widget can appear on several bars and docks |
## Bar Pills ## Bar Pills
@@ -57,7 +59,7 @@ horizontalBarPill: Component {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DIcon {
name: "star" name: "star"
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: Theme.iconSize font.pixelSize: Theme.iconSize
@@ -90,7 +92,7 @@ verticalBarPill: Component {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DIcon {
name: "star" name: "star"
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: Theme.iconSizeSmall font.pixelSize: Theme.iconSizeSmall
@@ -115,7 +117,6 @@ PluginComponent {
PopoutComponent { PopoutComponent {
headerText: "My Plugin" headerText: "My Plugin"
detailsText: "Optional subtitle" detailsText: "Optional subtitle"
showCloseButton: true
Column { Column {
width: parent.width width: parent.width
@@ -138,7 +139,6 @@ PluginComponent {
|----------|------|---------|-------------| |----------|------|---------|-------------|
| `headerText` | string | `""` | Main header (bold, large). Hidden if empty. | | `headerText` | string | `""` | Main header (bold, large). Hidden if empty. |
| `detailsText` | string | `""` | Subtitle below header. Hidden if empty. | | `detailsText` | string | `""` | Subtitle below header. Hidden if empty. |
| `showCloseButton` | bool | `false` | Show X button in top-right corner. |
| `closePopout` | function | (injected) | Call to close the popout programmatically. | | `closePopout` | function | (injected) | Call to close the popout programmatically. |
| `headerHeight` | int | (readonly) | Height of header area (0 if hidden). | | `headerHeight` | int | (readonly) | Height of header area (0 if hidden). |
| `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). | | `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). |
@@ -241,16 +241,49 @@ PluginComponent {
**Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically. **Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically.
## Docks
Users add any loaded widget plugin to a dock from the dock's edit mode or Settings > Dock > Apps & widgets. Nothing in the manifest is needed.
- Define both `horizontalBarPill` and `verticalBarPill`; docks sit on any edge.
- Size icons with `root.iconSize` and text with `Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)`. The dock hands its own thickness and scale in those properties.
- Gate timers and animations on `surfaceLive` (host visible and `visibilityCommand` passing). Docks auto-hide.
- Don't assume one instance. Use `widgetInstanceId` if an instance needs its own state.
Optional attached panel: set `attachedContent: Component { ... }`. When the dock's "Open widgets" setting is Inline (`surfaceContext.inlineExpansion`), a click grows the dock and shows the panel next to the apps. With Popout (the default) the normal `popoutContent` opens instead; a widget with no popout still expands inline. Bars always use `popoutContent`. A `pillClickAction` always wins over both.
```qml
PluginComponent {
id: root
attachedContent: Component {
Column {
padding: Theme.spacingM
spacing: Theme.spacingM
DButton {
text: I18n.trFor("myPlugin", "Close")
onClicked: root.surfaceContext.dismissExpansion()
}
}
}
}
```
The panel spans the dock's length and grows up to 320 px away from the edge (at most half the screen). The plugin cannot size it. `attachedActive` is true while this widget owns the panel. Escape closes it. See `quickshell/PLUGINS/AttachedPanelExample` and `quickshell/PLUGINS/SURFACE_WIDGETS.md`.
## Popout Namespace ## Popout Namespace
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state: For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
```qml ```qml
PluginComponent { PluginComponent {
layerNamespacePlugin: true layerNamespacePlugin: "my-plugin"
} }
``` ```
The shell prefixes it, so the popout's namespace becomes `dms:plugins:my-plugin`.
## Reading Plugin Data ## Reading Plugin Data
Access saved settings reactively via the injected `pluginData`: Access saved settings reactively via the injected `pluginData`:
@@ -278,6 +311,7 @@ import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
import qs.DCommon.Widgets
import qs.Services import qs.Services
import qs.Modules.Plugins import qs.Modules.Plugins
@@ -333,9 +367,8 @@ PluginComponent {
popoutContent: Component { popoutContent: Component {
PopoutComponent { PopoutComponent {
headerText: "Emoji Picker" headerText: "Emoji Picker"
showCloseButton: true
DankGridView { DGridView {
width: parent.width width: parent.width
height: 300 height: 300
cellWidth: 50 cellWidth: 50
+1 -1
View File
@@ -3,7 +3,7 @@
github: [avengemedia] github: [avengemedia]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: danklinux ko_fi: duxlabs
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username liberapay: # Replace with a single Liberapay username
+1 -1
View File
@@ -6,7 +6,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
## DankMaterialShell Bug Report ## Dux Shell Bug Report
Limit your report to one issue per submission unless similarly related Limit your report to one issue per submission unless similarly related
- type: dropdown - type: dropdown
id: compositor id: compositor
+1 -1
View File
@@ -6,7 +6,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
## DankMaterialShell Feature Request ## Dux Shell Feature Request
- type: textarea - type: textarea
id: feature_description id: feature_description
attributes: attributes:
+2 -2
View File
@@ -1,12 +1,12 @@
name: Support Request name: Support Request
description: Help with installation, usage, or general questions about DankMaterialShell description: Help with installation, usage, or general questions about Dux Shell
labels: labels:
- support - support
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
## DankMaterialShell Support Request ## Dux Shell Support Request
- type: dropdown - type: dropdown
id: compositor id: compositor
attributes: attributes:
+1 -1
View File
@@ -28,4 +28,4 @@
- [ ] New user-facing strings are wrapped in `I18n.tr()` with translator context, reusing existing terms where possible - [ ] New user-facing strings are wrapped in `I18n.tr()` with translator context, reusing existing terms where possible
- [ ] Go changes: ran `make fmt`, added/updated tests, `make test` passes, and `go mod tidy` is clean - [ ] Go changes: ran `make fmt`, added/updated tests, `make test` passes, and `go mod tidy` is clean
- [ ] QML changes: ran `make lint-qml` with no new warnings - [ ] QML changes: ran `make lint-qml` with no new warnings
- [ ] I have opened a corresponding pull request in dlx-docs to document any new behaviors: https://github.com/AvengeMedia/DankLinux-Docs - [ ] I have opened a corresponding pull request in dlx-docs to document any new behaviors: https://github.com/duxlabs-dev/dux-docs
+37 -12
View File
@@ -64,7 +64,14 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
- uses: anthropics/claude-code-action@v1 - name: Resolve base branch
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: echo "base=$(gh pr view ${{ github.event.issue.number }} --json baseRefName --jq .baseRefName)" >> "$GITHUB_OUTPUT"
- uses: anthropics/claude-code-action@9ca9355b36297178e28d37c799d1c9c8a28e6507
id: claude
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: | prompt: |
@@ -75,12 +82,13 @@ jobs:
--- ---
Text after `/claude review` is an optional focus. Honor it but still report anything serious outside it. Text after `/claude review` is an optional focus. Honor it but still report anything serious outside it.
The PR head is checked out. Use `gh pr view ${{ github.event.issue.number }}` and `gh pr diff ${{ github.event.issue.number }}`, read surrounding code as needed. The PR head is checked out and the base branch is fetched as `origin/${{ steps.pr.outputs.base }}`. Use `gh pr view ${{ github.event.issue.number }}` for title and description.
Get the diff from git, never `gh pr diff` (GitHub refuses diffs over 20000 lines): `git diff --stat origin/${{ steps.pr.outputs.base }}...HEAD`, then `git diff origin/${{ steps.pr.outputs.base }}...HEAD -- <path>` per file or directory. Read surrounding code as needed. Split a large PR across Task agents by area.
Steps: Steps:
1. Read CONTRIBUTING.md. 1. Read CONTRIBUTING.md.
2. Read the PR title, description and diff. 2. Read the PR title, description and diff.
3. Check existing comments so you don't repeat them: `gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments` and `gh pr view ${{ github.event.issue.number }} --comments`. Skip findings already covered by a comment that still applies. 3. Check existing comments so you don't repeat them: `gh api --method GET repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments` and `gh pr view ${{ github.event.issue.number }} --comments`. Skip findings already covered by a comment that still applies.
4. Verify each candidate finding against the code. Drop anything you can't confirm. 4. Verify each candidate finding against the code. Drop anything you can't confirm.
5. Post one inline comment per confirmed finding with `mcp__github_inline_comment__create_inline_comment` (`confirmed: true`). Say what's wrong, why, and the fix. Add a committable suggestion only if it alone fully fixes the issue. 5. Post one inline comment per confirmed finding with `mcp__github_inline_comment__create_inline_comment` (`confirmed: true`). Say what's wrong, why, and the fix. Add a committable suggestion only if it alone fully fixes the issue.
6. Post one new summary comment with `gh pr comment ${{ github.event.issue.number }} --body ...`. Never edit earlier comments. Format: 6. Post one new summary comment with `gh pr comment ${{ github.event.issue.number }} --body ...`. Never edit earlier comments. Format:
@@ -88,26 +96,43 @@ jobs:
## Claude review ## Claude review
<one line verdict> <one line verdict>
- <finding, file:line> per inline comment posted this run - <finding, file:line> per inline comment posted this run
Checked: <one line>. Model: claude-opus-5. Checked: <one line>. Model: claude-opus-5-5.
No findings: "No issues found." plus what was checked. No findings: "No issues found." plus what was checked.
Flag only issues introduced by this PR: Flag only issues introduced by this PR:
- Bugs: wrong logic, crashes, nil/undefined access, races, leaks (timers, connections, processes, handles), bad Go error handling, edge cases that will clearly be hit. - Bugs: wrong logic, crashes, nil/undefined access, races, leaks (timers, connections, processes, handles), bad Go error handling, edge cases that will clearly be hit.
- QML: undefined refs, missing imports, binding loops, bindings that never update, handlers on the wrong object, raw Flickable/ListView/ScrollView instead of the Dank* wrappers. - QML: undefined refs, missing imports, binding loops, bindings that never update, handlers on the wrong object, raw Flickable/ListView/ScrollView instead of the D* wrappers (DFlickable, DListView, DGridView).
- Deprecated shims: added code that uses a deprecated type or imports a deprecated module. Every shim calls `Deprecation.type(` or `Deprecation.module(`; list them with `git grep -h -e "Deprecation.type(" -e "Deprecation.module(" origin/${{ steps.pr.outputs.base }} -- quickshell`, which gives the old and new name. Grep the base, not HEAD, since a PR branched before the rename has no shims. Flag it even when the rest of the file still uses old names on the PR's base; suggest the new name and a rebase if the base predates it.
- Security: command injection, path traversal, unsafe file permissions, secrets in code. - Security: command injection, path traversal, unsafe file permissions, secrets in code.
- CONTRIBUTING.md rules: user-facing strings in `I18n.tr()` with translator context, reuse existing terms from quickshell/translations/en.json; en.json and template.json must not be touched in a PR; mocks under core/internal/mocks/ are regenerated not hand-edited; new dank-qml-common contract properties need the matching library change. - CONTRIBUTING.md rules: user-facing strings in `I18n.tr()` with translator context, reuse existing terms from quickshell/translations/en.json; en.json and template.json must not be touched in a PR; mocks under core/internal/mocks/ are regenerated not hand-edited; new dux-qml-common contract properties need the matching library change.
- Go: unformatted code, go.mod/go.sum out of sync with imports, tests that no longer compile.
Don't flag style or naming, pre-existing issues, anything a linter or compiler catches, speculative input-dependent problems, missing tests or docs, or refactor ideas. Not sure it's real: don't post it. Don't flag style or naming (deprecated shims are not naming), pre-existing issues, anything a linter or compiler catches, speculative input-dependent problems, missing tests or docs, or refactor ideas. Not sure it's real: don't post it.
Review only. Never edit files, commit, push, approve or request changes. Post each finding once. Keep comments short. Review only. Never edit files, commit, push, approve or request changes. Do not build, vet, format or run tests; CI already does that and the tools are not available here. Post each finding once. Keep comments short.
claude_args: | claude_args: |
--model claude-opus-5 --model claude-opus-5-5
--max-turns 80 --max-turns 120
--allowedTools "Read,Grep,Glob,LS,Task,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*),Bash(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*)" --allowedTools "Read,Grep,Glob,LS,Task,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr comment:*),Bash(gh api --method GET repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*),Bash(git merge-base:*),Bash(git grep:*)"
--disallowedTools "Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch" --disallowedTools "Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch"
- name: Upload transcript
if: always() && steps.claude.outputs.execution_file != ''
uses: actions/upload-artifact@v4
with:
name: claude-review-${{ github.event.issue.number }}-${{ github.run_id }}
path: ${{ steps.claude.outputs.execution_file }}
retention-days: 7
if-no-files-found: ignore
- name: Require a summary comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SINCE: ${{ github.event.comment.created_at }}
run: |
gh api "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments?since=$SINCE&per_page=100" \
--jq '[.[] | select(.body | startswith("## Claude review"))] | length' | grep -qv '^0$'
- if: failure() - if: failure()
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+9 -8
View File
@@ -1,12 +1,13 @@
name: Sync flake.lock to dank-qml-common name: Sync flake.lock to dux-qml-common
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
paths: paths:
- "dank-qml-common" - "dux-qml-common"
branches: branches:
- master - master
- "stable-*"
permissions: permissions:
contents: write contents: write
@@ -17,7 +18,7 @@ jobs:
steps: steps:
- name: Create GitHub App token - name: Create GitHub App token
id: app_token id: app_token
uses: actions/create-github-app-token@v1 uses: actions/create-github-app-token@v3
with: with:
app-id: ${{ secrets.APP_ID }} app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
@@ -34,10 +35,10 @@ jobs:
- name: Point flake input at the submodule commit - name: Point flake input at the submodule commit
run: | run: |
set -euo pipefail set -euo pipefail
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only) submodule_rev=$(git ls-tree HEAD dux-qml-common --object-only)
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])") 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; } [ "$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" nix flake lock --override-input dank-qml-common "github:duxlabs-dev/dux-qml-common/$submodule_rev"
- name: Commit and push flake.lock update - name: Commit and push flake.lock update
env: env:
@@ -48,9 +49,9 @@ jobs:
echo "No changes to flake.lock" echo "No changes to flake.lock"
exit 0 exit 0
fi fi
git config user.name "dms-ci[bot]" git config user.name "dux-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "344434142+dux-ci[bot]@users.noreply.github.com"
git add flake.lock git add flake.lock
git commit -m "nix: sync flake.lock to dank-qml-common submodule" git commit -m "nix: sync flake.lock to dux-qml-common submodule"
git pull --rebase origin ${{ github.ref_name }} git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }} git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
cache-dependency-path: ./core/go.sum cache-dependency-path: ./core/go.sum
- name: Test - name: Test
run: go test -v ./... run: go test -race -v ./...
- name: Build dms - name: Build dms
run: go build -v ./cmd/dms run: go build -v ./cmd/dms
+2 -2
View File
@@ -81,8 +81,8 @@ jobs:
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
echo "::error::tag ${TAG} already exists"; exit 1 echo "::error::tag ${TAG} already exists"; exit 1
fi fi
git config user.name "dms-ci[bot]" git config user.name "dux-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "344434142+dux-ci[bot]@users.noreply.github.com"
echo "${TAG}" > quickshell/VERSION echo "${TAG}" > quickshell/VERSION
git add quickshell/VERSION git add quickshell/VERSION
+1 -1
View File
@@ -7,7 +7,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
target: target:
description: "Release branch to audit (default: newest release/*)" description: "Release branch to audit (default: newest stable-*)"
required: false required: false
type: string type: string
+4 -4
View File
@@ -48,8 +48,8 @@ jobs:
COMMITS: ${{ toJSON(github.event.commits) }} COMMITS: ${{ toJSON(github.event.commits) }}
run: | run: |
set -euo pipefail set -euo pipefail
git config user.name "dms-ci[bot]" git config user.name "dux-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "344434142+dux-ci[bot]@users.noreply.github.com"
for sha in $(jq -r '.[].id' <<<"$COMMITS"); do for sha in $(jq -r '.[].id' <<<"$COMMITS"); do
git cat-file -e "$sha" 2>/dev/null || continue git cat-file -e "$sha" 2>/dev/null || continue
@@ -86,8 +86,8 @@ jobs:
PORT_SOURCE_PR: ${{ github.event.pull_request.number }} PORT_SOURCE_PR: ${{ github.event.pull_request.number }}
run: | run: |
set -euo pipefail set -euo pipefail
git config user.name "dms-ci[bot]" git config user.name "dux-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "344434142+dux-ci[bot]@users.noreply.github.com"
targets=$(jq -r '.[] | select(startswith("port ")) | sub("^port +"; "")' <<<"$LABELS" | targets=$(jq -r '.[] | select(startswith("port ")) | sub("^port +"; "")' <<<"$LABELS" |
sed -E 's,^stable-,,' | sort -u) sed -E 's,^stable-,,' | sort -u)
+7
View File
@@ -26,6 +26,13 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: core/go.mod go-version-file: core/go.mod
cache-dependency-path: core/go.sum
- name: Download Go modules
working-directory: core
run: for attempt in 1 2 3; do go mod download && exit 0; sleep 15; done; exit 1
- name: run pre-commit hooks - name: run pre-commit hooks
uses: j178/prek-action@v2 uses: j178/prek-action@v2
env:
SKIP: qml-tests
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
prune: prune:
name: Prune archive snapshots name: Prune archive snapshots
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell' if: github.repository == 'duxlabs-dev/dux-shell'
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
+62
View File
@@ -0,0 +1,62 @@
name: QML tests
on:
push:
paths:
- "quickshell/**"
- "dux-qml-common"
- "Makefile"
- "core/Makefile"
- "scripts/shell-test-excludes.txt"
- "flake.nix"
- ".pre-commit-config.yaml"
- ".github/workflows/qml-tests.yml"
- ".github/workflows/prek.yml"
pull_request:
branches: [master, main, "stable-*"]
paths:
- "quickshell/**"
- "dux-qml-common"
- "Makefile"
- "core/Makefile"
- "scripts/shell-test-excludes.txt"
- "flake.nix"
- ".pre-commit-config.yaml"
- ".github/workflows/qml-tests.yml"
- ".github/workflows/prek.yml"
workflow_dispatch:
permissions:
contents: read
jobs:
test-qml:
runs-on: ubuntu-latest
container: archlinux:base
timeout-minutes: 10
steps:
- name: Install test dependencies
run: |
pacman-key --init
pacman -Syu --noconfirm --needed git make nodejs python lua quickshell qt6-declarative ttf-dejavu
- name: Record runtime versions
run: |
pacman -Q quickshell qt6-declarative
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
persist-credentials: false
- name: Run QML pre-commit hook
uses: j178/prek-action@5337cb91e0fa35a7ff31b9ca345126d8bbbcdf16
with:
prek-version: "0.5.5"
extra-args: qml-tests --all-files
cache: ${{ github.event_name != 'pull_request' }}
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: safe.directory
GIT_CONFIG_VALUE_0: ${{ github.workspace }}
+24 -19
View File
@@ -73,6 +73,7 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Format check - name: Format check
if: matrix.goos == 'linux' if: matrix.goos == 'linux'
@@ -207,8 +208,8 @@ jobs:
# GH_TOKEN: ${{ steps.app_token.outputs.token }} # GH_TOKEN: ${{ steps.app_token.outputs.token }}
# run: | # run: |
# set -euo pipefail # set -euo pipefail
# git config user.name "dms-ci[bot]" # git config user.name "dux-ci[bot]"
# git config user.email "dms-ci[bot]@users.noreply.github.com" # git config user.email "344434142+dux-ci[bot]@users.noreply.github.com"
# version="${GITHUB_REF#refs/tags/}" # version="${GITHUB_REF#refs/tags/}"
# echo "Updating to version: $version" # echo "Updating to version: $version"
@@ -247,6 +248,7 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v5
@@ -280,7 +282,7 @@ jobs:
## Installation ## Installation
```bash ```bash
curl -fsSL https://install.danklinux.com | sh curl -fsSL https://install.duxlabs.dev | sh
``` ```
## Assets ## Assets
@@ -304,7 +306,7 @@ jobs:
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems - **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging) - **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
- **`dms-qml.tar.gz`** - QML source code only - **`dms-qml.tar.gz`** - QML source code only
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging) - **`dms-source.tar.gz`** - Full repository source with bundled DCommon (for distro packaging)
### Checksums ### Checksums
- **`*.sha256`** - SHA256 checksums for verifying download integrity - **`*.sha256`** - SHA256 checksums for verifying download integrity
@@ -352,15 +354,15 @@ jobs:
# Copy completions # Copy completions
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Replace the DankCommon symlink with real submodule content for packaging # Replace the DCommon symlink with real submodule content for packaging
rm quickshell/DankCommon rm quickshell/DCommon
cp -r dank-qml-common/DankCommon quickshell/DankCommon cp -r dux-qml-common/DCommon quickshell/DCommon
# Create full source tarball (GitHub tag archives never contain submodule content) # Create full source tarball (GitHub tag archives never contain submodule content)
VERSION_NUM=${TAG#v} VERSION_NUM=${TAG#v}
tar --exclude='.git' \ tar --exclude='.git' \
--exclude='.github' \ --exclude='.github' \
--exclude='dank-qml-common' \ --exclude='dux-qml-common' \
--exclude='_release_assets' \ --exclude='_release_assets' \
--exclude='_core_assets' \ --exclude='_core_assets' \
--exclude='RELEASE_BODY.md' \ --exclude='RELEASE_BODY.md' \
@@ -378,7 +380,9 @@ jobs:
# Tar the CONTENTS of quickshell/, not the directory itself # Tar the CONTENTS of quickshell/, not the directory itself
(cd quickshell && tar --exclude='.git' \ (cd quickshell && tar --exclude='.git' \
--exclude='.github' \ --exclude='.github' \
--exclude='./tests' \
--exclude='*.tar.gz' \ --exclude='*.tar.gz' \
--exclude-from=../scripts/shell-test-excludes.txt \
-czf ../_release_assets/dms-qml.tar.gz .) -czf ../_release_assets/dms-qml.tar.gz .)
# Generate checksum for QML package # Generate checksum for QML package
@@ -418,7 +422,7 @@ jobs:
# Create installation guide # Create installation guide
cat > _temp_full/INSTALL.md << 'EOFINSTALL' cat > _temp_full/INSTALL.md << 'EOFINSTALL'
# DankMaterialShell Installation # Dux Shell Installation
## Requirements ## Requirements
@@ -456,8 +460,8 @@ jobs:
## Configuration ## Configuration
- Settings are stored in `~/.config/DankMaterialShell/settings.json` - Settings are stored in `~/.config/dms/settings.json`
- Plugins go in `~/.config/DankMaterialShell/plugins/` - Plugins go in `~/.config/dms/plugins/`
- See the documentation in the `dms/` directory for more details - See the documentation in the `dms/` directory for more details
## Troubleshooting ## Troubleshooting
@@ -518,8 +522,8 @@ jobs:
bash scripts/notify-issues.sh "$PREVIOUS_TAG" "$TAG" bash scripts/notify-issues.sh "$PREVIOUS_TAG" "$TAG"
# Pin the satellite repos to this release's version. Each dispatch target # Pin the satellite repos to this release's version. Each dispatch target
# decides for itself whether to tag: X.Y.0 always tags, X.Y.Z skips when the # decides for itself whether to tag: X.Y.0 always tags master and cuts
# repo has no commits of its own since its last tag. # stable-X.Y, X.Y.Z tags stable-X.Y and skips when it has nothing new.
fanout: fanout:
name: Pin ${{ matrix.repo }} name: Pin ${{ matrix.repo }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -529,12 +533,13 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
repo: repo:
- dgop - dux-gop
- dankgo - dux-go
- dank-qml-common - dux-qml-common
- dankcalendar - dux-calendar
- danksearch - dux-search
- dank-greeter - dux-greeter
- dux-files
steps: steps:
- name: Create GitHub App token - name: Create GitHub App token
id: app_token id: app_token
+1 -1
View File
@@ -78,7 +78,7 @@ jobs:
echo "📦 Downloading DMS QML source for v${VERSION}..." echo "📦 Downloading DMS QML source for v${VERSION}..."
# Download DMS QML source # Download DMS QML source
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || { wget "https://github.com/duxlabs-dev/dux-shell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}" echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1 exit 1
} }
+4 -3
View File
@@ -23,7 +23,7 @@ jobs:
check-updates: check-updates:
name: Check for updates name: Check for updates
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell' if: github.repository == 'duxlabs-dev/dux-shell'
outputs: outputs:
has_updates: ${{ steps.check.outputs.has_updates }} has_updates: ${{ steps.check.outputs.has_updates }}
@@ -61,7 +61,7 @@ jobs:
# Helper function to check dms stable tag # Helper function to check dms stable tag
# Sets LATEST_TAG variable in parent scope if update needed # Sets LATEST_TAG variable in parent scope if update needed
check_dms_stable() { check_dms_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "") LATEST_TAG=$(curl -s https://api.github.com/repos/duxlabs-dev/dux-shell/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:dms/dms/dms.spec" 2>/dev/null || echo "") local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "") local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
@@ -192,7 +192,7 @@ jobs:
# Determine version for dms stable using the API # Determine version for dms stable using the API
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref # 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 }}" == "all" ]]; then
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "") LATEST_TAG=$(curl -s https://api.github.com/repos/duxlabs-dev/dux-shell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Using latest release from API: $LATEST_TAG" echo "Using latest release from API: $LATEST_TAG"
@@ -308,6 +308,7 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Install OSC - name: Install OSC
run: | run: |
+2 -1
View File
@@ -23,7 +23,7 @@ jobs:
check-updates: check-updates:
name: Check package/series updates name: Check package/series updates
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell' if: github.repository == 'duxlabs-dev/dux-shell'
outputs: outputs:
has_updates: ${{ steps.check.outputs.has_updates }} has_updates: ${{ steps.check.outputs.has_updates }}
@@ -100,6 +100,7 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
cache: false cache: false
- name: Install build dependencies - name: Install build dependencies
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
name: Build & Deploy XBPS packages name: Build & Deploy XBPS packages
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: preflight needs: preflight
if: github.repository == 'AvengeMedia/DankMaterialShell' && needs.preflight.outputs.should_build == 'true' if: github.repository == 'duxlabs-dev/dux-shell' && needs.preflight.outputs.should_build == 'true'
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
+4 -17
View File
@@ -8,6 +8,7 @@ on:
- "core/go.sum" - "core/go.sum"
branches: branches:
- master - master
- "stable-*"
permissions: permissions:
contents: write contents: write
@@ -33,21 +34,7 @@ jobs:
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31
- name: Update vendorHash in flake.nix - name: Update vendorHash in flake.nix
run: | run: scripts/update-vendor-hash.sh
set -euo pipefail
echo "Attempting nix build to get new vendorHash..."
if output=$(nix build .#dms-shell 2>&1); then
echo "Build succeeded, no hash update needed"
exit 0
fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
echo "Verifying build with new vendorHash..."
nix build .#dms-shell
echo "vendorHash updated successfully!"
- name: Commit and push vendorHash update - name: Commit and push vendorHash update
env: env:
@@ -55,8 +42,8 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
if ! git diff --quiet flake.nix; then if ! git diff --quiet flake.nix; then
git config user.name "dms-ci[bot]" git config user.name "dux-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "344434142+dux-ci[bot]@users.noreply.github.com"
git add flake.nix git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0 git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin ${{ github.ref_name }} git pull --rebase origin ${{ github.ref_name }}
+2 -1
View File
@@ -64,7 +64,7 @@ AGENTS-temp.md
niri-colors.generated.kdl niri-colors.generated.kdl
ghostty-colors.generated.conf ghostty-colors.generated.conf
# Notepad files (should be in ~/.local/state/DankMaterialShell/) # Notepad files (should be in ~/.local/state/dms/)
untitled-*.txt untitled-*.txt
file:* file:*
notepad-files/ notepad-files/
@@ -130,5 +130,6 @@ distro/void/masterdir*/
# Often gets built # Often gets built
core/dms core/dms
core/dankinstall
core/internal/shellembed/dist core/internal/shellembed/dist
+2 -2
View File
@@ -1,4 +1,4 @@
[submodule "dank-qml-common"] [submodule "dank-qml-common"]
path = dank-qml-common path = dux-qml-common
url = https://github.com/AvengeMedia/dank-qml-common.git url = https://github.com/duxlabs-dev/dux-qml-common.git
branch = master branch = master
+17 -1
View File
@@ -5,6 +5,16 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
- repo: local
hooks:
- id: qml-tests
name: shell logic and QML behavior tests
entry: python3 quickshell/tests/run-qml.py
language: system
types: []
files: ^(quickshell/|dux-qml-common$|Makefile$|core/Makefile$|scripts/shell-test-excludes\.txt$|flake\.nix$|\.pre-commit-config\.yaml$|\.github/workflows/(qml-tests|prek)\.yml$)
pass_filenames: false
require_serial: true
- repo: local - repo: local
hooks: hooks:
- id: shellcheck - id: shellcheck
@@ -20,6 +30,12 @@ repos:
language: system language: system
files: ^core/.*\.(go|mod|sum)$ files: ^core/.*\.(go|mod|sum)$
pass_filenames: false pass_filenames: false
- id: nix-vendor-hash
name: update Nix vendor hash
entry: bash -c 'command -v nix >/dev/null 2>&1 || exit 0; scripts/update-vendor-hash.sh --stage'
language: system
files: ^core/go\.(mod|sum)$
pass_filenames: false
- repo: local - repo: local
hooks: hooks:
- id: settings-search-index - id: settings-search-index
@@ -51,4 +67,4 @@ repos:
entry: bash -c 'if grep -nE "console\.(log|error|info|warn|debug)" "$@"; then echo "Use the Log service (log.info/warn/error/debug/fatal) instead of console.*" >&2; exit 1; fi' -- entry: bash -c 'if grep -nE "console\.(log|error|info|warn|debug)" "$@"; then echo "Use the Log service (log.info/warn/error/debug/fatal) instead of console.*" >&2; exit 1; fi' --
language: system language: system
files: ^quickshell/.*\.qml$ files: ^quickshell/.*\.qml$
exclude: ^quickshell/(Services/Log\.qml$|dms-plugins/|PLUGINS/) exclude: ^quickshell/(Services/Log\.qml$|dms-plugins/|PLUGINS/|tests/)
+33 -49
View File
@@ -1,62 +1,46 @@
## What is this? ## What is this?
DMS is an open-source, MIT-licensed desktop shell for Wayland compositors on linux. Like KWin, gnome-shell, or cosmic-shell - but designed to work with many different compositors. DMS is an open-source desktop shell for Wayland compositors on linux, built to work with many compositors.
## Repo Structure: ## Repo Structure
There are 2 main apps in the repo, they are designed to operate together. Additionally there is a dank-qml-common submodule which contains common QML widgets, like a shared library, that is leveraged by other quickshell apps in the Dank Desktop suite. - core/ - Go. `cmd/dms` runs quickshell, CLI utilities, and the unix socket server the UI depends on. `cmd/dankinstall` is a separate installer TUI.
- quickshell/ - the entire UI: Common/ (singletons, Theme, settings), Services/ (system and core access), Modules/ (bar, dock, lock, ...), Modals/, Widgets/ (shell-specific widgets and deprecated Dank* shims), PLUGINS/, translations/
- dux-qml-common/ - submodule of shared QML widgets
- docs/, distro/ - IPC and theme docs, packaging
There is a GO module (dankgo) that is used by core for certain common go functionality. ## Rules
### 1- core: - Keep it simple.
- Do not edit code in response to a question.
- Never commit, push, or open a PR unless asked.
- Point out glaring issues we missed.
- The shell runs 24/7. Audit every change for idle CPU, extra processes, timers, and retained memory.
- `core` and `quickshell` share the socket protocol. Changing one means checking the other.
- QML uses Theme tokens, never hardcoded colors, spacing, or constants.
- Use the D* widgets from qs.DCommon.Widgets (DListView, DFlickable, and so on) instead of raw ListView/Flickable/ScrollView. The Dank* names in Widgets/ are deprecated shims.
- User-facing text goes through I18n.tr(). Reuse catalog terms. Never edit translation catalogs.
- Guard clauses, early returns.
- Comment only what the code cannot say: a non-obvious why, a trap, a workaround. Never narrate what the code does or restate a name. Keep it to a line or two, and delete useless or paragraph comments you come across.
- Do not edit generated code. Mocks come from mockery.
- No library for a simple problem. Use a modern one for a complex, common problem.
Contains 2 apps: ## Tests
1. cmd/dms - The parent process for quickshell (shell runner), and general purpose tools and utilities (such as screenshot, clipboard). It also runs the unix socket server the UI uses for functionality quickshell doesn't provide. - A test must fail on the broken code. Otherwise do not write it.
2. cmd/dankinstall - An entirely separate app, a TUI for quickly installing DMS - No useless tests: "main.go contains func main()", defaults, "it instantiates", behavior the change didn't touch.
- QML tests must be fast. Call logic directly, load the smallest component, no fixed delays or animation waits, few meaningful assertions over a matrix.
- Tests cover `.js` modules, functions of `Services/` and `Common/` singletons, and IPC handlers. Never load, slice or mock anything under `Modules/`, `Modals/` or `Widgets/`, and never spawn a compositor, display server or shell. Those tests only ever broke on UI tweaks.
### 2- quickshell: ## Connections vs bound properties
A quickshell application, the entirety of the UI for DMS. It uses quickshells own services supplemented by the core unix socket server for additional functionality. - In per-item or per-screen components (delegates, bar widgets, popout and modal hosts, OSDs), no `Connections` for `on<Prop>Changed` handlers.
- Source in the same file: put the handler on it.
Layout: - Source outside (singleton, loader item, delegate parent): bind to a typed readonly property, never `var`, and handle that.
- Keep `Connections` for plain signals, hand-emitted change signals (`SettingsData.barConfigsChanged()`), constant-pointer targets, reassignable targets, and per-frame `var` sources.
- Common/ - app-wide singletons and helpers (Theme, SettingsData, SessionData, I18n, PopoutManager) - A bound handler fires once at creation if the initial value differs from the type default, and never on a same-value write. Convert only idempotent bodies. `enabled:` conditions become a guard inside the handler.
- Services/ - headless singletons that talk to the system and to core - Singletons under Services/ and Common/ keep their `Connections`.
- Modules/ - the visible shell: DankBar, ControlCenter, Dock, Lock, Notifications, OSD, and so on
- Modals/ - large standalone surfaces: Settings, DankLauncherV2, Clipboard, Greeter
- Widgets/ - reusable Dank* components (DankListView, DankIcon, DankToggle, ...)
- PLUGINS/ - the plugin system; third-party plugins load from the user's config dir
- translations/ - POEditor-synced catalogs
### Other directories:
- docs/ - IPC reference, custom theme docs
- distro/ - packaging for debian, fedora, nix, opensuse, ubuntu, void
## General Rules:
- Keep it simple. Do not overcomplicate things.
- Follow each apps' own conventions. For example, QML widgets use Theme tokens instead of hardcoding colors, spacing, or other constants.
- Resource usage is extremely important to DMS. The shell runs 24/7 on every machine it's installed on - audit any change for idle CPU cost, extra processes, timers, and retained memory.
- `core` and `quickshell` are tightly coupled through the unix socket protocol. When you are changing one, be mindful of the consequences for the other.
- Use the Dank* wrappers in Widgets/ (DankListView, DankFlickable, and so on) instead of raw ListView/Flickable/ScrollView.
- All user-facing text goes through I18n.tr(). Prefer reusing existing catalog terms over adding new ones. Never edit the translation catalogs by hand, they are synced with POEditor.
- Do not start editing code in response to a question. We'll tell you when to edit code.
- Do not leave paragraphs of comments on top of the code. You should try to avoid them as much as possible with understandable function names and code. If they are necessary even then, make them concise. Remove such comments when you come by them in the codebase. Comments should always move with code, not be left behind.
- Use guard statement patterns in any code you write.
- Do not write any useless tests, tests should cover input output validation - not useless things like "main.go contains func main()"
- Do not edit generated code directly, mocks are generated with mockery. Not edited by hand
- If we are missing a glaring issue when we ask you to do something, do not hesitate to point it out.
- Reinvent the wheel but do not reinvent the car. If you are solving a simple problem do not introduce a library. If you are solving a complex but a common problem, there is likely a modern library for it, if so, use it.
- Never commit or push code unless explicitly asked to do so.
- Never make a PR unless explicitly asked to do so.
## Commit Messages ## Commit Messages
Commit messages start with the part of the system they touched, followed by a short lowercase explanation of the work: `area: short lowercase explanation`, for example `network: report wifi state from the associated adapter`. Description only if required, in simple language.
network: report wifi state from the associated adapter
doctor: flag config files that fail to parse
The title should be concise. Description should explain the work in more detail (only if required) while still being concise. Use simple language, do not try to sound smart.
+1
View File
@@ -1,6 +1,7 @@
This file is more of a quick reference so I know what to account for before next releases. This file is more of a quick reference so I know what to account for before next releases.
# Next # Next
- notifications: timed per-app popup mutes (1h / 4h) from the notification context menu
- Go core migrated to dankgo (shared log/paths/errdefs/ipc/shellapp) - 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) - 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 - release binaries + all -git packages build with withshell; sockets/pidfiles/env names unchanged, IPC APIVersion 28 wire-compatible
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+27 -18
View File
@@ -6,10 +6,10 @@ To contribute fork this repository, make your changes, and open a pull request.
## Setup ## 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`: Clone with submodules — the shared widget library ([dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common)) is vendored at `dux-qml-common/` and symlinked into `quickshell/DCommon`:
```bash ```bash
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git git clone --recurse-submodules https://github.com/duxlabs-dev/dux-shell.git
# or, in an existing clone: # or, in an existing clone:
git submodule update --init git submodule update --init
``` ```
@@ -54,34 +54,29 @@ 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 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: The Go core depends on [dankgo](https://github.com/duxlabs-dev/dux-go) 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 go work init ./core ../dankgo
use (
./core
../dankgo
)
``` ```
## Shared widgets (dank-qml-common) ## Shared widgets (dux-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: Everything under `quickshell/DCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dux-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`. 1. Edit files under `dux-qml-common/` (or through the `quickshell/DCommon` 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 dux-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. 2. Commit and PR those changes in the `dux-qml-common` repo: `cd dux-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. 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: 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 ```bash
git config --global url."[email protected]:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/" git config --global url."[email protected]:duxlabs-dev/".pushInsteadOf "https://github.com/duxlabs-dev/"
``` ```
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. Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dux-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. Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so existing `qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` imports keep working for the shell and for plugins. The `Dank*` widget names are deprecated: each one logs a warning naming its replacement. New code imports `qs.DCommon.Widgets` and uses the `D*` names (`DButton`, `DIcon`, `DListView`, and so on).
## VSCode Setup ## VSCode Setup
@@ -143,7 +138,9 @@ touch .qmlls.ini
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`. 5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`.
6. Make your changes, test, and open a pull request. 6. Run `make test-qml` for QML unit tests (`nix develop --command make test-qml` with Nix). These use Qt 6 `qmltestrunner` offscreen and do not require a running shell. Set `QMLTESTRUNNER=/path/to/qmltestrunner` if needed.
7. Make your changes, test, and open a pull request.
### I18n/Localization ### I18n/Localization
@@ -161,7 +158,7 @@ Preferably, try to keep new terms to a minimum and re-use existing terms where p
Don't re-extract the translations. `en.json` and `template.json` are synced with POEditor by a maintainer script, so running `extract_translations.py` in your PR just makes a diff that fights the next sync. Add your `I18n.tr()` calls and leave the catalogs alone. (`settings_search_index.json` is the exception, the pre-commit hook regenerates that one when you touch settings QML.) Don't re-extract the translations. `en.json` and `template.json` are synced with POEditor by a maintainer script, so running `extract_translations.py` in your PR just makes a diff that fights the next sync. Add your `I18n.tr()` calls and leave the catalogs alone. (`settings_search_index.json` is the exception, the pre-commit hook regenerates that one when you touch settings QML.)
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. Strings inside `quickshell/DCommon/` are owned by the dux-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 `dux-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dux-qml-common/DCommon/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 `dux-qml-common`-tagged terms from the DMS project.
### GO (`core` directory) ### GO (`core` directory)
@@ -186,6 +183,18 @@ mockery
`core/.mockery.yml` lists every mocked interface and where its mock goes (e.g. `network.Backend` -> `internal/mocks/network/mock_Backend.go`). To mock a new interface, add it there under its package and regenerate. `core/.mockery.yml` lists every mocked interface and where its mock goes (e.g. `network.Backend` -> `internal/mocks/network/mock_Backend.go`). To mock a new interface, add it there under its package and regenerate.
## Generative AI
Using an LLM to help write code is fine. Submitting its output unread is not.
- You are responsible for every line you submit. You have read it, tested it, and can explain it in review.
- Say in the PR when a meaningful part of the code was AI generated.
- Write PR descriptions, issues, and comments yourself. No AI-written or AI-rewritten text. A few plain sentences in your own words beat a page of generated summary. PRs and issues with AI-written text get closed without review.
- You can use AI to troubleshoot, gather data, and information for new issues. The description should be written by a human.
- If you are not an english-native speaker you can use AI to translate your request or PR description
- Do not file issues or leave comments you have not verified yourself. Reports that do not reproduce get closed.
- PRs that read like unreviewed output, with narrating comments, invented APIs, or style that ignores the file they are in, get closed without review.
## Pull request ## Pull request
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting. Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
+19 -8
View File
@@ -1,4 +1,4 @@
# Root Makefile for DankMaterialShell (DMS) # Root Makefile for Dux Shell (DMS)
# Orchestrates building, installation, and systemd management # Orchestrates building, installation, and systemd management
# Build configuration # Build configuration
@@ -14,6 +14,7 @@ USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6)
SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user
SHELL_DIR=quickshell SHELL_DIR=quickshell
QMLTESTRUNNER ?=
SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications APPLICATIONS_DIR=$(DATA_DIR)/applications
@@ -41,10 +42,14 @@ clean:
lint-qml: lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh @./quickshell/scripts/qmllint-entrypoints.sh
# Pull the latest dank-qml-common and pin it everywhere it is consumed .PHONY: test-qml
test-qml:
QMLTESTRUNNER="$(QMLTESTRUNNER)" python3 quickshell/tests/run-qml.py
# Pull the latest dux-qml-common and pin it everywhere it is consumed
# (submodule pointer + nix flake input). Commit both in one change. # (submodule pointer + nix flake input). Commit both in one change.
update-common: update-common:
git submodule update --remote --merge dank-qml-common git submodule update --remote --merge dux-qml-common
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
# Installation targets # Installation targets
@@ -78,15 +83,17 @@ endif
install-icon: install-icon:
@echo "Installing icon..." @echo "Installing icon..."
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg @install -D -m 644 $(ASSETS_DIR)/dev.duxlabs.shell.svg $(ICON_DIR)/dev.duxlabs.shell.svg
@rm -f $(ICON_DIR)/com.danklinux.dms.svg
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true @gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
@echo "Icon installed" @echo "Icon installed"
install-desktop: install-desktop:
@echo "Installing desktop entries..." @echo "Installing desktop entries..."
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop @install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.desktop @install -D -m 644 $(ASSETS_DIR)/dev.duxlabs.shell.desktop $(APPLICATIONS_DIR)/dev.duxlabs.shell.desktop
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.notepad.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop @install -D -m 644 $(ASSETS_DIR)/dev.duxlabs.shell.notepad.desktop $(APPLICATIONS_DIR)/dev.duxlabs.shell.notepad.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true @update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entries installed" @echo "Desktop entries installed"
@@ -122,6 +129,8 @@ uninstall-systemd:
uninstall-icon: uninstall-icon:
@echo "Removing icon..." @echo "Removing icon..."
@rm -f $(ICON_DIR)/dev.duxlabs.shell.svg
@rm -f $(ICON_DIR)/com.danklinux.dms.svg
@rm -f $(ICON_DIR)/danklogo.svg @rm -f $(ICON_DIR)/danklogo.svg
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true @gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
@echo "Icon removed" @echo "Icon removed"
@@ -129,8 +138,9 @@ uninstall-icon:
uninstall-desktop: uninstall-desktop:
@echo "Removing desktop entries..." @echo "Removing desktop entries..."
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop @rm -f $(APPLICATIONS_DIR)/dms-open.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop @rm -f $(APPLICATIONS_DIR)/dev.duxlabs.shell.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop @rm -f $(APPLICATIONS_DIR)/dev.duxlabs.shell.notepad.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true @update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entries removed" @echo "Desktop entries removed"
@@ -147,6 +157,7 @@ help:
@echo " build - Same as 'all'" @echo " build - Same as 'all'"
@echo " clean - Clean build artifacts" @echo " clean - Clean build artifacts"
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS" @echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
@echo " test-qml - Run shell logic and Qt unit tests"
@echo "" @echo ""
@echo "Install:" @echo "Install:"
@echo " install - Build and install everything (requires sudo)" @echo " install - Build and install everything (requires sudo)"
+37 -34
View File
@@ -1,32 +1,32 @@
# DankMaterialShell # Dux Shell
<div align="center"> <div align="center">
<a href="https://danklinux.com"> <a href="https://duxlabs.dev">
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200"> <img src="assets/danklogo.svg" alt="Dux Shell" width="200">
</a> </a>
### A modern desktop shell for Wayland ### A modern desktop shell for Wayland
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/) Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
[![Documentation](https://img.shields.io/badge/docs-danklinux.com-9ccbfb?style=for-the-badge&labelColor=101418)](https://danklinux.com/docs) [![Documentation](https://img.shields.io/badge/docs-duxlabs.dev-9ccbfb?style=for-the-badge&labelColor=101418)](https://duxlabs.dev/docs)
[![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers) [![GitHub stars](https://img.shields.io/github/stars/duxlabs-dev/dux-shell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/duxlabs-dev/dux-shell/stargazers)
[![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE) [![GitHub License](https://img.shields.io/github/license/duxlabs-dev/dux-shell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/duxlabs-dev/dux-shell/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases) [![GitHub release](https://img.shields.io/github/v/release/duxlabs-dev/dux-shell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/duxlabs-dev/dux-shell/releases)
[![Arch version](https://img.shields.io/archlinux/v/extra/x86_64/dms-shell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://archlinux.org/packages/extra/x86_64/dms-shell/) [![Arch version](https://img.shields.io/archlinux/v/extra/x86_64/dms-shell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://archlinux.org/packages/extra/x86_64/dms-shell/)
[![AUR version (git)](<https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git)>)](https://aur.archlinux.org/packages/dms-shell-git) [![AUR version (git)](<https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git)>)](https://aur.archlinux.org/packages/dms-shell-git)
[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux) [![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fduxlabs)](https://ko-fi.com/duxlabs)
</div> </div>
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop. Dux Shell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure ## Repository Structure
This is a monorepo containing both the shell interface and the core backend services: This is a monorepo containing both the shell interface and the core backend services:
``` ```
DankMaterialShell/ dux-shell/
├── quickshell/ # QML-based shell interface ├── quickshell/ # QML-based shell interface
│ ├── Modules/ # UI components (panels, widgets, overlays) │ ├── Modules/ # UI components (panels, widgets, overlays)
│ ├── Services/ # System integration (audio, network, bluetooth) │ ├── Services/ # System integration (audio, network, bluetooth)
@@ -70,12 +70,14 @@ https://github.com/user-attachments/assets/d310418b-cc44-4ace-ac0d-2fc0c851291d
## Installation ## Installation
```bash ```bash
curl -fsSL https://install.danklinux.com | sh curl -fsSL https://install.duxlabs.dev | sh
``` ```
One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo. One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
**[Manual installation guide](https://danklinux.com/docs/dankmaterialshell/installation)** **FreeBSD:** native support is included in-tree; see [distro/freebsd/README.md](distro/freebsd/README.md) for supported integrations and setup notes.
**[Manual installation guide](https://duxlabs.dev/docs/dms/installation)**
## Features ## Features
@@ -83,10 +85,10 @@ One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, o
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16. Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
**System Monitoring** **System Monitoring**
Real-time CPU, RAM, GPU metrics and temperatures built into the dms daemon (powered by [dgop](https://github.com/AvengeMedia/dgop)). Process list with search and management. Real-time CPU, RAM, GPU metrics and temperatures built into the dms daemon (powered by [dgop](https://github.com/duxlabs-dev/dux-gop)). Process list with search and management.
**Powerful Launcher** **Powerful Launcher**
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins. Spotlight-style search for applications, files ([dsearch](https://github.com/duxlabs-dev/dux-search)), emojis, running windows, calculator, and commands. Extensible with plugins.
**Control Center** **Control Center**
Unified interface for network, Bluetooth, audio devices, display settings, and night mode. Unified interface for network, Bluetooth, audio devices, display settings, and night mode.
@@ -98,18 +100,18 @@ Notification center with grouping, rich text support, and keyboard navigation.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews. MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management** **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 a settings front-end for [dux-greeter](https://github.com/duxlabs-dev/dux-greeter).
**Plugin System** **Plugin System**
Extend functionality with the [plugin registry](https://plugins.danklinux.com). DMS keeps Extend functionality with the [plugin registry](https://plugins.duxlabs.dev). DMS keeps
`~/.config/DankMaterialShell/plugins.lock.json` synchronized with managed plugin installs and `~/.config/dms/plugins.lock.json` synchronized with managed plugin installs and
their exact Git commits, so the same plugins can be reproduced on another machine. their exact Git commits, so the same plugins can be reproduced on another machine.
## Supported Compositors ## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features. Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors) [Compositor configuration guide](https://duxlabs.dev/docs/dms/compositors)
## Command Line Interface ## Command Line Interface
@@ -126,26 +128,26 @@ dms plugins lock # Refresh the portable plugin lockfile
dms plugins restore ~/plugins.lock.json dms plugins restore ~/plugins.lock.json
``` ```
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc) [Full CLI and IPC documentation](https://duxlabs.dev/docs/dms/keybinds-ipc)
## Documentation ## Documentation
- **Website:** [danklinux.com](https://danklinux.com) - **Website:** [duxlabs.dev](https://duxlabs.dev)
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs/) - **Docs:** [duxlabs.dev/docs](https://duxlabs.dev/docs/)
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes) - **Theming:** [Application themes](https://duxlabs.dev/docs/dms/application-themes) | [Custom themes](https://duxlabs.dev/docs/dms/custom-themes)
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview) - **Plugins:** [Development guide](https://duxlabs.dev/docs/dms/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc) - **Support:** [Ko-fi](https://ko-fi.com/duxlabs)
## Dank Projects ## Dux Projects
DMS is one piece of the suite. The rest lives in its own repos: 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. - [dux-greeter](https://github.com/duxlabs-dev/dux-greeter) - greetd login screen with the Dux Shell 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. - [dux-calendar](https://github.com/duxlabs-dev/dux-calendar) - Local, Google, Microsoft, and CalDAV calendars for the Dux desktop.
- [dgop](https://github.com/AvengeMedia/dgop) - System monitoring TUI and Go library; its library powers the process list and dashboard widgets inside the dms daemon. - [dgop](https://github.com/duxlabs-dev/dux-gop) - System monitoring TUI and Go library; its library powers the process list and dashboard widgets inside the dms daemon.
- [dsearch](https://github.com/AvengeMedia/danksearch) - Fast file search that powers file results in the launcher. - [dsearch](https://github.com/duxlabs-dev/dux-search) - 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. - [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) - Shared QML widgets and components used by DMS, dux-greeter, and dux-calendar.
- [dankgo](https://github.com/AvengeMedia/dankgo) - Common Go modules behind the single binary apps. - [dux-go](https://github.com/duxlabs-dev/dux-go) - Common Go modules behind the single binary apps.
## Development ## Development
@@ -175,7 +177,7 @@ quickshell -p quickshell/
```nix ```nix
{ {
inputs.dms.url = "github:AvengeMedia/DankMaterialShell"; inputs.dms.url = "github:duxlabs-dev/dux-shell";
# Use in home-manager or NixOS configuration # Use in home-manager or NixOS configuration
imports = [ inputs.dms.homeModules.dank-material-shell ]; imports = [ inputs.dms.homeModules.dank-material-shell ];
@@ -191,7 +193,7 @@ Contributions welcome. Bug fixes, widgets, features, documentation, and plugins
3. Test thoroughly 3. Test thoroughly
4. Open a pull request 4. Open a pull request
For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs). For documentation contributions, see [dux-docs](https://github.com/duxlabs-dev/dux-docs).
## Credits ## Credits
@@ -200,10 +202,11 @@ For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeM
- [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell) - [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration - [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration - [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
- [Reverier-Xu](https://github.com/Reverier-Xu) - [Ori-fcitx5](https://github.com/Reverier-Xu/Ori-fcitx5) inspiration for Fcitx5 theme
## Star History ## Star History
[![Star History Chart](https://star-history.dera.page/svg?repos=AvengeMedia/DankMaterialShell&type=date&legend=top-left)](https://star-history.dera.page/#AvengeMedia/DankMaterialShell&type=date&legend=top-left) [![Star History Chart](https://star-history.dera.page/svg?repos=duxlabs-dev/dux-shell&type=date&legend=top-left)](https://star-history.dera.page/#duxlabs-dev/dux-shell&type=date&legend=top-left)
## License ## License
@@ -1,10 +1,10 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=DankMaterialShell Name=Dux Shell
GenericName=Desktop Shell GenericName=Desktop Shell
Comment=Material 3 inspired shell for Wayland compositors Comment=Material 3 inspired shell for Wayland compositors
Exec=dms run Exec=dms run
Icon=danklogo Icon=dev.duxlabs.shell
Terminal=false Terminal=false
NoDisplay=true NoDisplay=true
StartupNotify=false StartupNotify=false
@@ -2,9 +2,9 @@
Type=Application Type=Application
Name=DMS Notepad Name=DMS Notepad
GenericName=Text Editor GenericName=Text Editor
Comment=Quick notes and text files in the dank desktop shell Comment=Quick notes and text files in Dux Shell
Exec=dms ipc call notepad openFile %f Exec=dms ipc call notepad openFile %f
Icon=danklogo Icon=dev.duxlabs.shell
Terminal=false Terminal=false
NoDisplay=true NoDisplay=true
Categories=Utility;TextEditor; Categories=Utility;TextEditor;
+113
View File
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="482.90668"
height="558.15088"
viewBox="0 0 482.90667 558.15088"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
shape-rendering="auto"
style="image-rendering: auto; filter: url(#smoothing);">
<defs
id="defs1">
<filter id="smoothing" x="-0.05" y="-0.05" width="1.1" height="1.1">
<feGaussianBlur in="SourceGraphic" stdDeviation="0.5" />
</filter>
<color-profile
name="sRGB IEC61966-2.1"
xlink:href="data:application/vnd.iccprofile;base64,AAAMbExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
id="color-profile1" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-673.87432,-704.25842)"
id="path21" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath25">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-466.30451,-703.59782)"
id="path25" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath27">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-695.28002,-473.92741)"
id="path27" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath29">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-457.93881,-632.99062)"
id="path29" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath31">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-466.30451,-703.59782)"
id="path31" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath33">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-614.51722,-638.93302)"
id="path33" />
</clipPath>
</defs>
<g
id="layer-MC0"
transform="translate(-486.31024,-515.02722)">
<path
id="path20"
d="M 0,0 C -1.568,1.568 -3.163,3.098 -4.787,4.61 -5.944,3.966 -7.185,3.35 -8.529,2.762 -9.658,2.277 -10.815,1.82 -11.981,1.4 c 1.885,-1.689 3.742,-3.425 5.552,-5.207 0.448,-0.429 0.886,-0.868 1.325,-1.306 2.221,-2.221 4.386,-4.498 6.476,-6.84 27.639,-30.784 44.453,-71.459 44.453,-116.089 0,-29.347 -7.259,-56.977 -20.09,-81.21 -2.192,-4.134 -4.544,-8.174 -7.054,-12.102 -6.83,-10.74 -14.827,-20.669 -23.785,-29.636 -5.944,-5.944 -12.317,-11.459 -19.073,-16.498 -0.56,-0.42 -1.12,-0.83 -1.689,-1.231 -28.675,-20.893 -63.975,-33.201 -102.186,-33.201 -48.018,0 -91.464,19.456 -122.948,50.93 -0.737,0.737 -1.465,1.474 -2.174,2.221 -0.55,0.569 -1.101,1.147 -1.633,1.726 -15.545,16.553 -27.881,36.14 -36.018,57.779 -3.098,8.211 -5.58,16.712 -7.409,25.464 -2.417,11.534 -3.686,23.496 -3.686,35.758 0.01,42.326 15.117,81.097 40.246,111.246 -2.072,1.278 -3.975,2.809 -5.534,4.637 -26.174,-31.399 -41.934,-71.822 -41.934,-115.883 0,-18.187 2.678,-35.748 7.67,-52.311 3.359,-11.142 7.754,-21.835 13.092,-31.95 8.528,-16.208 19.446,-30.961 32.276,-43.801 1.251,-1.25 2.529,-2.491 3.817,-3.685 0.662,-0.644 1.334,-1.26 2.006,-1.876 10.862,-9.938 22.945,-18.578 36,-25.661 25.632,-13.913 55.016,-21.816 86.229,-21.816 2.454,0 4.908,0.047 7.334,0.14 36.056,1.446 69.424,13.427 97.054,32.976 0.569,0.392 1.148,0.793 1.698,1.204 7.82,5.655 15.164,11.925 21.966,18.718 7.904,7.904 15.07,16.526 21.406,25.773 2.556,3.723 4.973,7.558 7.25,11.477 15.499,26.697 24.382,57.723 24.382,90.812 C 53.038,-78.055 32.762,-32.762 0,0"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,898.49907,660.9888)"
clip-path="url(#clipPath21)" />
<path
id="path24"
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
clip-path="url(#clipPath25)" />
<path
id="path26"
d="m 0,0 c -6.336,-9.247 -13.502,-17.869 -21.406,-25.773 -6.802,-6.793 -14.146,-13.063 -21.965,-18.718 -0.551,-0.411 -1.129,-0.812 -1.699,-1.204 -27.629,-19.549 -60.998,-31.53 -97.053,-32.976 -33.247,1.129 -64.852,8.762 -93.564,21.676 -13.054,7.082 -25.138,15.723 -36,25.661 -0.672,0.616 -1.343,1.232 -2.006,1.876 -1.288,1.194 -2.566,2.435 -3.816,3.685 -12.831,12.84 -23.748,27.593 -32.277,43.801 -1.092,7.651 -1.941,15.378 -2.445,23.039 -0.102,1.502 -0.186,3.004 -0.261,4.497 0,0 -2.865,29.795 23.944,36.634 26.827,6.84 65.654,19.745 87.722,50.305 0,0 0.327,-8.38 5.506,-15.779 8.034,-11.422 46.674,-100.46 46.674,-100.46 l 6.121,51.135 -13.978,15.079 -4.553,-1.987 15.228,16.544 c 0,0 8.94,4.218 16.554,-0.653 7.605,-4.899 14.146,-16.153 14.146,-16.153 l -5.879,3.892 -4.227,-12.364 c -0.756,-2.184 -0.83,-4.535 -0.233,-6.784 l 16.796,-62.687 c 0,0 -0.243,87.415 -2.781,111.685 0,0 14.221,-10.367 21.621,-14.454 7.381,-4.078 47.215,-20.407 53.738,-22.395 6.504,-1.987 13.222,-5.841 15.882,-11.916 1.026,-2.351 8.594,-26.939 17.486,-56.229 C -1.829,6.028 -0.914,3.023 0,0"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,927.04,968.0968)"
clip-path="url(#clipPath27)" />
<path
id="path28"
d="m 0,0 c 0,0 -3.081,-67.22 -8.64,-90.616 -5.559,-23.397 30.316,0 30.316,0 l 20.9,68.629 z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,610.58507,756.01253)"
clip-path="url(#clipPath29)" />
<path
id="path30"
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
clip-path="url(#clipPath31)" />
<path
id="path32"
d="m 0,0 c 0,0 12.178,7.604 15.029,18.355 0,0 36.913,-9.243 52.904,-26.719 C 66.887,-8.091 29.642,2.031 0,0 m -36.152,-5.643 c -26.791,0.404 -31.781,16.05 -34.086,29.523 -0.511,2.958 -3.148,5.073 -6.13,4.883 l -46.275,-4.764 14.922,7.14 c 3.505,1.687 7.164,3.077 10.895,4.17 3.742,1.093 7.556,1.901 11.429,2.376 4.051,0.499 8.542,1.01 13.46,1.509 0,0 6.19,2.459 7.414,-4.479 1.259,-6.926 0.499,-31.864 28.287,-31.103 27.777,0.76 32.612,19.412 32.612,19.412 0,0 3.458,14.471 9.232,14.257 5.263,-0.202 7.27,-6.142 7.853,-10.479 0.142,-1.045 -0.06,-3.54 0.582,-4.324 -0.939,1.152 -2.162,2.032 -3.564,2.554 C 7.604,26.113 3.065,26.945 2.471,21.658 1.616,13.877 -9.599,-6.059 -36.152,-5.643 m 13.306,48.354 9.528,-2.377 c 0,0 -2.388,-17.5 -11.227,-21.979 -0.25,-0.13 4.194,14.281 1.699,24.356 m 59.022,3.041 3.79,-1.687 c 0,0 2.198,-14.209 -3.79,-20.577 0,0 2.174,12.474 0,22.264 m -187.451,35.63 c 12.225,2.067 24.45,4.229 36.664,6.332 l 36.663,6.392 73.303,12.748 c 2.958,0.522 5.774,-1.462 6.285,-4.408 0.522,-2.947 -1.462,-5.762 -4.42,-6.285 -0.119,-0.024 -0.261,-0.036 -0.38,-0.047 H -3.184 L -114.243,85.029 c -12.344,-1.224 -24.676,-2.495 -37.032,-3.647 M 43.97,16.93 c 3.54,4.693 5.215,23.096 5.215,23.096 l 1.497,2.518 v 8.744 C 29.381,61.327 11.869,50.682 11.869,50.682 l -16.491,0.749 c -3.112,0.142 -7.449,2.637 -10.644,3.433 -4.42,1.093 -8.804,2.068 -13.342,2.566 -16.407,1.735 -32.933,0 -49.091,-2.934 -7.021,-1.271 -13.971,-2.851 -20.957,-4.325 -6.701,-1.425 -12.878,-3.908 -19.151,-6.605 -4.313,-1.842 -8.649,-3.659 -12.641,-6.119 -2.436,-1.496 -4.741,-3.243 -6.737,-5.31 -2.126,-2.21 -3.659,-4.859 -5.476,-7.319 -1.545,-2.091 -4.907,-0.463 -4.147,2.032 0.024,0.059 0.048,0.119 0.06,0.154 0.867,2.044 2.221,4.646 3.659,6.345 3.588,4.241 8.958,8.292 13.686,11.12 10.039,6.071 21.48,9.766 32.767,12.985 24.771,7.057 51.442,8.138 77.009,10.55 14.114,1.331 28.43,2.091 42.473,4.016 12.784,1.734 37.935,4.859 36.176,22.882 -1.71,18.129 -59.355,18.712 -59.355,18.712 0,0 -21.943,45.027 -27.372,50.468 -5.418,5.418 -16.503,18.474 -74.254,4.384 -57.739,-14.078 -55.327,-42.259 -55.327,-42.259 l -2.412,-39.622 c 0,0 -43.198,-11.179 -41.832,-23.321 1.391,-12.142 39.171,-12.819 39.171,-12.819 0,0 8.577,0.439 20.696,1.152 l -5.382,-9.683 c -6.749,-12.13 -6.38,-25.258 -8.851,-38.908 -4.277,-23.631 11.963,-52.702 33.978,-62.147 21.1,-9.053 53.949,-13.782 99.012,6.368 11.085,4.954 20.328,13.282 26.66,23.618 l 0.499,0.796 c 22.728,26.125 80.574,9.386 80.574,9.386 C 81.774,1.699 43.97,16.93 43.97,16.93"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,819.35627,748.08933)"
clip-path="url(#clipPath33)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+1 -1
View File
@@ -3,7 +3,7 @@ Type=Application
Name=DMS Name=DMS
Comment=Select an application to open links and files Comment=Select an application to open links and files
Exec=dms open %u Exec=dms open %u
Icon=danklogo Icon=dev.duxlabs.shell
Terminal=false Terminal=false
NoDisplay=true NoDisplay=true
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/dms;text/html;application/xhtml+xml; MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/dms;text/html;application/xhtml+xml;
+4 -3
View File
@@ -1,12 +1,13 @@
[Unit] [Unit]
Description=Dank Material Shell (DMS) Description=Dux Shell (DMS)
PartOf=graphical-session.target PartOf=graphical-session.target
After=graphical-session.target After=graphical-session.target
Requisite=graphical-session.target Requisite=graphical-session.target
[Service] [Service]
Type=dbus Type=notify
BusName=org.freedesktop.Notifications NotifyAccess=main
Environment=QS_DISABLE_CRASH_HANDLER=1
ExecStart=/usr/bin/dms run --session ExecStart=/usr/bin/dms run --session
ExecReload=/bin/kill -USR1 $MAINPID ExecReload=/bin/kill -USR1 $MAINPID
LimitNOFILE=16384:infinity LimitNOFILE=16384:infinity
+10 -10
View File
@@ -6,13 +6,13 @@ template: testify
template-data: template-data:
unroll-variadic: true unroll-variadic: true
packages: packages:
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation: github.com/duxlabs-dev/dux-shell/core/internal/geolocation:
config: config:
dir: internal/mocks/geolocation dir: internal/mocks/geolocation
pkgname: mocks_geolocation pkgname: mocks_geolocation
interfaces: interfaces:
Client: {} Client: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups: github.com/duxlabs-dev/dux-shell/core/internal/server/cups:
config: config:
dir: internal/mocks/cups dir: internal/mocks/cups
pkgname: mocks_cups pkgname: mocks_cups
@@ -22,46 +22,46 @@ packages:
config: config:
dir: internal/mocks/cups_pkhelper dir: internal/mocks/cups_pkhelper
pkgname: mocks_cups_pkhelper pkgname: mocks_cups_pkhelper
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev: github.com/duxlabs-dev/dux-shell/core/internal/server/evdev:
config: config:
dir: internal/mocks/evdev dir: internal/mocks/evdev
pkgname: mocks_evdev pkgname: mocks_evdev
interfaces: interfaces:
EvdevDevice: {} EvdevDevice: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network: github.com/duxlabs-dev/dux-shell/core/internal/server/network:
config: config:
dir: internal/mocks/network dir: internal/mocks/network
pkgname: mocks_network pkgname: mocks_network
interfaces: interfaces:
Backend: {} Backend: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext: github.com/duxlabs-dev/dux-shell/core/internal/server/wlcontext:
config: config:
dir: internal/mocks/wlcontext dir: internal/mocks/wlcontext
pkgname: mocks_wlcontext pkgname: mocks_wlcontext
interfaces: interfaces:
WaylandContext: {} WaylandContext: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/utils: github.com/duxlabs-dev/dux-shell/core/internal/utils:
config: config:
dir: internal/mocks/utils dir: internal/mocks/utils
pkgname: mocks_utils pkgname: mocks_utils
interfaces: interfaces:
AppChecker: {} AppChecker: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/version: github.com/duxlabs-dev/dux-shell/core/internal/version:
config: config:
dir: internal/mocks/version dir: internal/mocks/version
pkgname: mocks_version pkgname: mocks_version
interfaces: interfaces:
VersionFetcher: {} VersionFetcher: {}
github.com/AvengeMedia/dankgo/wayland/client: github.com/duxlabs-dev/dux-go/wayland/client:
config: config:
dir: internal/mocks/wlclient dir: internal/mocks/wlclient
pkgname: mocks_wlclient pkgname: mocks_wlclient
interfaces: interfaces:
WaylandDisplay: {} WaylandDisplay: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/plugins: github.com/duxlabs-dev/dux-shell/core/internal/plugins:
interfaces: interfaces:
GitClient: {} GitClient: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness: github.com/duxlabs-dev/dux-shell/core/internal/server/brightness:
config: config:
dir: internal/mocks/brightness dir: internal/mocks/brightness
pkgname: mocks_brightness pkgname: mocks_brightness
+20 -7
View File
@@ -1,13 +1,26 @@
repos: repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.13.1
hooks:
- id: golangci-lint-fmt
require_serial: true
- id: golangci-lint-full
- id: golangci-lint-config-verify
- repo: local - repo: local
hooks: hooks:
- id: golangci-lint-fmt
name: golangci-lint-fmt
entry: go run github.com/golangci/golangci-lint/v2/cmd/[email protected] fmt
language: system
types: [go]
require_serial: true
pass_filenames: false
- id: golangci-lint-full
name: golangci-lint-full
entry: go run github.com/golangci/golangci-lint/v2/cmd/[email protected] run --fix
language: system
types: [go]
require_serial: true
pass_filenames: false
- id: golangci-lint-config-verify
name: golangci-lint-config-verify
entry: go run github.com/golangci/golangci-lint/v2/cmd/[email protected] config verify
language: system
files: '\.golangci\.(?:yml|yaml|toml|json)'
pass_filenames: false
- id: go-test - id: go-test
name: go test name: go test
entry: go test ./... entry: go test ./...
+3 -10
View File
@@ -37,20 +37,13 @@ endif
# Default target # Default target
all: build 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: sync-shell:
@test -e $(SHELL_SRC)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; } @test -e $(SHELL_SRC)/DCommon/Widgets/DIcon.qml || { echo "DCommon missing: run git submodule update --init"; exit 1; }
@rm -rf $(EMBED_DIR) @rm -rf $(EMBED_DIR)
@mkdir -p $(EMBED_DIR) @mkdir -p $(EMBED_DIR)
@tar -C $(SHELL_SRC) --exclude=.qmlls.ini -chf - . | tar -C $(EMBED_DIR) -xf - @tar -C $(SHELL_SRC) --exclude=.qmlls.ini --exclude-from=../scripts/shell-test-excludes.txt -chf - . | tar -C $(EMBED_DIR) -xf -
@rm -rf $(EMBED_DIR)/.git* $(EMBED_DIR)/.github @rm -rf $(EMBED_DIR)/.git* $(EMBED_DIR)/.github
@rm -rf $(EMBED_DIR)/tests
@find $(EMBED_DIR) -type d \( -name .claude -o -name .vscode \) -prune -exec rm -rf {} + @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)/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/i18nsync.py $(EMBED_DIR)/scripts/build-vscode-vsix.sh $(EMBED_DIR)/scripts/qmllint-entrypoints.sh
+9 -9
View File
@@ -1,6 +1,6 @@
# DMS Backend & CLI # DMS Backend & CLI
Go-based backend for DankMaterialShell providing system integration, IPC, and installation tools. Go-based backend for Dux Shell providing system integration, IPC, and installation tools.
**See [root README](../README.md) for project overview and installation.** **See [root README](../README.md) for project overview and installation.**
@@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication.
- `dms brightness [list|set]` - Control display/monitor brightness - `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below) - `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages) - `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` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/duxlabs-dev/dux-greeter) binary
### Color Picker ### Color Picker
@@ -132,10 +132,10 @@ This runs gofmt, golangci-lint, tests, and builds before each commit when `core/
**Regenerating Wayland Protocol Bindings:** **Regenerating Wayland Protocol Bindings:**
Protocol XML files live in `internal/proto/xml/`. Bindings are generated with the [dankgo](https://github.com/AvengeMedia/dankgo) `go-wayland-scanner`: Protocol XML files live in `internal/proto/xml/`. Bindings are generated with the [dankgo](https://github.com/duxlabs-dev/dux-go) `go-wayland-scanner`:
```bash ```bash
go run github.com/AvengeMedia/dankgo/cmd/go-wayland-scanner@latest \ go run github.com/duxlabs-dev/dux-go/cmd/go-wayland-scanner@latest \
-i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \ -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
-pkg wlr_gamma_control -o internal/proto/wlr_gamma_control/gamma_control.go -pkg wlr_gamma_control -o internal/proto/wlr_gamma_control/gamma_control.go
``` ```
@@ -152,7 +152,7 @@ go run github.com/AvengeMedia/dankgo/cmd/go-wayland-scanner@latest \
**Interactive (TUI):** **Interactive (TUI):**
```bash ```bash
curl -fsSL https://install.danklinux.com | sh curl -fsSL https://install.duxlabs.dev | sh
``` ```
**Headless (unattended):** **Headless (unattended):**
@@ -160,9 +160,9 @@ curl -fsSL https://install.danklinux.com | sh
Headless mode requires cached credentials or a passwordless rule for your privilege escalation tool (sudo, doas, or run0). With sudo, run `sudo -v` first: Headless mode requires cached credentials or a passwordless rule for your privilege escalation tool (sudo, doas, or run0). With sudo, run `sudo -v` first:
```bash ```bash
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y sudo -v && curl -fsSL https://install.duxlabs.dev | sh -s -- -c niri -t ghostty -y
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --dms-greeter -y sudo -v && curl -fsSL https://install.duxlabs.dev | sh -s -- -c hyprland -t kitty --dms-greeter -y
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty --git-deps niri,quickshell --all-features -y sudo -v && curl -fsSL https://install.duxlabs.dev | sh -s -- -c niri -t ghostty --git-deps niri,quickshell --all-features -y
``` ```
| Flag | Short | Description | | Flag | Short | Description |
@@ -225,6 +225,6 @@ Most packages available in standard repos. Minimal building required.
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration. Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
**Void Linux** **Void Linux**
Uses XBPS with the DMS and DankLinux self-hosted repositories. Uses XBPS with the DMS and Dux Labs self-hosted repositories.
See installer output for distribution-specific details during installation. See installer output for distribution-specific details during installation.
+5 -5
View File
@@ -5,10 +5,10 @@ import (
"os" "os"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/headless"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/duxlabs-dev/dux-shell/core/internal/headless"
"github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/duxlabs-dev/dux-shell/core/internal/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -33,8 +33,8 @@ var (
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "dankinstall", Use: "dankinstall",
Short: "Install DankMaterialShell and its dependencies", Short: "Install Dux Shell and its dependencies",
Long: `dankinstall sets up DankMaterialShell with your chosen compositor and terminal. Long: `dankinstall sets up Dux Shell with your chosen compositor and terminal.
Without flags, it launches an interactive TUI. Providing either --compositor Without flags, it launches an interactive TUI. Providing either --compositor
or --term activates headless (unattended) mode, which requires both flags. or --term activates headless (unattended) mode, which requires both flags.
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam" sharedpam "github.com/duxlabs-dev/dux-shell/core/internal/pam"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/backup" "github.com/duxlabs-dev/dux-shell/core/internal/backup"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -17,7 +17,7 @@ var backupCmd = &cobra.Command{
var backupCreateCmd = &cobra.Command{ var backupCreateCmd = &cobra.Command{
Use: "create", Use: "create",
Short: "Create a tar.gz backup of ~/.config/DankMaterialShell (settings, plugins, themes)", Short: "Create a tar.gz backup of ~/.config/dms (settings, plugins, themes)",
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: runBackupCreate, Run: runBackupCreate,
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur" "github.com/duxlabs-dev/dux-shell/core/internal/blur"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" "github.com/duxlabs-dev/dux-shell/core/internal/server/brightness"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+3 -3
View File
@@ -8,11 +8,11 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight"
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight/lexers"
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight/styles"
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/duxlabs-dev/dux-shell/core/internal/highlight"
"github.com/duxlabs-dev/dux-shell/core/internal/highlight/lexers"
"github.com/duxlabs-dev/dux-shell/core/internal/highlight/styles"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
+26 -20
View File
@@ -28,10 +28,10 @@ import (
_ "golang.org/x/image/tiff" _ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp" _ "golang.org/x/image/webp"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/duxlabs-dev/dux-go/ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-go/wlclipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/duxlabs-dev/dux-shell/core/internal/clipboard"
"github.com/AvengeMedia/dankgo/wlclipboard" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -430,7 +430,7 @@ func runCommand(args []string, stdin []byte) {
} }
func runClipHistory(cmd *cobra.Command, args []string) { func runClipHistory(cmd *cobra.Command, args []string) {
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.getHistory", Method: "clipboard.getHistory",
} }
@@ -501,7 +501,7 @@ func runClipGet(cmd *cobra.Command, args []string) {
} }
if clipGetCopy { if clipGetCopy {
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.copyEntry", Method: "clipboard.copyEntry",
Params: map[string]any{"id": id}, Params: map[string]any{"id": id},
@@ -519,7 +519,7 @@ func runClipGet(cmd *cobra.Command, args []string) {
return return
} }
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.getEntry", Method: "clipboard.getEntry",
Params: map[string]any{ Params: map[string]any{
@@ -565,7 +565,7 @@ func runClipDelete(cmd *cobra.Command, args []string) {
log.Fatalf("Invalid ID: %v", err) log.Fatalf("Invalid ID: %v", err)
} }
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.deleteEntry", Method: "clipboard.deleteEntry",
Params: map[string]any{ Params: map[string]any{
@@ -586,7 +586,7 @@ func runClipDelete(cmd *cobra.Command, args []string) {
} }
func runClipClear(cmd *cobra.Command, args []string) { func runClipClear(cmd *cobra.Command, args []string) {
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.clearHistory", Method: "clipboard.clearHistory",
} }
@@ -621,7 +621,7 @@ func runClipSearch(cmd *cobra.Command, args []string) {
params["isImage"] = false params["isImage"] = false
} }
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.search", Method: "clipboard.search",
Params: params, Params: params,
@@ -688,7 +688,7 @@ func runClipSearch(cmd *cobra.Command, args []string) {
} }
func runClipConfigGet(cmd *cobra.Command, args []string) { func runClipConfigGet(cmd *cobra.Command, args []string) {
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.getConfig", Method: "clipboard.getConfig",
} }
@@ -742,7 +742,7 @@ func runClipConfigSet(cmd *cobra.Command, args []string) {
return return
} }
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.setConfig", Method: "clipboard.setConfig",
Params: params, Params: params,
@@ -761,7 +761,7 @@ func runClipConfigSet(cmd *cobra.Command, args []string) {
} }
func runClipExport(cmd *cobra.Command, args []string) { func runClipExport(cmd *cobra.Command, args []string) {
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.getHistory", Method: "clipboard.getHistory",
} }
@@ -1004,19 +1004,25 @@ func downloadToTempFile(rawURL string) (string, error) {
return filePath, nil return filePath, nil
} }
// copyFileToClipboard copies through the server so the entry lands in
// history, and serves the same offers itself when the server is down or
// refuses (e.g. file over the history size limit).
func copyFileToClipboard(filePath string) error { func copyFileToClipboard(filePath string) error {
req := models.Request{ if abs, err := filepath.Abs(filePath); err == nil {
filePath = abs
}
resp, ok := tryServerRequest(ipc.Request{
ID: 1, ID: 1,
Method: "clipboard.copyFile", Method: "clipboard.copyFile",
Params: map[string]any{"filePath": filePath}, Params: map[string]any{"filePath": filePath},
})
if ok && resp.Error == "" {
return nil
} }
resp, err := sendServerRequest(req) data, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return fmt.Errorf("server request: %w", err) return fmt.Errorf("read file: %w", err)
} }
if resp.Error != "" { return clipboard.CopyMulti(clipboard.FileOffers(filePath, filePath, data), false, false)
return fmt.Errorf("server error: %s", resp.Error)
}
return nil
} }
+4 -3
View File
@@ -5,8 +5,9 @@ import (
"os" "os"
"runtime/debug" "runtime/debug"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/duxlabs-dev/dux-shell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker" "github.com/duxlabs-dev/dux-shell/core/internal/colorpicker"
"github.com/duxlabs-dev/dux-shell/core/internal/site"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -26,7 +27,7 @@ var colorCmd = &cobra.Command{
This is the screen eyedropper CLI. To open the in-shell color modal, use: This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle dms ipc call color-picker toggle
See: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`, See: ` + site.Docs + "/dms/keybinds-ipc",
} }
var colorPickCmd = &cobra.Command{ var colorPickCmd = &cobra.Command{
+19 -17
View File
@@ -6,10 +6,12 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" "github.com/duxlabs-dev/dux-shell/core/internal/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server" "github.com/duxlabs-dev/dux-shell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed" "github.com/duxlabs-dev/dux-shell/core/internal/server/sysupdate"
"github.com/duxlabs-dev/dux-shell/core/internal/shellembed"
"github.com/duxlabs-dev/dux-shell/core/internal/site"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -27,7 +29,7 @@ var ipcCmd = &cobra.Command{
dms ipc call <target> <function> [args...] invoke a command dms ipc call <target> <function> [args...] invoke a command
dms ipc list list all targets and functions dms ipc list list all targets and functions
Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`, Full reference: ` + site.Docs + "/dms/keybinds-ipc",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
}, },
@@ -217,21 +219,17 @@ func runVersion(cmd *cobra.Command, args []string) {
// Git builds: dms (git) v0.6.2-XXXX // Git builds: dms (git) v0.6.2-XXXX
// Stable releases: dms v0.6.2 // Stable releases: dms v0.6.2
func formatVersion(version string) string { func formatVersion(version string) string {
// Arch/Debian/Ubuntu/OpenSUSE git format: 0.6.2+git2264.c5c5ce84 if n := sysupdate.GitBuildCount(version); n > 0 {
re := regexp.MustCompile(`^([\d.]+)\+git(\d+)\.`) // Fedora COPR (0.0.git.2267.d430cae9) carries no real base version.
if matches := re.FindStringSubmatch(version); matches != nil { base, _, ok := strings.Cut(version, "+git")
return fmt.Sprintf("dms (git) v%s-%s", matches[1], matches[2]) if !ok {
} base = getBaseVersion()
}
// Fedora COPR git format: 0.0.git.2267.d430cae9 return fmt.Sprintf("dms (git) v%s-%d", strings.TrimPrefix(base, "v"), n)
re = regexp.MustCompile(`^[\d.]+\.git\.(\d+)\.`)
if matches := re.FindStringSubmatch(version); matches != nil {
baseVersion := getBaseVersion()
return fmt.Sprintf("dms (git) v%s-%s", baseVersion, matches[1])
} }
// Stable release format: 0.6.2 // Stable release format: 0.6.2
re = regexp.MustCompile(`^([\d.]+)$`) re := regexp.MustCompile(`^([\d.]+)$`)
if matches := re.FindStringSubmatch(version); matches != nil { if matches := re.FindStringSubmatch(version); matches != nil {
return fmt.Sprintf("dms v%s", matches[1]) return fmt.Sprintf("dms v%s", matches[1])
} }
@@ -773,6 +771,7 @@ func getCommonCommands() []*cobra.Command {
registryCmd, registryCmd,
dank16Cmd, dank16Cmd,
brightnessCmd, brightnessCmd,
iccCmd,
dpmsCmd, dpmsCmd,
keybindsCmd, keybindsCmd,
greeterCmd, greeterCmd,
@@ -793,6 +792,9 @@ func getCommonCommands() []*cobra.Command {
blurCmd, blurCmd,
trashCmd, trashCmd,
systemCmd, systemCmd,
systemdCmd,
switchUserCmd, switchUserCmd,
switchCmd,
dmenuCmd,
}...) }...)
} }
+69 -69
View File
@@ -7,9 +7,10 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/luaconfig" "github.com/duxlabs-dev/dux-shell/core/internal/luaconfig"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/duxlabs-dev/dux-shell/core/internal/mangoconf"
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -35,6 +36,7 @@ var resolveIncludeCmd = &cobra.Command{
"layout.lua", "layout.lua",
"outputs.lua", "outputs.lua",
"cursor.lua", "cursor.lua",
"input.lua",
"windowrules.lua", "windowrules.lua",
"cursor.kdl", "cursor.kdl",
"layout.kdl", "layout.kdl",
@@ -42,6 +44,7 @@ var resolveIncludeCmd = &cobra.Command{
"binds.kdl", "binds.kdl",
"input.kdl", "input.kdl",
"cursor.conf", "cursor.conf",
"input.conf",
"layout.conf", "layout.conf",
"outputs.conf", "outputs.conf",
"binds.conf", "binds.conf",
@@ -52,8 +55,32 @@ var resolveIncludeCmd = &cobra.Command{
Run: runResolveInclude, Run: runResolveInclude,
} }
var mangoMigrateMain bool
var mangoMigrateCmd = &cobra.Command{
Use: "mango-migrate",
Short: "Respell Mango config keys for the installed Mango",
Long: "Detects whether the installed Mango uses legacy or snake_case config keys, rewrites the DMS fragments in mango/dms to match, and prints the dialect on the last line. --main also rewrites config.conf after a backup.",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
dialect := mangoconf.Detect()
changed, err := dialect.Migrate(mangoconf.Dir(), mangoMigrateMain)
for _, path := range changed {
fmt.Fprintf(os.Stderr, "migrated %s\n", path)
}
// Printed before any error: the shell needs the dialect even when a fragment failed.
fmt.Println(dialect)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
func init() { func init() {
configCmd.AddCommand(resolveIncludeCmd) configCmd.AddCommand(resolveIncludeCmd)
mangoMigrateCmd.Flags().BoolVar(&mangoMigrateMain, "main", false, "also rewrite config.conf (a backup is kept)")
configCmd.AddCommand(mangoMigrateCmd)
} }
type IncludeResult struct { type IncludeResult struct {
@@ -104,32 +131,21 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
return result, err return result, err
} }
targetRel := filepath.ToSlash(filepath.Join("dms", filename))
mainLua := filepath.Join(configDir, "hyprland.lua") mainLua := filepath.Join(configDir, "hyprland.lua")
if _, err := os.Stat(mainLua); err == nil { if _, err := os.Stat(mainLua); err == nil {
result.ConfigFormat = "lua" result.ConfigFormat = "lua"
result.ReadOnly = false result.Included = luaconfig.RequiresTarget(mainLua, targetAbs, make(map[string]bool))
processedLua := make(map[string]bool) return result, nil
if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) {
result.Included = true
return result, nil
}
} }
// 0.55/0.56 installs still on hyprland.conf: read-only until migrated.
mainConf := filepath.Join(configDir, "hyprland.conf") mainConf := filepath.Join(configDir, "hyprland.conf")
if _, err := os.Stat(mainConf); err == nil { if _, err := os.Stat(mainConf); err != nil {
if result.ConfigFormat == "" { return result, nil
result.ConfigFormat = "hyprlang"
result.ReadOnly = true
}
processed := make(map[string]bool)
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
result.Included = true
return result, nil
}
} }
result.ConfigFormat = "hyprlang"
result.ReadOnly = true
result.Included = hyprlandFindIncludeHyprlang(mainConf, targetAbs, make(map[string]bool))
return result, nil return result, nil
} }
@@ -150,14 +166,8 @@ func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]b
} }
baseDir := filepath.Dir(absPath) baseDir := filepath.Dir(absPath)
lines := strings.SplitSeq(string(data), "\n") for line := range strings.SplitSeq(string(data), "\n") {
for line := range lines {
trimmed := strings.TrimSpace(line) trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
}
if !strings.HasPrefix(trimmed, "source") { if !strings.HasPrefix(trimmed, "source") {
continue continue
} }
@@ -167,22 +177,12 @@ func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]b
continue continue
} }
sourcePath := strings.TrimSpace(parts[1]) fullPath, err := resolveSourcePath(baseDir, strings.TrimSpace(parts[1]))
if matchesTarget(sourcePath, target) {
return true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil { if err != nil {
continue continue
} }
if hyprlandFindIncludeHyprlang(expanded, target, processed) { if fullPath == target || hyprlandFindIncludeHyprlang(fullPath, target, processed) {
return true return true
} }
} }
@@ -205,8 +205,13 @@ func checkNiriInclude(filename string) (IncludeResult, error) {
return result, nil return result, nil
} }
targetAbs, err := filepath.Abs(targetPath)
if err != nil {
return result, err
}
processed := make(map[string]bool) processed := make(map[string]bool)
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed) result.Included = niriFindInclude(mainConfig, targetAbs, processed)
return result, nil return result, nil
} }
@@ -248,17 +253,12 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
continue continue
} }
includePath := trimmed[startQuote+1 : endQuote] fullPath, err := resolveSourcePath(baseDir, trimmed[startQuote+1:endQuote])
if matchesTarget(includePath, target) { if err != nil {
return true continue
} }
fullPath := includePath if fullPath == target || niriFindInclude(fullPath, target, processed) {
if !filepath.IsAbs(includePath) {
fullPath = filepath.Join(baseDir, includePath)
}
if niriFindInclude(fullPath, target, processed) {
return true return true
} }
} }
@@ -267,7 +267,7 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
} }
func checkMangoWCInclude(filename string) (IncludeResult, error) { func checkMangoWCInclude(filename string) (IncludeResult, error) {
configDir := filepath.Join(utils.XDGConfigHome(), "mango") configDir := mangoconf.Dir()
targetPath := filepath.Join(configDir, "dms", filename) targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{} result := IncludeResult{}
@@ -284,8 +284,13 @@ func checkMangoWCInclude(filename string) (IncludeResult, error) {
return result, nil return result, nil
} }
targetAbs, err := filepath.Abs(targetPath)
if err != nil {
return result, err
}
processed := make(map[string]bool) processed := make(map[string]bool)
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed) result.Included = mangowcFindInclude(mainConfig, targetAbs, processed)
return result, nil return result, nil
} }
@@ -323,22 +328,12 @@ func mangowcFindInclude(filePath, target string, processed map[string]bool) bool
continue continue
} }
sourcePath := strings.TrimSpace(parts[1]) fullPath, err := resolveSourcePath(baseDir, strings.TrimSpace(parts[1]))
if matchesTarget(sourcePath, target) {
return true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil { if err != nil {
continue continue
} }
if mangowcFindInclude(expanded, target, processed) { if fullPath == target || mangowcFindInclude(fullPath, target, processed) {
return true return true
} }
} }
@@ -346,8 +341,13 @@ func mangowcFindInclude(filePath, target string, processed map[string]bool) bool
return false return false
} }
func matchesTarget(path, target string) bool { func resolveSourcePath(baseDir, raw string) (string, error) {
path = strings.TrimPrefix(path, "./") expanded, err := utils.ExpandPath(raw)
target = strings.TrimPrefix(target, "./") if err != nil {
return path == target || strings.HasSuffix(path, "/"+target) return "", err
}
if !filepath.IsAbs(expanded) {
expanded = filepath.Join(baseDir, expanded)
}
return filepath.Abs(expanded)
} }
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func writeConfigFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestNiriFindIncludeNestedRelativeToIncludingFile(t *testing.T) {
dir := t.TempDir()
writeConfigFile(t, filepath.Join(dir, "config.kdl"), "include \"dms/dms.kdl\"\n")
writeConfigFile(t, filepath.Join(dir, "dms", "dms.kdl"), "include \"binds.kdl\"\ninclude \"./outputs.kdl\"\n")
for _, name := range []string{"binds.kdl", "outputs.kdl"} {
target := filepath.Join(dir, "dms", name)
if !niriFindInclude(filepath.Join(dir, "config.kdl"), target, map[string]bool{}) {
t.Errorf("%s included through dms/dms.kdl not detected", name)
}
}
if niriFindInclude(filepath.Join(dir, "config.kdl"), filepath.Join(dir, "dms", "layout.kdl"), map[string]bool{}) {
t.Error("layout.kdl reported included without an include line")
}
}
func TestMangowcFindIncludeNestedRelativeToIncludingFile(t *testing.T) {
dir := t.TempDir()
writeConfigFile(t, filepath.Join(dir, "config.conf"), "source=./dms/dms.conf\n")
writeConfigFile(t, filepath.Join(dir, "dms", "dms.conf"), "source = binds.conf\n")
if !mangowcFindInclude(filepath.Join(dir, "config.conf"), filepath.Join(dir, "dms", "binds.conf"), map[string]bool{}) {
t.Error("binds.conf sourced through dms/dms.conf not detected")
}
}
func TestCheckHyprlandIncludeFormat(t *testing.T) {
tests := []struct {
name string
files map[string]string
wantFormat string
wantReadOnly bool
wantIncluded bool
}{
{"hyprlang sourced", map[string]string{"hyprland.conf": "source = ./dms/colors.conf\n"}, "hyprlang", true, true},
{"hyprlang not sourced", map[string]string{"hyprland.conf": "bind = SUPER, Q, killactive\n"}, "hyprlang", true, false},
{"lua wins over leftover conf", map[string]string{"hyprland.conf": "source = ./dms/colors.conf\n", "hyprland.lua": "\n"}, "lua", false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
xdg := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", xdg)
for name, content := range tt.files {
writeConfigFile(t, filepath.Join(xdg, "hypr", name), content)
}
got, err := checkHyprlandInclude("colors.conf")
if err != nil {
t.Fatal(err)
}
if got.ConfigFormat != tt.wantFormat || got.ReadOnly != tt.wantReadOnly || got.Included != tt.wantIncluded {
t.Fatalf("got %+v, want format %q readOnly %v included %v", got, tt.wantFormat, tt.wantReadOnly, tt.wantIncluded)
}
})
}
}
+14 -7
View File
@@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16" "github.com/duxlabs-dev/dux-shell/core/internal/dank16"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -28,9 +28,13 @@ func init() {
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format") dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
dank16Cmd.Flags().String("background", "", "Custom background color") dank16Cmd.Flags().String("background", "", "Custom background color")
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag") dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
dank16Cmd.Flags().String("profile", "default", "Palette: default, low, medium or high")
dank16Cmd.Flags().Bool("variants", false, "Output all variants (dark/light/default) in JSON") dank16Cmd.Flags().Bool("variants", false, "Output all variants (dark/light/default) in JSON")
dank16Cmd.Flags().String("primary-dark", "", "Primary color for dark mode (use with --variants)") dank16Cmd.Flags().String("primary-dark", "", "Primary color for dark mode (use with --variants)")
dank16Cmd.Flags().String("primary-light", "", "Primary color for light mode (use with --variants)") dank16Cmd.Flags().String("primary-light", "", "Primary color for light mode (use with --variants)")
_ = dank16Cmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"default", "low", "medium", "high"}, cobra.ShellCompDirectiveNoFileComp
})
_ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { _ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp
}) })
@@ -47,6 +51,7 @@ func runDank16(cmd *cobra.Command, args []string) {
isWezterm, _ := cmd.Flags().GetBool("wezterm") isWezterm, _ := cmd.Flags().GetBool("wezterm")
background, _ := cmd.Flags().GetString("background") background, _ := cmd.Flags().GetString("background")
contrastAlgo, _ := cmd.Flags().GetString("contrast") contrastAlgo, _ := cmd.Flags().GetString("contrast")
profile, _ := cmd.Flags().GetString("profile")
useVariants, _ := cmd.Flags().GetBool("variants") useVariants, _ := cmd.Flags().GetBool("variants")
primaryDark, _ := cmd.Flags().GetString("primary-dark") primaryDark, _ := cmd.Flags().GetString("primary-dark")
primaryLight, _ := cmd.Flags().GetString("primary-light") primaryLight, _ := cmd.Flags().GetString("primary-light")
@@ -79,11 +84,12 @@ func runDank16(cmd *cobra.Command, args []string) {
primaryLight = primaryColor primaryLight = primaryColor
} }
variantOpts := dank16.VariantOptions{ variantOpts := dank16.VariantOptions{
PrimaryDark: primaryDark, PrimaryDark: primaryDark,
PrimaryLight: primaryLight, PrimaryLight: primaryLight,
Background: background, BackgroundDark: background,
UseDPS: contrastAlgo == "dps", UseDPS: contrastAlgo == "dps",
IsLightMode: isLight, IsLightMode: isLight,
Profile: profile,
} }
variantColors := dank16.GenerateVariantPalette(variantOpts) variantColors := dank16.GenerateVariantPalette(variantOpts)
fmt.Print(dank16.GenerateVariantJSON(variantColors)) fmt.Print(dank16.GenerateVariantJSON(variantColors))
@@ -102,6 +108,7 @@ func runDank16(cmd *cobra.Command, args []string) {
IsLight: isLight, IsLight: isLight,
Background: background, Background: background,
UseDPS: contrastAlgo == "dps", UseDPS: contrastAlgo == "dps",
Profile: profile,
} }
colors := dank16.GeneratePalette(primaryColor, opts) colors := dank16.GeneratePalette(primaryColor, opts)
+643
View File
@@ -0,0 +1,643 @@
package main
import (
"bufio"
"bytes"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/duxlabs-dev/dux-shell/core/internal/dmenuipc"
"github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/duxlabs-dev/dux-shell/core/internal/qsipc"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const dmenuConnectTimeout = 5 * time.Second
var (
dmenuPrompt string
dmenuLines int
dmenuPlaceholder string
dmenuPromptOnly bool
dmenuSep string
dmenuActiveSpec string
dmenuUrgentSpec string
dmenuOnlyMatch bool
dmenuNoCustom bool
dmenuSelect string
dmenuMesg string
dmenuInput string
dmenuPassword bool
dmenuMarkupRows bool
dmenuSync bool
dmenuNoRunIfEmpty bool
dmenuFormat string
dmenuMultiSelect bool
dmenuDump bool
dmenuKeybindSpecs []string
dmenuPrintInfo bool
dmenuView string
dmenuSize string
dmenuIcon string
dmenuCaseInsens bool
dmenuDisableHist bool
)
var dmenuCmd = &cobra.Command{
Use: "dmenu",
Short: "dmenu-compatible picker backed by the DMS app launcher",
Long: `Read newline-separated items from stdin, show them in the DMS launcher,
and print the selected item to stdout
Examples:
printf '%s\n' opt1 opt2 opt3 | dms dmenu -p "Pick one:"
dms dmenu --prompt-only -p "Rename to:"
Matching is always case-insensitive; -i/--case-insensitive is accepted for
dmenu script compatibility but has no effect.
Most, but not all, rofi flags are implemented.
Long-form flags also accept rofi's single-dash form
(e.g. -only-match as well as --only-match).
Exit codes: 0 on accept, 1 if the user cancelled (or stdin was empty),
2 if the shell was unreachable.`,
Run: runDmenu,
}
func init() {
dmenuCmd.Flags().BoolVarP(&dmenuCaseInsens, "case-insensitive", "i", false, "No effect: matching is always case-insensitive. Accepted for dmenu/rofi script compatibility")
dmenuCmd.Flags().StringVarP(&dmenuPrompt, "prompt", "p", "dmenu", "Prompt label")
dmenuCmd.Flags().IntVarP(&dmenuLines, "lines", "l", 0, "Max visible result rows (0 = shell default)")
dmenuCmd.Flags().StringVar(&dmenuPlaceholder, "placeholder", "", "Input placeholder text")
dmenuCmd.Flags().BoolVar(&dmenuPromptOnly, "prompt-only", false, "No piped item list — pure free-text prompt")
dmenuCmd.Flags().StringVar(&dmenuSep, "sep", "\n", "Input item separator")
dmenuCmd.Flags().StringVarP(&dmenuActiveSpec, "active", "a", "", "Mark rows active by index (e.g. \"1,3,7:11,-3:\")")
dmenuCmd.Flags().StringVarP(&dmenuUrgentSpec, "urgent", "u", "", "Mark rows urgent by index (same spec syntax as --active)")
dmenuCmd.Flags().BoolVar(&dmenuOnlyMatch, "only-match", false, "Reject free text; only a listed item may be returned; also blocks Escape/click-outside cancel, forcing a real pick")
dmenuCmd.Flags().BoolVar(&dmenuNoCustom, "no-custom", false, "Reject free text; only a listed item may be returned (unlike --only-match, Escape/cancel still works normally)")
dmenuCmd.Flags().StringVar(&dmenuSelect, "select", "", "Pre-select the first row matching this string (or, with --dump, filter to matching rows)")
dmenuCmd.Flags().StringVar(&dmenuMesg, "mesg", "", "Extra message line shown alongside the list")
dmenuCmd.Flags().StringVar(&dmenuInput, "input", "", "Read items from this file instead of stdin")
dmenuCmd.Flags().BoolVar(&dmenuPassword, "password", false, "Mask typed input (cosmetic only, not a security boundary)")
dmenuCmd.Flags().BoolVar(&dmenuMarkupRows, "markup-rows", false, "Render each row's text as rich markup")
dmenuCmd.Flags().BoolVar(&dmenuSync, "sync", false, "Read all input to EOF before showing the picker (default: stream as items arrive)")
dmenuCmd.Flags().BoolVar(&dmenuNoRunIfEmpty, "no-run-if-empty", false, "If stdin is empty, exit 1 immediately with no UI shown")
dmenuCmd.Flags().StringVar(&dmenuFormat, "format", "s", "What to print on accept: s=text (default), i=0-based index, d=1-based index, q=shell-quoted, p=markup-stripped, f=typed filter text, F=quoted filter text")
dmenuCmd.Flags().BoolVar(&dmenuMultiSelect, "multi-select", false, "Allow selecting multiple rows; each is printed on its own line on accept")
dmenuCmd.Flags().BoolVar(&dmenuDump, "dump", false, "Apply --select against the item list and print the result immediately; no UI is ever shown")
dmenuCmd.Flags().StringArrayVar(&dmenuKeybindSpecs, "keybind", nil, "Bind a custom accept key (repeatable): N=keyspec, e.g. --keybind 1=ctrl+e (N is 1-19); exits 9+N (10-28) instead of 0 when accepted that way")
dmenuCmd.Flags().BoolVar(&dmenuPrintInfo, "print-info", false, "Also print an accepted row's info metadata (the per-row \\0info\\x1f... field, see the per-row metadata protocol) on a second line")
dmenuCmd.Flags().BoolVar(&dmenuDisableHist, "disable-history", false, "Don't add this session's search query to the launcher's persistent history (default: recorded, like other launcher modes)")
dmenuCmd.Flags().StringVar(&dmenuView, "view", "", "Override the result view mode for this invocation: list, grid, or tile (default: list)")
dmenuCmd.Flags().StringVar(&dmenuSize, "size", "", "Override the popup size preset for this invocation: 1-4, matching the Appearance settings size buttons (default: the shell's own launcher size setting)")
dmenuCmd.Flags().StringVar(&dmenuIcon, "icon", "", "Show a Material Symbols icon next to the prompt badge (default: no icon)")
}
var dmenuLongFlagToken = regexp.MustCompile(`^-([a-zA-Z][a-zA-Z0-9-]+)(=.*)?$`)
func dmenuAliasableLongFlags() map[string]bool {
set := map[string]bool{}
dmenuCmd.Flags().VisitAll(func(f *pflag.Flag) {
if len(f.Name) > 1 {
set[f.Name] = true
}
})
return set
}
func normalizeDmenuArgv(args []string) []string {
if len(args) < 2 || args[1] != "dmenu" {
return args
}
aliasable := dmenuAliasableLongFlags()
out := make([]string, len(args))
copy(out, args)
for i := 2; i < len(out); i++ {
tok := out[i]
if tok == "--" {
break
}
m := dmenuLongFlagToken.FindStringSubmatch(tok)
if m == nil {
continue
}
name, valueSuffix := m[1], m[2]
if aliasable[name] {
out[i] = "--" + name + valueSuffix
}
}
return out
}
func dmenuSplitFunc(sep string) bufio.SplitFunc {
if sep == "\n" {
return bufio.ScanLines
}
sepBytes := []byte(sep)
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, sepBytes); i >= 0 {
return i + len(sepBytes), data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
}
func parseIndexSpec(spec string) ([]dmenuipc.Range, error) {
spec = strings.TrimSpace(spec)
if spec == "" {
return nil, nil
}
var ranges []dmenuipc.Range
for _, tok := range strings.Split(spec, ",") {
tok = strings.TrimSpace(tok)
if tok == "" {
continue
}
if idx := strings.Index(tok, ":"); idx >= 0 {
startStr, endStr := strings.TrimSpace(tok[:idx]), strings.TrimSpace(tok[idx+1:])
start, end := 0, -1
if startStr != "" {
n, err := strconv.Atoi(startStr)
if err != nil {
return nil, fmt.Errorf("invalid range start %q in spec %q", startStr, spec)
}
start = n
}
if endStr != "" {
n, err := strconv.Atoi(endStr)
if err != nil {
return nil, fmt.Errorf("invalid range end %q in spec %q", endStr, spec)
}
end = n
}
ranges = append(ranges, dmenuipc.Range{Start: start, End: end})
continue
}
n, err := strconv.Atoi(tok)
if err != nil {
return nil, fmt.Errorf("invalid index %q in spec %q", tok, spec)
}
ranges = append(ranges, dmenuipc.Range{Start: n, End: n + 1})
}
return ranges, nil
}
func parseDmenuKeybinds(specs []string) ([]dmenuipc.Keybind, error) {
var out []dmenuipc.Keybind
seen := map[int]bool{}
for _, s := range specs {
eq := strings.IndexByte(s, '=')
if eq <= 0 {
return nil, fmt.Errorf("invalid --keybind %q (expected N=keyspec, e.g. 1=ctrl+e)", s)
}
n, err := strconv.Atoi(strings.TrimSpace(s[:eq]))
if err != nil || n < 1 || n > 19 {
return nil, fmt.Errorf("invalid --keybind index in %q (expected 1-19)", s)
}
key := strings.TrimSpace(s[eq+1:])
if key == "" {
return nil, fmt.Errorf("invalid --keybind %q: empty key spec", s)
}
if seen[n] {
return nil, fmt.Errorf("duplicate --keybind index %d", n)
}
seen[n] = true
out = append(out, dmenuipc.Keybind{N: n, Key: key})
}
return out, nil
}
func dmenuParseBool(val string) bool {
switch strings.ToLower(strings.TrimSpace(val)) {
case "", "0", "false", "no":
return false
default:
return true
}
}
func parseDmenuRow(line string) (text string, meta *dmenuipc.ItemMeta) {
idx := strings.IndexByte(line, 0)
if idx < 0 {
return line, nil
}
text = line[:idx]
fields := strings.Split(line[idx+1:], "\x1f")
m := dmenuipc.ItemMeta{}
any := false
for i := 0; i+1 < len(fields); i += 2 {
key, val := fields[i], fields[i+1]
switch key {
case "icon":
m.Icon = val
case "display":
m.Display = val
case "meta":
m.Meta = val
case "info":
m.Info = val
case "nonselectable":
m.NonSelectable = dmenuParseBool(val)
case "urgent":
m.Urgent = dmenuParseBool(val)
case "active":
m.Active = dmenuParseBool(val)
case "permanent":
m.Permanent = dmenuParseBool(val)
case "header":
m.Header = dmenuParseBool(val)
default:
continue
}
any = true
}
if !any {
return text, nil
}
return text, &m
}
func validateDmenuFormat(format string) error {
switch format {
case "", "s", "i", "d", "q", "p", "f", "F":
return nil
default:
return fmt.Errorf("invalid --format %q (expected one of s,i,d,q,p,f,F)", format)
}
}
func validateDmenuView(view string) error {
switch view {
case "", "list", "grid", "tile":
return nil
default:
return fmt.Errorf("invalid --view %q (expected one of list,grid,tile)", view)
}
}
func dmenuSizePreset(size string) (string, error) {
switch size {
case "":
return "", nil
case "1":
return "micro", nil
case "2":
return "compact", nil
case "3":
return "medium", nil
case "4":
return "large", nil
default:
return "", fmt.Errorf("invalid --size %q (expected one of 1,2,3,4)", size)
}
}
var dmenuMarkupTagRe = regexp.MustCompile(`<[^>]*>`)
func dmenuShellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}
func dmenuFormatOutput(format string, index int, text, filterText string) (string, error) {
switch format {
case "", "s":
return text, nil
case "i":
return strconv.Itoa(index), nil
case "d":
if index < 0 {
return strconv.Itoa(index), nil
}
return strconv.Itoa(index + 1), nil
case "q":
return dmenuShellQuote(text), nil
case "p":
return dmenuMarkupTagRe.ReplaceAllString(text, ""), nil
case "f":
return filterText, nil
case "F":
return dmenuShellQuote(filterText), nil
default:
return "", fmt.Errorf("invalid --format %q (expected one of s,i,d,q,p,f,F)", format)
}
}
func dmenuOpenInput() *os.File {
if dmenuPromptOnly {
return nil
}
if dmenuInput != "" {
f, err := os.Open(dmenuInput)
if err != nil {
log.Fatalf("Error opening --input file: %v", err)
}
return f
}
return os.Stdin
}
type dmenuRow struct {
Text string
Meta *dmenuipc.ItemMeta
}
func dmenuReadAll(input *os.File) []dmenuRow {
if input == nil {
return nil
}
scanner := bufio.NewScanner(input)
scanner.Buffer(make([]byte, 64*1024), 1<<20)
scanner.Split(dmenuSplitFunc(dmenuSep))
var rows []dmenuRow
for scanner.Scan() {
text, meta := parseDmenuRow(scanner.Text())
rows = append(rows, dmenuRow{Text: text, Meta: meta})
}
if err := scanner.Err(); err != nil {
log.Fatalf("Error reading items: %v", err)
}
return rows
}
func dmenuDumpLines(rows []dmenuRow, select_, format string, printInfo bool) ([]string, error) {
var lower string
filtering := select_ != ""
if filtering {
lower = strings.ToLower(select_)
}
var lines []string
for i, row := range rows {
if row.Meta != nil && (row.Meta.NonSelectable || row.Meta.Header) {
continue
}
if filtering && !strings.Contains(strings.ToLower(row.Text), lower) {
continue
}
out, err := dmenuFormatOutput(format, i, row.Text, select_)
if err != nil {
return nil, err
}
lines = append(lines, out)
if printInfo && row.Meta != nil && row.Meta.Info != "" {
lines = append(lines, row.Meta.Info)
}
}
return lines, nil
}
func runDmenuDump(rows []dmenuRow, format string) {
lines, err := dmenuDumpLines(rows, dmenuSelect, format, dmenuPrintInfo)
if err != nil {
log.Fatalf("Error formatting output: %v", err)
}
for _, l := range lines {
fmt.Println(l)
}
os.Exit(0)
}
func runDmenu(cmd *cobra.Command, args []string) {
if cmd.Flags().Changed("case-insensitive") {
fmt.Fprintln(os.Stderr, "dms dmenu: warning: -i/--case-insensitive has no effect (matching is always case-insensitive)")
}
os.Exit(dmenuSession())
}
func dmenuSession() int {
format := dmenuFormat
if format == "" {
format = "s"
}
if err := validateDmenuFormat(format); err != nil {
log.Fatalf("Error in --format: %v", err)
}
if dmenuDump {
input := dmenuOpenInput()
if input != nil && input != os.Stdin {
defer input.Close()
}
runDmenuDump(dmenuReadAll(input), format)
return 0
}
activeRanges, err := parseIndexSpec(dmenuActiveSpec)
if err != nil {
log.Fatalf("Error parsing --active: %v", err)
}
urgentRanges, err := parseIndexSpec(dmenuUrgentSpec)
if err != nil {
log.Fatalf("Error parsing --urgent: %v", err)
}
keybinds, err := parseDmenuKeybinds(dmenuKeybindSpecs)
if err != nil {
log.Fatalf("Error parsing --keybind: %v", err)
}
if err := validateDmenuView(dmenuView); err != nil {
log.Fatalf("Error in --view: %v", err)
}
dmenuSizePresetName, err := dmenuSizePreset(dmenuSize)
if err != nil {
log.Fatalf("Error in --size: %v", err)
}
input := dmenuOpenInput()
if input != nil && input != os.Stdin {
defer input.Close()
}
var scanner *bufio.Scanner
if input != nil {
scanner = bufio.NewScanner(input)
scanner.Buffer(make([]byte, 64*1024), 1<<20)
scanner.Split(dmenuSplitFunc(dmenuSep))
}
var bufferedItems []string
streaming := scanner != nil && !dmenuSync
if scanner != nil && !streaming {
for scanner.Scan() {
bufferedItems = append(bufferedItems, scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Errorf("Error reading items: %v", err)
return 1
}
}
var pendingFirstItem string
havePendingFirstItem := false
if dmenuNoRunIfEmpty {
if streaming {
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
log.Errorf("Error reading items: %v", err)
return 1
}
return 1
}
pendingFirstItem = scanner.Text()
havePendingFirstItem = true
} else if len(bufferedItems) == 0 {
return 1
}
}
release, err := dmenuipc.AcquireQueueSlot(func() {
fmt.Fprintln(os.Stderr, "dms dmenu: waiting for another dmenu session to finish...")
})
if err != nil {
log.Fatalf("Error acquiring dmenu queue slot: %v", err)
}
defer release()
sess, err := dmenuipc.Listen()
if err != nil {
log.Fatalf("Error creating dmenu socket: %v", err)
}
defer sess.Close()
pid, ok := shellApp.SessionPID()
if !ok {
log.Errorf("DMS shell is not running")
return 1
}
mode := "dmenu:" + sess.Path()
if _, _, err := qsipc.Call(qsipc.SocketPathForPID(pid), "launcher", "openWith", []string{mode}); err != nil {
log.Errorf("Error opening launcher: %v", err)
return 1
}
conn, err := sess.Accept(dmenuConnectTimeout)
if err != nil {
log.Debugf("dmenu: shell never connected: %v", err)
return 2
}
defer conn.Close()
header := dmenuipc.Header{
Prompt: dmenuPrompt,
Placeholder: dmenuPlaceholder,
Lines: dmenuLines,
PromptOnly: dmenuPromptOnly,
Select: dmenuSelect,
Mesg: dmenuMesg,
Password: dmenuPassword,
MarkupRows: dmenuMarkupRows,
OnlyMatch: dmenuOnlyMatch,
NoCustom: dmenuNoCustom,
MultiSelect: dmenuMultiSelect,
Active: activeRanges,
Urgent: urgentRanges,
Keybinds: keybinds,
View: dmenuView,
Size: dmenuSizePresetName,
Icon: dmenuIcon,
DisableHistory: dmenuDisableHist,
}
if err := conn.SendHeader(header); err != nil {
log.Debugf("dmenu: failed to send header: %v", err)
return 2
}
var sendErr error
sendItem := func(line string) {
text, meta := parseDmenuRow(line)
if err := conn.SendItem(text, meta); err != nil {
log.Debugf("dmenu: failed to send item: %v", err)
sendErr = err
}
}
if streaming {
if havePendingFirstItem && sendErr == nil {
sendItem(pendingFirstItem)
}
for sendErr == nil && scanner.Scan() {
sendItem(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Debugf("dmenu: error reading items mid-stream: %v", err)
return 2
}
} else {
for _, item := range bufferedItems {
if sendErr != nil {
break
}
sendItem(item)
}
}
if sendErr != nil {
return 2
}
if err := conn.SendEnd(); err != nil {
log.Debugf("dmenu: failed to send end marker: %v", err)
return 2
}
sel, selected, err := conn.ReadSelection()
if err != nil {
log.Debugf("dmenu: failed to read result: %v", err)
return 2
}
if !selected {
return 1
}
switch sel.Kind {
case "multi":
for _, item := range sel.Items {
out, err := dmenuFormatOutput(format, item.Index, item.Text, sel.FilterText)
if err != nil {
log.Errorf("Error formatting output: %v", err)
return 1
}
fmt.Println(out)
if dmenuPrintInfo && item.Info != "" {
fmt.Println(item.Info)
}
}
case "freetext":
out, err := dmenuFormatOutput(format, -1, sel.Text, sel.FilterText)
if err != nil {
log.Errorf("Error formatting output: %v", err)
return 1
}
fmt.Println(out)
default: // "row"
out, err := dmenuFormatOutput(format, sel.Index, sel.Text, sel.FilterText)
if err != nil {
log.Errorf("Error formatting output: %v", err)
return 1
}
fmt.Println(out)
if dmenuPrintInfo && sel.Info != "" {
fmt.Println(sel.Info)
}
}
return dmenuKeybindExitCode(sel.KeybindN)
}
func dmenuKeybindExitCode(keybindN int) int {
if keybindN <= 0 {
return 0
}
return 9 + keybindN
}
+350
View File
@@ -0,0 +1,350 @@
package main
import (
"reflect"
"testing"
"github.com/duxlabs-dev/dux-shell/core/internal/dmenuipc"
)
func TestParseIndexSpec(t *testing.T) {
for _, tc := range []struct {
spec string
want []dmenuipc.Range
wantErr bool
}{
{"", nil, false},
{"1,3", []dmenuipc.Range{{Start: 1, End: 2}, {Start: 3, End: 4}}, false},
{"7:11", []dmenuipc.Range{{Start: 7, End: 11}}, false},
{"-3:", []dmenuipc.Range{{Start: -3, End: -1}}, false},
{":5", []dmenuipc.Range{{Start: 0, End: 5}}, false},
{"1, 3 , 7:11", []dmenuipc.Range{{Start: 1, End: 2}, {Start: 3, End: 4}, {Start: 7, End: 11}}, false},
{"1,,3", []dmenuipc.Range{{Start: 1, End: 2}, {Start: 3, End: 4}}, false},
{"x", nil, true},
{"1:y", nil, true},
{"x:1", nil, true},
} {
got, err := parseIndexSpec(tc.spec)
if tc.wantErr {
if err == nil {
t.Errorf("parseIndexSpec(%q): expected error, got %v", tc.spec, got)
}
continue
}
if err != nil {
t.Errorf("parseIndexSpec(%q): unexpected error: %v", tc.spec, err)
continue
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("parseIndexSpec(%q) = %+v, want %+v", tc.spec, got, tc.want)
}
}
}
func TestParseDmenuKeybinds(t *testing.T) {
for _, tc := range []struct {
name string
specs []string
want []dmenuipc.Keybind
wantErr bool
}{
{"empty", nil, nil, false},
{"multiple", []string{"1=ctrl+e", "2=f5"}, []dmenuipc.Keybind{{N: 1, Key: "ctrl+e"}, {N: 2, Key: "f5"}}, false},
{"missing equals", []string{"bad"}, nil, true},
{"zero index", []string{"0=x"}, nil, true},
{"out of range index", []string{"20=x"}, nil, true},
{"empty key spec", []string{"1="}, nil, true},
{"duplicate index", []string{"1=ctrl+e", "1=f5"}, nil, true},
{"non numeric index", []string{"a=x"}, nil, true},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := parseDmenuKeybinds(tc.specs)
if tc.wantErr {
if err == nil {
t.Fatalf("parseDmenuKeybinds(%v): expected error, got %v", tc.specs, got)
}
return
}
if err != nil {
t.Fatalf("parseDmenuKeybinds(%v): unexpected error: %v", tc.specs, err)
}
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("parseDmenuKeybinds(%v) = %+v, want %+v", tc.specs, got, tc.want)
}
})
}
}
func TestDmenuParseBool(t *testing.T) {
for _, tc := range []struct {
val string
want bool
}{
{"", false},
{"0", false},
{"false", false},
{"False", false},
{"no", false},
{"anything-else", true},
} {
if got := dmenuParseBool(tc.val); got != tc.want {
t.Errorf("dmenuParseBool(%q) = %v, want %v", tc.val, got, tc.want)
}
}
}
func TestParseDmenuRow(t *testing.T) {
t.Run("plain text, no metadata", func(t *testing.T) {
text, meta := parseDmenuRow("apple")
if text != "apple" || meta != nil {
t.Fatalf("parseDmenuRow(apple) = %q, %+v, want apple, nil", text, meta)
}
})
t.Run("single field", func(t *testing.T) {
text, meta := parseDmenuRow("Firefox\x00icon\x1ffirefox")
if text != "Firefox" {
t.Fatalf("text = %q, want Firefox", text)
}
if meta == nil || meta.Icon != "firefox" {
t.Fatalf("meta = %+v, want Icon=firefox", meta)
}
})
t.Run("all recognized fields", func(t *testing.T) {
line := "Firefox\x00icon\x1ffirefox\x1fdisplay\x1fMozilla Firefox\x1fmeta\x1fbrowser\x1finfo\x1ffirefox.desktop" +
"\x1fnonselectable\x1ftrue\x1furgent\x1ftrue\x1factive\x1ftrue\x1fpermanent\x1ftrue\x1fheader\x1ftrue"
text, meta := parseDmenuRow(line)
want := dmenuipc.ItemMeta{
Icon: "firefox", Display: "Mozilla Firefox", Meta: "browser", Info: "firefox.desktop",
NonSelectable: true, Urgent: true, Active: true, Permanent: true, Header: true,
}
if text != "Firefox" {
t.Fatalf("text = %q, want Firefox", text)
}
if meta == nil || *meta != want {
t.Fatalf("meta = %+v, want %+v", meta, want)
}
})
t.Run("unknown field ignored but still produces meta", func(t *testing.T) {
text, meta := parseDmenuRow("apple\x00bogus\x1fvalue")
if text != "apple" {
t.Fatalf("text = %q, want apple", text)
}
if meta != nil {
t.Fatalf("meta = %+v, want nil (no recognized fields)", meta)
}
})
t.Run("dangling unpaired field ignored", func(t *testing.T) {
text, meta := parseDmenuRow("apple\x00icon\x1ffirefox\x1fdangling")
if text != "apple" {
t.Fatalf("text = %q, want apple", text)
}
if meta == nil || meta.Icon != "firefox" {
t.Fatalf("meta = %+v, want Icon=firefox", meta)
}
})
}
func TestValidateDmenuFormat(t *testing.T) {
for _, f := range []string{"", "s", "i", "d", "q", "p", "f", "F"} {
if err := validateDmenuFormat(f); err != nil {
t.Errorf("validateDmenuFormat(%q): unexpected error: %v", f, err)
}
}
for _, f := range []string{"x", "S", "ff"} {
if err := validateDmenuFormat(f); err == nil {
t.Errorf("validateDmenuFormat(%q): expected error, got nil", f)
}
}
}
func TestValidateDmenuView(t *testing.T) {
for _, v := range []string{"", "list", "grid", "tile"} {
if err := validateDmenuView(v); err != nil {
t.Errorf("validateDmenuView(%q): unexpected error: %v", v, err)
}
}
for _, v := range []string{"bogus", "List"} {
if err := validateDmenuView(v); err == nil {
t.Errorf("validateDmenuView(%q): expected error, got nil", v)
}
}
}
func TestDmenuShellQuote(t *testing.T) {
for _, tc := range []struct {
in string
want string
}{
{"banana", "'banana'"},
{"it's", `'it'\''s'`},
{"", "''"},
} {
if got := dmenuShellQuote(tc.in); got != tc.want {
t.Errorf("dmenuShellQuote(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
func TestDmenuFormatOutput(t *testing.T) {
for _, tc := range []struct {
format string
index int
text string
filterText string
want string
wantErr bool
}{
{"", 1, "banana", "ban", "banana", false},
{"s", 1, "banana", "ban", "banana", false},
{"i", 1, "banana", "ban", "1", false},
{"d", 1, "banana", "ban", "2", false},
{"d", -1, "banana", "xyz", "-1", false},
{"q", 0, "it's", "", "'it'\\''s'", false},
{"p", 0, "<b>Bold</b> item", "", "Bold item", false},
{"f", 0, "banana", "ban", "ban", false},
{"F", 0, "banana", "it's", "'it'\\''s'", false},
{"bogus", 0, "banana", "", "", true},
} {
got, err := dmenuFormatOutput(tc.format, tc.index, tc.text, tc.filterText)
if tc.wantErr {
if err == nil {
t.Errorf("dmenuFormatOutput(%q, ...): expected error, got %q", tc.format, got)
}
continue
}
if err != nil {
t.Errorf("dmenuFormatOutput(%q, ...): unexpected error: %v", tc.format, err)
continue
}
if got != tc.want {
t.Errorf("dmenuFormatOutput(%q, %d, %q, %q) = %q, want %q", tc.format, tc.index, tc.text, tc.filterText, got, tc.want)
}
}
}
func TestDmenuDumpLines(t *testing.T) {
rows := []dmenuRow{
{Text: "Header", Meta: &dmenuipc.ItemMeta{Header: true}},
{Text: "Firefox", Meta: &dmenuipc.ItemMeta{Info: "firefox.desktop"}},
{Text: "apple"},
{Text: "skip", Meta: &dmenuipc.ItemMeta{NonSelectable: true}},
{Text: "banana"},
}
t.Run("no filter, default format", func(t *testing.T) {
got, err := dmenuDumpLines(rows, "", "s", false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"Firefox", "apple", "banana"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
})
t.Run("with select filter", func(t *testing.T) {
got, err := dmenuDumpLines(rows, "an", "s", false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"banana"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
})
t.Run("print-info appends info lines", func(t *testing.T) {
got, err := dmenuDumpLines(rows, "", "s", true)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"Firefox", "firefox.desktop", "apple", "banana"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
})
t.Run("invalid format propagates error", func(t *testing.T) {
if _, err := dmenuDumpLines(rows, "", "bogus", false); err == nil {
t.Fatal("expected error, got nil")
}
})
}
func TestDmenuKeybindExitCode(t *testing.T) {
for _, tc := range []struct {
n int
want int
}{
{0, 0},
{1, 10},
{19, 28},
} {
if got := dmenuKeybindExitCode(tc.n); got != tc.want {
t.Errorf("dmenuKeybindExitCode(%d) = %d, want %d", tc.n, got, tc.want)
}
}
}
func TestNormalizeDmenuArgv(t *testing.T) {
for _, tc := range []struct {
name string
args []string
want []string
}{
{
"not the dmenu subcommand: untouched",
[]string{"dms", "ipc", "-password"},
[]string{"dms", "ipc", "-password"},
},
{
"rofi-style single-dash long flags get a second dash",
[]string{"dms", "dmenu", "-only-match", "-password", "-sep", ","},
[]string{"dms", "dmenu", "--only-match", "--password", "--sep", ","},
},
{
"= form is preserved",
[]string{"dms", "dmenu", "-format=i"},
[]string{"dms", "dmenu", "--format=i"},
},
{
"already-double-dash flags are untouched",
[]string{"dms", "dmenu", "--password", "-p", "hi"},
[]string{"dms", "dmenu", "--password", "-p", "hi"},
},
{
"single-char shorthands are untouched",
[]string{"dms", "dmenu", "-p", "hi", "-l", "5", "-i"},
[]string{"dms", "dmenu", "-p", "hi", "-l", "5", "-i"},
},
{
"negative --active/--urgent spec values are not mistaken for flag names",
[]string{"dms", "dmenu", "--active", "-3:"},
[]string{"dms", "dmenu", "--active", "-3:"},
},
{
"DMS-native long flags also get the single-dash alias",
[]string{"dms", "dmenu", "-icon", "star"},
[]string{"dms", "dmenu", "--icon", "star"},
},
{
"rofi's -a/-u abbreviations are real shorthands, untouched by the rewrite",
[]string{"dms", "dmenu", "-a", "1,3", "-u", "2"},
[]string{"dms", "dmenu", "-a", "1,3", "-u", "2"},
},
{
"stops rewriting after a bare --",
[]string{"dms", "dmenu", "--", "-password"},
[]string{"dms", "dmenu", "--", "-password"},
},
} {
got := normalizeDmenuArgv(tc.args)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("%s: normalizeDmenuArgv(%v) = %v, want %v", tc.name, tc.args, got, tc.want)
}
}
}
+367 -140
View File
@@ -1,10 +1,11 @@
package main package main
import ( import (
"bytes"
"debug/elf"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"maps"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -13,17 +14,20 @@ import (
"slices" "slices"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/duxlabs-dev/dux-shell/core/internal/blur"
"github.com/duxlabs-dev/dux-shell/core/internal/clipboard"
"github.com/duxlabs-dev/dux-shell/core/internal/config"
"github.com/duxlabs-dev/dux-shell/core/internal/distros"
"github.com/duxlabs-dev/dux-shell/core/internal/mangoconf"
"github.com/duxlabs-dev/dux-shell/core/internal/matugen"
"github.com/duxlabs-dev/dux-shell/core/internal/server/brightness"
"github.com/duxlabs-dev/dux-shell/core/internal/server/network"
"github.com/duxlabs-dev/dux-shell/core/internal/site"
"github.com/duxlabs-dev/dux-shell/core/internal/tui"
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
"github.com/duxlabs-dev/dux-shell/core/internal/version"
"github.com/godbus/dbus/v5"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -87,7 +91,6 @@ func (ds *DoctorStatus) OKCount() int {
var ( var (
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`) quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`) niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`) swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`) riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
@@ -95,6 +98,9 @@ var (
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`) labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`) mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`) miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
scrollVersionRegex = regexp.MustCompile(`scroll version (\d+\.\d+)`)
aqueousVersionRegex = regexp.MustCompile(`(?i)aqueous v?(\d+\.\d+(?:\.\d+)?)`)
umbrielVersionRegex = regexp.MustCompile(`umbriel (\d+\.\d+\.\d+)`)
) )
var doctorCmd = &cobra.Command{ var doctorCmd = &cobra.Command{
@@ -160,7 +166,7 @@ func (c category) String() string {
const ( const (
checkNameMaxLength = 21 checkNameMaxLength = 21
doctorDocsURL = "https://danklinux.com/docs/dankmaterialshell/cli-doctor" doctorDocsURL = site.Docs + "/dms/cli-doctor"
) )
type checkResult struct { type checkResult struct {
@@ -218,6 +224,7 @@ func runDoctor(cmd *cobra.Command, args []string) {
checkOptionalDependencies(), checkOptionalDependencies(),
checkConfigurationFiles(), checkConfigurationFiles(),
checkSystemdServices(), checkSystemdServices(),
checkSettingsPortal(),
checkEnvironmentVars(), checkEnvironmentVars(),
checkFonts(), checkFonts(),
) )
@@ -323,15 +330,24 @@ func checkEnvironmentVars() []checkResult {
var results []checkResult var results []checkResult
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...) results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")...) results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")...)
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME_QT6")...) switch {
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") != os.Getenv("QT_QPA_PLATFORMTHEME") { case os.Getenv("QT_QPA_PLATFORMTHEME_QT6") != "":
results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME_QT6")...) results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME_QT6")...)
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") != os.Getenv("QT_QPA_PLATFORMTHEME") {
results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME_QT6")...)
}
case os.Getenv("QT_QPA_PLATFORMTHEME") != "":
if doctorVerbose {
results = append(results, checkResult{catEnvironment, "QT_QPA_PLATFORMTHEME_QT6", statusInfo, "Not set, Qt6 uses QT_QPA_PLATFORMTHEME", "", doctorDocsURL + "#environment-variables"})
}
default:
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME_QT6")...)
}
if matugen.QtengineActive() {
results = append(results, checkQtengineConfig()...)
} }
results = append(results, checkEnvVar("QS_ICON_THEME")...) results = append(results, checkEnvVar("QS_ICON_THEME")...)
results = append(results, checkXDGMenuPrefix()...) results = append(results, checkXDGMenuPrefix()...)
if matugen.QtengineActive() {
results = append(results, checkQtenginePlugin()...)
}
return results return results
} }
@@ -352,7 +368,7 @@ func checkXDGMenuPrefix() []checkResult {
if checkXDGMenuFile(menuPrefix) { if checkXDGMenuFile(menuPrefix) {
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, menuPrefix, "", doctorDocsURL + "#xdg-menu-prefix"}} return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, menuPrefix, "", doctorDocsURL + "#xdg-menu-prefix"}}
} }
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, fmt.Sprintf("%s (menu file not found)", menuPrefix), fmt.Sprintf("Dolphin 'Open with…' dialog may be empty. Ensure /etc/xdg/menus/%sapplications.menu exists.", menuPrefix), doctorDocsURL + "#xdg-menu-prefix"}} return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, fmt.Sprintf("%s (menu file not found)", menuPrefix), fmt.Sprintf("Dolphin 'Open with…' dialog may be empty. Ensure %sapplications.menu exists in ~/.config/menus or /etc/xdg/menus.", menuPrefix), doctorDocsURL + "#xdg-menu-prefix"}}
} }
if _, err := exec.LookPath("keditfiletype"); err == nil { if _, err := exec.LookPath("keditfiletype"); err == nil {
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, "Not set", "Dolphin file associations and 'Open with…' dialog may be empty. Set XDG_MENU_PREFIX=plasma- in your compositor's environment block.", doctorDocsURL + "#xdg-menu-prefix"}} return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, "Not set", "Dolphin file associations and 'Open with…' dialog may be empty. Set XDG_MENU_PREFIX=plasma- in your compositor's environment block.", doctorDocsURL + "#xdg-menu-prefix"}}
@@ -378,55 +394,41 @@ var qtQueryBinaries = []struct{ bin, flag string }{
{"qtpaths", "--query"}, {"qtpaths", "--query"},
} }
// checkQtenginePlugin verifies the qtengine platform theme plugin is installed // checkQtengineConfig reports whether qtengine's config points Qt apps at the
// where Qt actually searches, for every Qt major that can be interrogated. // DMS color scheme, which DMS writes there when it generates a theme.
// func checkQtengineConfig() []checkResult {
// Qt is asked where it looks rather than the filesystem searched, because a
// package can install the Qt5 plugin under a prefix Qt5 does not search, after
// which Qt5 apps silently fall back while Qt6 apps look correct. A search would
// find the plugin and report OK on exactly that bug.
func checkQtenginePlugin() []checkResult {
url := doctorDocsURL + "#environment-variables" url := doctorDocsURL + "#environment-variables"
path := matugen.QtengineConfigPath()
pluginRoots := qtPluginRoots() data, err := os.ReadFile(path)
if os.IsNotExist(err) || (err == nil && len(bytes.TrimSpace(data)) == 0) {
// qmake/qtpaths ship in dev packages most users lack — never warn on a Qt return []checkResult{{catEnvironment, "qtengine config", statusInfo, "Not found", "Qt apps use qtengine's default palette until DMS generates a theme.", url}}
// that could not be interrogated, and never claim it is fine either. }
if len(pluginRoots) == 0 { var cfg struct {
return []checkResult{{ Theme struct {
catEnvironment, "qtengine plugin", statusInfo, "Cannot verify (no qmake/qtpaths)", ColorScheme string `json:"colorScheme"`
"Install a Qt development package to let dms doctor check the plugin against the path Qt actually searches.", } `json:"theme"`
url, }
}} if err == nil {
err = json.Unmarshal(data, &cfg)
}
if err != nil {
return []checkResult{{catEnvironment, "qtengine config", statusWarn, "Unreadable", fmt.Sprintf("DMS won't overwrite %s while it can't parse it, so Qt apps stop following DMS colors. Fix or remove the file.", path), url}}
} }
// Qt also searches QT_PLUGIN_PATH, so a plugin found there is loadable. scheme := matugen.QtengineColorSchemePath()
envRoots := filepath.SplitList(os.Getenv("QT_PLUGIN_PATH")) if cfg.Theme.ColorScheme == scheme {
return []checkResult{{catEnvironment, "qtengine config", statusOK, "Uses DMS colors", path, url}}
var results []checkResult
for _, major := range slices.Sorted(maps.Keys(pluginRoots)) {
// qtengine only ships Qt5 and Qt6 plugins.
if major != "5" && major != "6" {
continue
}
root := pluginRoots[major]
name := fmt.Sprintf("qtengine plugin (Qt%s)", major)
plugin := qtenginePluginPath(append([]string{root}, envRoots...), major)
if plugin == "" {
expected := qtenginePluginFile(root, major)
results = append(results, checkResult{
catEnvironment, name, statusWarn, fmt.Sprintf("Not found in Qt%s plugin path", major),
fmt.Sprintf("Expected %s — Qt%s apps will silently fall back to an unthemed palette. The plugin is likely installed under a prefix Qt%s does not search.", expected, major, major),
url,
})
continue
}
results = append(results, checkResult{catEnvironment, name, statusOK, "Installed", plugin, url})
} }
current := "No color scheme is set"
return results if cfg.Theme.ColorScheme != "" {
current = "The color scheme is " + cfg.Theme.ColorScheme
}
return []checkResult{{
catEnvironment, "qtengine config", statusInfo, "Not using DMS colors",
fmt.Sprintf("%s. DMS sets %s when it generates a theme with the qtengine and KColorScheme templates enabled.", current, scheme),
url,
}}
} }
// checkQtPlatformThemePlugin validates one of the QT_QPA_PLATFORMTHEME // checkQtPlatformThemePlugin validates one of the QT_QPA_PLATFORMTHEME
@@ -436,8 +438,8 @@ func checkQtenginePlugin() []checkResult {
// Quickshell tray menus draw a placeholder for every icon an app publishes. // Quickshell tray menus draw a placeholder for every icon an app publishes.
func checkQtPlatformThemePlugin(varName string) []checkResult { func checkQtPlatformThemePlugin(varName string) []checkResult {
value := os.Getenv(varName) value := os.Getenv(varName)
// Unset is reported by checkEnvVar; qtengine has its own plugin check. // Unset is reported by checkEnvVar.
if value == "" || value == "qtengine" { if value == "" {
return nil return nil
} }
@@ -453,46 +455,111 @@ func checkQtPlatformThemePlugin(varName string) []checkResult {
}} }}
} }
// DMS themes Qt6 through these variables; qt5ct is the Qt5 counterpart. roots := qtPluginRoots()
major := "6" var results []checkResult
if value == "qt5ct" { for _, major := range qtPlatformThemeMajors(varName, value, roots) {
major = "5" results = append(results, checkQtPlatformThemePluginFor(varName, value, major, roots[major]))
} }
files := qtPlatformThemePluginFiles(value) return results
}
// qtPlatformThemeMajors lists the Qt majors whose plugin a variable's value
// must provide. DMS themes Qt6 through these variables and qt5ct is the Qt5
// counterpart. qtengine ships both, and Qt5 reads only QT_QPA_PLATFORMTHEME, so
// its Qt5 plugin is checked there when a Qt5 build tool can be asked.
func qtPlatformThemeMajors(varName, value string, roots map[string]qtInstall) []string {
switch {
case value == "qt5ct":
return []string{"5"}
case value == "qtengine" && varName == "QT_QPA_PLATFORMTHEME" && roots["5"].plugins != "":
return []string{"5", "6"}
}
return []string{"6"}
}
func checkQtPlatformThemePluginFor(varName, value, major string, qt qtInstall) checkResult {
url := doctorDocsURL + "#environment-variables"
files := qtPlatformThemePluginFiles(value, major)
name := fmt.Sprintf("%s plugin (Qt%s)", value, major) name := fmt.Sprintf("%s plugin (Qt%s)", value, major)
root := qtPluginRoots()[major]
// qmake/qtpaths ship in dev packages most users lack — never warn on a Qt // qmake/qtpaths ship in dev packages most users lack — never warn on a Qt
// that could not be interrogated, and never claim it is fine either. // that could not be interrogated, and never claim it is fine either.
if root == "" { if qt.plugins == "" {
return []checkResult{{ return checkResult{
catEnvironment, name, statusInfo, "Cannot verify (no qmake/qtpaths)", catEnvironment, name, statusInfo, "Cannot verify (no qmake/qtpaths)",
"Install a Qt development package to let dms doctor check the plugin against the path Qt actually searches.", "Install a Qt development package to let dms doctor check the plugin against the path Qt actually searches.",
url, url,
}} }
} }
// Qt also searches QT_PLUGIN_PATH, so a plugin found there is loadable. // Qt also searches QT_PLUGIN_PATH, so a plugin found there is loadable.
plugin := qtPluginPath(append([]string{root}, filepath.SplitList(os.Getenv("QT_PLUGIN_PATH"))...), files...) plugin := qtPluginPath(append([]string{qt.plugins}, filepath.SplitList(os.Getenv("QT_PLUGIN_PATH"))...), files...)
if plugin == "" { if plugin == "" {
// plasma-integration ships the kde theme under a file name that has // plasma-integration ships the kde theme under a file name that has
// changed across versions; a miss says nothing reliable about the // changed across versions; a miss says nothing reliable about the
// install, so degrade to info instead of warning at a working setup. // install, so degrade to info instead of warning at a working setup.
if value == "kde" { if value == "kde" {
return []checkResult{{ return checkResult{
catEnvironment, name, statusInfo, "Cannot verify (plasma-integration)", catEnvironment, name, statusInfo, "Cannot verify (plasma-integration)",
"The kde theme comes from plasma-integration, whose plugin file name varies across versions. Verify visually that Qt apps follow your theme.", "The kde theme comes from plasma-integration, whose plugin file name varies across versions. Verify visually that Qt apps follow your theme.",
url, url,
}} }
} }
expected := filepath.Join(root, "platformthemes", files[0]) expected := filepath.Join(qt.plugins, "platformthemes", files[0])
return []checkResult{{ return checkResult{
catEnvironment, name, statusWarn, fmt.Sprintf("Not found in Qt%s plugin path", major), catEnvironment, name, statusWarn, fmt.Sprintf("Not found in Qt%s plugin path", major),
fmt.Sprintf("Expected %s — Qt apps silently fall back to a built-in theme, breaking icon lookups and palettes. Install the package providing it, or correct %s.", expected, varName), fmt.Sprintf("Expected %s — Qt apps silently fall back to a built-in theme, breaking icon lookups and palettes. Install the package providing it, or correct %s.", expected, varName),
url, url,
}} }
} }
return []checkResult{{catEnvironment, name, statusOK, "Installed", plugin, url}} // Qt skips QPA plugins built against another Qt major.minor without a
// warning, so an out-of-tree plugin silently stops loading after a Qt update.
if built, ok := qtPluginBuildVersion(plugin); ok && built != qtMinorVersion(qt.version) {
return checkResult{
catEnvironment, name, statusWarn, fmt.Sprintf("Built for Qt %s, Qt is %s", built, qt.version),
fmt.Sprintf("Qt ignores platform theme plugins built for another Qt minor version, so Qt apps silently fall back to a built-in theme, breaking icon lookups and palettes. Rebuild or reinstall the package providing %s against the installed Qt, then restart the shell.", plugin),
url,
}
}
return checkResult{catEnvironment, name, statusOK, "Installed", plugin, url}
}
// qtPluginBuildVersion reads the Qt major.minor a plugin was built against
// from the ELF note Qt 6 embeds in every plugin. Qt 5 plugins carry no such
// note and report false.
func qtPluginBuildVersion(path string) (string, bool) {
f, err := elf.Open(path)
if err != nil {
return "", false
}
defer f.Close()
section := f.Section(".note.qt.metadata")
if section == nil {
return "", false
}
data, err := section.Data()
if err != nil || len(data) < 12 {
return "", false
}
nameSize := f.ByteOrder.Uint32(data[0:4])
descSize := f.ByteOrder.Uint32(data[4:8])
if f.ByteOrder.Uint32(data[8:12]) != 0x74510001 || descSize < 3 {
return "", false
}
// The descriptor starts with the metadata format version, then Qt's major and minor.
desc := 12 + (uint64(nameSize)+3)&^3
if uint64(len(data)) < desc+uint64(descSize) {
return "", false
}
return fmt.Sprintf("%d.%d", data[desc+1], data[desc+2]), true
}
func qtMinorVersion(version string) string {
parts := strings.SplitN(version, ".", 3)
if len(parts) < 2 {
return version
}
return parts[0] + "." + parts[1]
} }
// qtPlatformThemePluginFiles lists the plugin file names that can provide a // qtPlatformThemePluginFiles lists the plugin file names that can provide a
@@ -500,7 +567,10 @@ func checkQtPlatformThemePlugin(varName string) []checkResult {
// through plugin metadata keys rather than file names, so these are the names // through plugin metadata keys rather than file names, so these are the names
// each integration is known to ship; unmapped values fall back to Qt's // each integration is known to ship; unmapped values fall back to Qt's
// libq<value> naming convention. // libq<value> naming convention.
func qtPlatformThemePluginFiles(value string) []string { func qtPlatformThemePluginFiles(value, major string) []string {
if value == "qtengine" {
return []string{fmt.Sprintf("libqt%sengine-plugin.so", major)}
}
if files, ok := map[string][]string{ if files, ok := map[string][]string{
"qt6ct": {"libqt6ct.so"}, "qt6ct": {"libqt6ct.so"},
"qt5ct": {"libqt5ct.so"}, "qt5ct": {"libqt5ct.so"},
@@ -512,22 +582,28 @@ func qtPlatformThemePluginFiles(value string) []string {
return []string{"libq" + value + ".so", "lib" + value + ".so"} return []string{"libq" + value + ".so", "lib" + value + ".so"}
} }
type qtInstall struct {
plugins string
version string
}
// qtPluginRoots asks every available Qt build tool where its Qt looks for // qtPluginRoots asks every available Qt build tool where its Qt looks for
// plugins, keyed by Qt major. A binary's name does not reliably say which Qt // plugins, keyed by Qt major. A binary's name does not reliably say which Qt
// it belongs to — plain "qmake" is Qt5 on Arch and Qt6 elsewhere — so each one // it belongs to — plain "qmake" is Qt5 on Arch and Qt6 elsewhere — so each one
// is asked for QT_VERSION and the major is taken from the answer. // is asked for QT_VERSION and the major is taken from the answer.
func qtPluginRoots() map[string]string { func qtPluginRoots() map[string]qtInstall {
pluginRoots := make(map[string]string) pluginRoots := make(map[string]qtInstall)
for _, q := range qtQueryBinaries { for _, q := range qtQueryBinaries {
if _, err := exec.LookPath(q.bin); err != nil { if _, err := exec.LookPath(q.bin); err != nil {
continue continue
} }
major, _, _ := strings.Cut(qtQuery(q.bin, q.flag, "QT_VERSION"), ".") version := qtQuery(q.bin, q.flag, "QT_VERSION")
if major == "" || pluginRoots[major] != "" { major, _, _ := strings.Cut(version, ".")
if major == "" || pluginRoots[major].plugins != "" {
continue continue
} }
if root := qtQuery(q.bin, q.flag, "QT_INSTALL_PLUGINS"); root != "" { if root := qtQuery(q.bin, q.flag, "QT_INSTALL_PLUGINS"); root != "" {
pluginRoots[major] = root pluginRoots[major] = qtInstall{plugins: root, version: version}
} }
} }
return pluginRoots return pluginRoots
@@ -550,16 +626,6 @@ func qtPluginPath(roots []string, files ...string) string {
return "" return ""
} }
// qtenginePluginPath returns the first of roots that holds the qtengine platform
// theme plugin for the given Qt major, or "" if none does.
func qtenginePluginPath(roots []string, major string) string {
return qtPluginPath(roots, fmt.Sprintf("libqt%sengine-plugin.so", major))
}
func qtenginePluginFile(root, major string) string {
return filepath.Join(root, "platformthemes", fmt.Sprintf("libqt%sengine-plugin.so", major))
}
// qtQuery asks a Qt build tool for a single QLibraryInfo property. // qtQuery asks a Qt build tool for a single QLibraryInfo property.
func qtQuery(bin, flag, key string) string { func qtQuery(bin, flag, key string) string {
output, err := exec.Command(bin, flag, key).Output() output, err := exec.Command(bin, flag, key).Output()
@@ -585,10 +651,23 @@ func qtQueryValue(output, key string) string {
return "" return ""
} }
// checkXDGMenuFile looks for the menu file where the XDG menu spec does: the
// user's config dir first, then each system config dir.
func checkXDGMenuFile(prefix string) bool { func checkXDGMenuFile(prefix string) bool {
menuPath := fmt.Sprintf("/etc/xdg/menus/%sapplications.menu", prefix) configDirs := os.Getenv("XDG_CONFIG_DIRS")
_, err := os.Stat(menuPath) if configDirs == "" {
return err == nil configDirs = "/etc/xdg"
}
dirs := append([]string{utils.XDGConfigHome()}, filepath.SplitList(configDirs)...)
for _, dir := range dirs {
if dir == "" {
continue
}
if _, err := os.Stat(filepath.Join(dir, "menus", prefix+"applications.menu")); err == nil {
return true
}
}
return false
} }
func readOSRelease() map[string]string { func readOSRelease() map[string]string {
@@ -728,17 +807,20 @@ func checkDMSInstallation() []checkResult {
func checkWindowManagers() []checkResult { func checkWindowManagers() []checkResult {
compositors := []struct { compositors := []struct {
name, versionCmd, versionArg string name, versionCmd, versionArg string
versionRegex *regexp.Regexp parseVersion func(string) string
commands []string commands []string
}{ }{
{"Hyprland", "Hyprland", "--version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}}, {"Hyprland", "Hyprland", "--version", distros.ParseHyprlandVersion, []string{"hyprland", "Hyprland"}},
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}}, {"niri", "niri", "--version", firstSubmatch(niriVersionRegex), []string{"niri"}},
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}}, {"Sway", "sway", "--version", firstSubmatch(swayVersionRegex), []string{"sway"}},
{"River", "river", "-version", riverVersionRegex, []string{"river"}}, {"River", "river", "-version", firstSubmatch(riverVersionRegex), []string{"river"}},
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}}, {"Wayfire", "wayfire", "--version", firstSubmatch(wayfireVersionRegex), []string{"wayfire"}},
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}}, {"labwc", "labwc", "--version", firstSubmatch(labwcVersionRegex), []string{"labwc"}},
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}}, {"mangowc", "mango", "-v", firstSubmatch(mangowcVersionRegex), []string{"mango"}},
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}}, {"Miracle WM", "miracle-wm", "--version", firstSubmatch(miracleVersionRegex), []string{"miracle-wm"}},
{"Scroll", "scroll", "--version", firstSubmatch(scrollVersionRegex), []string{"scroll"}},
{"Aqueous", "aqueous", "-version", firstSubmatch(aqueousVersionRegex), []string{"aqueous"}},
{"Umbriel", "umbriel", "--version", firstSubmatch(umbrielVersionRegex), []string{"umbriel"}},
} }
var results []checkResult var results []checkResult
@@ -760,18 +842,29 @@ func checkWindowManagers() []checkResult {
if doctorVerbose && compositorPath != "" { if doctorVerbose && compositorPath != "" {
details = compositorPath details = compositorPath
} }
display, ver := getVersionFromCommand(c.versionCmd, c.versionArg, c.parseVersion)
results = append(results, checkResult{ results = append(results, checkResult{
catCompositor, c.name, statusOK, catCompositor, c.name, statusOK, display, details,
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
doctorDocsURL + "#compositor-checks", doctorDocsURL + "#compositor-checks",
}) })
if c.name == "Hyprland" && hyprlandLacksLua(ver) {
results = append(results, checkResult{
catCompositor, "Hyprland Lua config", statusWarn,
fmt.Sprintf("Hyprland %s is older than 0.55", ver),
"DMS writes Hyprland config in Lua only, which needs Hyprland 0.55 or newer",
doctorDocsURL + "#compositor-checks",
})
}
if c.name == "Hyprland" {
results = append(results, checkHyprlandConfigFormat(filepath.Join(utils.XDGConfigHome(), "hypr"))...)
}
} }
if !foundAny { if !foundAny {
results = append(results, checkResult{ results = append(results, checkResult{
catCompositor, "Compositor", statusError, catCompositor, "Compositor", statusError,
"No supported Wayland compositor found", "No supported Wayland compositor found",
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm", "Install Hyprland, niri, Sway, River, Wayfire, labwc, mangowc, miracle-wm, Scroll, Aqueous, or Umbriel",
doctorDocsURL + "#compositor-checks", doctorDocsURL + "#compositor-checks",
}) })
} }
@@ -780,11 +873,67 @@ func checkWindowManagers() []checkResult {
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"}) results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
} }
if os.Getenv("AQUEOUS_SOCKET") != "" {
results = append(results, checkAqueousConfigHelper())
}
if os.Getenv("MANGO_INSTANCE_SIGNATURE") != "" {
results = append(results, checkMangoConfig(mangoconf.Dir(), mangoconf.Detect())...)
}
results = append(results, checkCompositorBlurSupport()) results = append(results, checkCompositorBlurSupport())
return results return results
} }
var mangoLegacyOverviewBinds = regexp.MustCompile(`(?im)^\s*mousebind\s*=\s*none\s*,\s*(btn_left\s*,\s*toggleoverview\s*,\s*1|btn_right\s*,\s*killclient\s*,\s*0)\s*$`)
func checkMangoConfig(mangoDir string, dialect mangoconf.Dialect) []checkResult {
url := doctorDocsURL + "#compositor-checks"
paths, _ := filepath.Glob(filepath.Join(mangoDir, "dms", "*.conf"))
paths = append([]string{filepath.Join(mangoDir, "config.conf")}, paths...)
var stale, overviewBinds []string
for _, path := range paths {
data, err := os.ReadFile(path)
if err != nil {
continue
}
if dialect.Translate(string(data)) != string(data) {
stale = append(stale, filepath.Base(path))
}
if mangoLegacyOverviewBinds.Match(data) {
overviewBinds = append(overviewBinds, filepath.Base(path))
}
}
var results []checkResult
if len(stale) > 0 {
results = append(results, checkResult{catCompositor, "Mango config keys", statusError,
fmt.Sprintf("Not %s keys: %s", dialect, strings.Join(stale, ", ")),
"This Mango rejects those lines as unknown keywords. Run `dms config mango-migrate --main` (config.conf is backed up).", url})
}
if len(overviewBinds) > 0 {
results = append(results, checkResult{catCompositor, "Mango mouse binds", statusWarn,
"Obsolete overview binds in " + strings.Join(overviewBinds, ", "),
"mousebind=NONE,btn_left,toggleoverview,1 and mousebind=NONE,btn_right,killclient,0 swallow normal clicks; remove them.", url})
}
return results
}
func checkAqueousConfigHelper() checkResult {
url := doctorDocsURL + "#compositor-checks"
path, err := exec.LookPath("aqueous-config")
if err != nil {
return checkResult{catCompositor, "aqueous-config", statusWarn, "Not found", "Keybind and display settings edits from the shell need the aqueous-config helper", url}
}
details := "Keybind and display settings edits from the shell"
if doctorVerbose {
details = path
}
return checkResult{catCompositor, "aqueous-config", statusOK, "Installed", details, url}
}
func checkCompositorBlurSupport() checkResult { func checkCompositorBlurSupport() checkResult {
supported, err := blur.ProbeSupport() supported, err := blur.ProbeSupport()
if err != nil { if err != nil {
@@ -798,25 +947,56 @@ func checkCompositorBlurSupport() checkResult {
return checkResult{catCompositor, "Background Blur", statusWarn, "Unsupported", "Compositor does not support ext-background-effect-v1", doctorDocsURL + "#compositor-checks"} return checkResult{catCompositor, "Background Blur", statusWarn, "Unsupported", "Compositor does not support ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
} }
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string { // Same detection as the keybinds parser: a regular hyprland.lua wins, else hyprland.conf is read-only.
func checkHyprlandConfigFormat(hyprDir string) []checkResult {
if st, err := os.Stat(filepath.Join(hyprDir, "hyprland.lua")); err == nil && st.Mode().IsRegular() {
return nil
}
if st, err := os.Stat(filepath.Join(hyprDir, "hyprland.conf")); err != nil || !st.Mode().IsRegular() {
return nil
}
return []checkResult{{
catCompositor, "Hyprland Lua config", statusWarn,
"Hyprland config is hyprland.conf",
"DMS keybinds, window rules and displays are read-only until you run `dms setup` to migrate to Lua. Hyprland 0.57 drops .conf support.",
doctorDocsURL + "#compositor-checks",
}}
}
func hyprlandLacksLua(ver string) bool {
return ver != "" && version.CompareVersions(ver, "0.55.0") < 0
}
func firstSubmatch(re *regexp.Regexp) func(string) string {
return func(s string) string {
if m := re.FindStringSubmatch(s); len(m) > 1 {
return m[1]
}
return ""
}
}
func getVersionFromCommand(cmd, arg string, parse func(string) string) (display, ver string) {
output, err := exec.Command(cmd, arg).CombinedOutput() output, err := exec.Command(cmd, arg).CombinedOutput()
if err != nil && len(output) == 0 { if err != nil && len(output) == 0 {
return "installed" return "installed", ""
} }
outStr := string(output) outStr := string(output)
if matches := regex.FindStringSubmatch(outStr); len(matches) > 1 { ver = parse(outStr)
ver := matches[1] if ver == "" {
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") { return strings.TrimSpace(outStr), ""
return ver + " (git)"
}
return ver
} }
return strings.TrimSpace(outStr) if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
return ver + " (git)", ver
}
return ver, ver
} }
func detectRunningWM() string { func detectRunningWM() string {
switch { switch {
case os.Getenv("AQUEOUS_SOCKET") != "":
return "Aqueous"
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "": case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
return "Hyprland" return "Hyprland"
case os.Getenv("NIRI_SOCKET") != "": case os.Getenv("NIRI_SOCKET") != "":
@@ -825,6 +1005,8 @@ func detectRunningWM() string {
return "MangoWC" return "MangoWC"
case os.Getenv("MIRACLESOCK") != "": case os.Getenv("MIRACLESOCK") != "":
return "Miracle WM" return "Miracle WM"
case os.Getenv("UMBRIEL_SOCKET") != "":
return "Umbriel"
case os.Getenv("XDG_CURRENT_DESKTOP") != "": case os.Getenv("XDG_CURRENT_DESKTOP") != "":
return os.Getenv("XDG_CURRENT_DESKTOP") return os.Getenv("XDG_CURRENT_DESKTOP")
} }
@@ -911,7 +1093,7 @@ ShellRoot {
{"Polkit", "Authentication prompts"}, {"Polkit", "Authentication prompts"},
{"IdleMonitor", "Idle detection"}, {"IdleMonitor", "Idle detection"},
{"IdleInhibitor", "Prevent idle/sleep"}, {"IdleInhibitor", "Prevent idle/sleep"},
{"ShortcutInhibitor", "Allow shortcut management (niri)"}, {"ShortcutInhibitor", "Allow shortcut management (niri, Aqueous)"},
{"BackgroundBlur", "Background blur API support in Quickshell"}, {"BackgroundBlur", "Background blur API support in Quickshell"},
} }
@@ -1225,7 +1407,7 @@ func checkAdwGtk3() checkResult {
func checkConfigurationFiles() []checkResult { func checkConfigurationFiles() []checkResult {
configDir, _ := os.UserConfigDir() configDir, _ := os.UserConfigDir()
cacheDir, _ := os.UserCacheDir() cacheDir, _ := os.UserCacheDir()
dmsDir := "DankMaterialShell" dmsDir := "dms"
configFiles := []struct{ name, path string }{ configFiles := []struct{ name, path string }{
{"settings.json", filepath.Join(configDir, dmsDir, "settings.json")}, {"settings.json", filepath.Join(configDir, dmsDir, "settings.json")},
@@ -1267,6 +1449,7 @@ func checkSystemdServices() []checkResult {
var results []checkResult var results []checkResult
dmsState := getServiceState("dms", true) dmsState := getServiceState("dms", true)
wanted := config.SessionWanted()
if !dmsState.exists { if !dmsState.exists {
results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service", doctorDocsURL + "#services"}) results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service", doctorDocsURL + "#services"})
} else { } else {
@@ -1278,12 +1461,28 @@ func checkSystemdServices() []checkResult {
case dmsState.active == "failed": case dmsState.active == "failed":
status = statusError status = statusError
case dmsState.active == "active": case dmsState.active == "active":
case dmsState.enabled == "disabled" && len(wanted) > 0:
message = "Started with " + strings.Join(wanted, ", ")
if dmsState.active != "" {
message += ", " + dmsState.active
}
case dmsState.enabled == "disabled" && os.Getenv("MANGO_INSTANCE_SIGNATURE") != "":
status, message = statusInfo, "Disabled (Mango starts DMS from its config)"
case dmsState.enabled == "disabled": case dmsState.enabled == "disabled":
status, message = statusWarn, "Disabled" status, message = statusWarn, "Disabled"
case dmsState.active == "inactive": case dmsState.active == "inactive":
status = statusError status = statusError
} }
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"}) details := ""
if len(wanted) > 0 {
details = "Wanted by " + strings.Join(wanted, ", ")
}
if wm, ok := config.RunningCompositor(); ok && status != statusOK {
if unit := config.SessionUnitName(wm); !slices.Contains(wanted, unit) {
details = fmt.Sprintf("Nothing starts dms.service with %s. Run 'dms systemd enable'.", unit)
}
}
results = append(results, checkResult{catServices, "dms.service", status, message, details, doctorDocsURL + "#services"})
} }
if dmsState.exists && dmsState.enabled == "enabled" { if dmsState.exists && dmsState.enabled == "enabled" {
@@ -1308,6 +1507,24 @@ func checkSystemdServices() []checkResult {
return results return results
} }
// Chromium, Electron and libadwaita follow light/dark through the settings
// portal; without it they fall back to GTK theme colours that never update.
func checkSettingsPortal() []checkResult {
conn, err := dbus.SessionBus()
if err != nil {
return nil
}
var value dbus.Variant
err = conn.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop").
Call("org.freedesktop.portal.Settings.ReadOne", 0, "org.freedesktop.appearance", "color-scheme").
Store(&value)
if err == nil {
return []checkResult{{catServices, "xdg-desktop-portal", statusOK, "Settings portal reachable", "", doctorDocsURL + "#services"}}
}
hint := "Apps cannot follow light/dark mode. The portal is D-Bus activated and needs graphical-session.target; without systemd, or a compositor that never starts that target (Mango before 0.17.1), exec the portal backend and xdg-desktop-portal at session start."
return []checkResult{{catServices, "xdg-desktop-portal", statusWarn, "Settings portal unreachable", hint, doctorDocsURL + "#services"}}
}
type serviceState struct { type serviceState struct {
exists bool exists bool
enabled string enabled string
@@ -1395,7 +1612,7 @@ func printResultLine(r checkResult, styles tui.Styles) {
fmt.Printf(" %s %s %s %s\n", style.Render(icon), name, styles.Subtle.Render(dots), r.message) fmt.Printf(" %s %s %s %s\n", style.Render(icon), name, styles.Subtle.Render(dots), r.message)
if doctorVerbose && r.details != "" { if r.details != "" && (doctorVerbose || r.status != statusOK) {
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details)) fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
} }
@@ -1454,7 +1671,7 @@ func formatResultsPlain(results []checkResult) string {
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message) fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message)
if doctorVerbose && r.details != "" { if r.details != "" && (doctorVerbose || r.status != statusOK) {
fmt.Fprintf(&sb, " - %s\n", r.details) fmt.Fprintf(&sb, " - %s\n", r.details)
} }
} }
@@ -1472,25 +1689,29 @@ func formatResultsPlain(results []checkResult) string {
} }
const ( const (
defaultDoctorFontFamily = "Inter Variable" defaultDoctorFontFamily = "Google Sans Flex"
defaultDoctorMonoFontFamily = "Fira Code" defaultDoctorMonoFontFamily = "Fira Code"
defaultDoctorDisplayFamily = "DM Serif Display"
) )
// bundledFontRelPaths maps settings/default family names to font files shipped with
// the shell and loaded via Qt FontLoader (not registered with fontconfig).
var bundledFontRelPaths = map[string][]string{ var bundledFontRelPaths = map[string][]string{
"inter variable": { "google sans flex": {
"DankCommon/assets/fonts/inter/InterVariable.ttf", "DCommon/assets/fonts/google-sans-flex/GoogleSansFlex.ttf",
"assets/fonts/inter/InterVariable.ttf",
}, },
"fira code": { "fira code": {
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf", "DCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf", "assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
}, },
"firacode nerd font": { "firacode nerd font": {
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf", "DCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf", "assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
}, },
"dm serif display": {
"DCommon/assets/fonts/dm-serif-display/DMSerifDisplay-Regular.ttf",
},
"notable": {
"DCommon/assets/fonts/notable/Notable-Regular.ttf",
},
} }
func resolveDoctorShellPath() string { func resolveDoctorShellPath() string {
@@ -1561,7 +1782,7 @@ func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailab
return checkResult{ return checkResult{
catFonts, label, statusWarn, catFonts, label, statusWarn,
fmt.Sprintf("'%s' bundled file missing", family), fmt.Sprintf("'%s' bundled file missing", family),
"Expected font file missing from shell install. Reinstall DMS or check DankCommon assets.", "Expected font file missing from shell install. Reinstall DMS or check DCommon assets.",
url, url,
} }
} }
@@ -1602,13 +1823,15 @@ func checkFonts() []checkResult {
fontFamily := defaultDoctorFontFamily fontFamily := defaultDoctorFontFamily
monoFontFamily := defaultDoctorMonoFontFamily monoFontFamily := defaultDoctorMonoFontFamily
displayFontFamily := defaultDoctorDisplayFamily
if configDir, err := os.UserConfigDir(); err == nil { if configDir, err := os.UserConfigDir(); err == nil {
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json") settingsPath := filepath.Join(configDir, "dms", "settings.json")
if data, err := os.ReadFile(settingsPath); err == nil { if data, err := os.ReadFile(settingsPath); err == nil {
var settings struct { var settings struct {
FontFamily string `json:"fontFamily"` FontFamily string `json:"fontFamily"`
MonoFontFamily string `json:"monoFontFamily"` MonoFontFamily string `json:"monoFontFamily"`
DisplayFontFamily string `json:"displayFontFamily"`
} }
if err := json.Unmarshal(data, &settings); err == nil { if err := json.Unmarshal(data, &settings); err == nil {
if settings.FontFamily != "" { if settings.FontFamily != "" {
@@ -1617,12 +1840,15 @@ func checkFonts() []checkResult {
if settings.MonoFontFamily != "" { if settings.MonoFontFamily != "" {
monoFontFamily = settings.MonoFontFamily monoFontFamily = settings.MonoFontFamily
} }
if settings.DisplayFontFamily != "" {
displayFontFamily = settings.DisplayFontFamily
}
} }
} }
} }
shellPath := resolveDoctorShellPath() shellPath := resolveDoctorShellPath()
needFontconfig := !isBundledDefaultFont(fontFamily) || !isBundledDefaultFont(monoFontFamily) needFontconfig := !isBundledDefaultFont(fontFamily) || !isBundledDefaultFont(monoFontFamily) || !isBundledDefaultFont(displayFontFamily)
fcListAvailable := utils.CommandExists("fc-list") fcListAvailable := utils.CommandExists("fc-list")
fcCache := "" fcCache := ""
@@ -1646,6 +1872,7 @@ func checkFonts() []checkResult {
results = append(results, results = append(results,
checkConfiguredFont("Normal Font", fontFamily, shellPath, fcCache, fcListAvailable, url), checkConfiguredFont("Normal Font", fontFamily, shellPath, fcCache, fcListAvailable, url),
checkConfiguredFont("Monospace Font", monoFontFamily, shellPath, fcCache, fcListAvailable, url), checkConfiguredFont("Monospace Font", monoFontFamily, shellPath, fcCache, fcListAvailable, url),
checkConfiguredFont("Display Font", displayFontFamily, shellPath, fcCache, fcListAvailable, url),
) )
return results return results
@@ -0,0 +1,28 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestCheckBundledGoogleSans(t *testing.T) {
shellPath := t.TempDir()
fontPath := filepath.Join(shellPath, "DCommon", "assets", "fonts", "google-sans-flex", "GoogleSansFlex.ttf")
missing := checkConfiguredFont("UI font", "Google Sans Flex", shellPath, "", false, "")
if missing.status != statusWarn {
t.Fatalf("missing bundled font status = %v, want %v", missing.status, statusWarn)
}
if err := os.MkdirAll(filepath.Dir(fontPath), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(fontPath, []byte("font"), 0o644); err != nil {
t.Fatal(err)
}
for _, family := range []string{"Google Sans Flex", " google sans flex "} {
result := checkConfiguredFont("UI font", family, shellPath, "", false, "")
if result.status != statusOK {
t.Fatalf("bundled font %q status = %v, want %v", family, result.status, statusOK)
}
}
}
+228 -28
View File
@@ -1,10 +1,15 @@
package main package main
import ( import (
"bytes"
"debug/elf"
"encoding/binary"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/duxlabs-dev/dux-shell/core/internal/mangoconf"
) )
func TestQuickshellVersionFailureDetails(t *testing.T) { func TestQuickshellVersionFailureDetails(t *testing.T) {
@@ -59,24 +64,12 @@ QT_VERSION:5.15.19`
key: "QT_INSTALL_PLUGINS", key: "QT_INSTALL_PLUGINS",
want: "/usr/lib/qt/plugins", want: "/usr/lib/qt/plugins",
}, },
{
name: "a different key from the same dump",
output: dumpAll,
key: "QT_VERSION",
want: "5.15.19",
},
{ {
name: "key absent from a multi line dump yields nothing", name: "key absent from a multi line dump yields nothing",
output: dumpAll, output: dumpAll,
key: "QT_INSTALL_LIBEXECS", key: "QT_INSTALL_LIBEXECS",
want: "", want: "",
}, },
{
name: "empty output yields nothing",
output: "",
key: "QT_INSTALL_PLUGINS",
want: "",
},
{ {
name: "surrounding whitespace is trimmed", name: "surrounding whitespace is trimmed",
output: " QT_INSTALL_PLUGINS: /usr/lib/qt/plugins \n", output: " QT_INSTALL_PLUGINS: /usr/lib/qt/plugins \n",
@@ -119,24 +112,43 @@ func writePlugin(t *testing.T, pluginsDir, file string) {
} }
} }
// qtenginePluginFile("", major) is a path with a directory component, and // writeQtPluginELF writes a minimal ELF holding only the .note.qt.metadata
// qtPluginPath joins platformthemes/ again — a regression here silently warns // note Qt 6 embeds in plugins, recording the Qt version it was built against.
// at every correctly installed system, so pin the full path down. func writeQtPluginELF(t *testing.T, pluginsDir, file string, major, minor byte) {
func TestQtenginePluginPath(t *testing.T) { t.Helper()
root := t.TempDir() var note bytes.Buffer
if got := qtenginePluginPath([]string{root}, "6"); got != "" { binary.Write(&note, binary.LittleEndian, []uint32{12, 4, 0x74510001})
t.Fatalf("missing plugin should yield an empty path, got %q", got) note.WriteString("qt-project!\x00")
note.Write([]byte{1, major, minor, 1})
shstrtab := []byte("\x00.note.qt.metadata\x00.shstrtab\x00")
noteOff := uint64(64)
strOff := noteOff + uint64(note.Len())
shOff := strOff + uint64(len(shstrtab))
var out bytes.Buffer
header := elf.Header64{
Type: uint16(elf.ET_DYN), Machine: uint16(elf.EM_X86_64), Version: uint32(elf.EV_CURRENT),
Shoff: shOff, Ehsize: 64, Shentsize: 64, Shnum: 3, Shstrndx: 2,
} }
want := filepath.Join(root, "platformthemes", "libqt6engine-plugin.so") copy(header.Ident[:], elf.ELFMAG)
if err := os.MkdirAll(filepath.Join(root, "platformthemes"), 0o755); err != nil { header.Ident[elf.EI_CLASS] = byte(elf.ELFCLASS64)
header.Ident[elf.EI_DATA] = byte(elf.ELFDATA2LSB)
header.Ident[elf.EI_VERSION] = byte(elf.EV_CURRENT)
binary.Write(&out, binary.LittleEndian, header)
out.Write(note.Bytes())
out.Write(shstrtab)
binary.Write(&out, binary.LittleEndian, []elf.Section64{
{},
{Name: 1, Type: uint32(elf.SHT_NOTE), Off: noteOff, Size: uint64(note.Len()), Addralign: 4},
{Name: 20, Type: uint32(elf.SHT_STRTAB), Off: strOff, Size: uint64(len(shstrtab)), Addralign: 1},
})
if err := os.MkdirAll(filepath.Join(pluginsDir, "platformthemes"), 0o755); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := os.WriteFile(want, nil, 0o644); err != nil { if err := os.WriteFile(filepath.Join(pluginsDir, "platformthemes", file), out.Bytes(), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got := qtenginePluginPath([]string{root}, "6"); got != want {
t.Fatalf("qtenginePluginPath = %q, want %q", got, want)
}
} }
func TestCheckQtPlatformThemePlugin(t *testing.T) { func TestCheckQtPlatformThemePlugin(t *testing.T) {
@@ -169,6 +181,34 @@ func TestCheckQtPlatformThemePlugin(t *testing.T) {
} }
}) })
t.Run("plugin built for the installed Qt minor reports OK", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
plugins := t.TempDir()
writeQtPluginELF(t, plugins, "libqt6ct.so", 6, 12)
stubQtQueryTool(t, "qmake6", "6.12.3", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusOK {
t.Fatalf("got %+v, want one OK result", results)
}
})
t.Run("plugin built for another Qt minor warns", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
plugins := t.TempDir()
writeQtPluginELF(t, plugins, "libqt6ct.so", 6, 11)
stubQtQueryTool(t, "qmake6", "6.12.0", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusWarn {
t.Fatalf("got %+v, want one warn result", results)
}
if results[0].message != "Built for Qt 6.11, Qt is 6.12.0" {
t.Fatalf("message = %q, want both Qt versions", results[0].message)
}
if !strings.Contains(results[0].details, filepath.Join(plugins, "platformthemes", "libqt6ct.so")) {
t.Fatalf("details should name the stale plugin, got %q", results[0].details)
}
})
t.Run("missing plugin warns with the expected path", func(t *testing.T) { t.Run("missing plugin warns with the expected path", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct") t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
plugins := t.TempDir() plugins := t.TempDir()
@@ -192,15 +232,55 @@ func TestCheckQtPlatformThemePlugin(t *testing.T) {
} }
}) })
t.Run("unset and qtengine are left to their own checks", func(t *testing.T) { t.Run("unset is left to checkEnvVar", func(t *testing.T) {
t.Setenv("PATH", t.TempDir()) t.Setenv("PATH", t.TempDir())
t.Setenv("QT_QPA_PLATFORMTHEME", "") t.Setenv("QT_QPA_PLATFORMTHEME", "")
if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil { if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil {
t.Fatalf("unset should yield no results, got %+v", results) t.Fatalf("unset should yield no results, got %+v", results)
} }
})
t.Run("qtengine checks the Qt5 and Qt6 plugins", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qtengine") t.Setenv("QT_QPA_PLATFORMTHEME", "qtengine")
if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil { qt5, qt6 := t.TempDir(), t.TempDir()
t.Fatalf("qtengine should yield no results, got %+v", results) writePlugin(t, qt6, "libqt6engine-plugin.so")
dir5 := stubQtQueryTool(t, "qmake-qt5", "5.15.19", qt5)
dir6 := stubQtQueryTool(t, "qmake6", "6.11.2", qt6)
t.Setenv("PATH", dir6+string(os.PathListSeparator)+dir5)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 2 {
t.Fatalf("got %d results, want 2: %+v", len(results), results)
}
if results[0].name != "qtengine plugin (Qt5)" || results[0].status != statusWarn {
t.Fatalf("Qt5 result = %+v, want a warning for the missing plugin", results[0])
}
if !strings.Contains(results[0].details, filepath.Join(qt5, "platformthemes", "libqt5engine-plugin.so")) {
t.Fatalf("Qt5 details should contain the expected path, got %q", results[0].details)
}
if results[1].name != "qtengine plugin (Qt6)" || results[1].status != statusOK {
t.Fatalf("Qt6 result = %+v, want OK", results[1])
}
})
t.Run("qtengine in the qt6 variable skips Qt5", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME_QT6", "qtengine")
qt5, qt6 := t.TempDir(), t.TempDir()
writePlugin(t, qt6, "libqt6engine-plugin.so")
dir5 := stubQtQueryTool(t, "qmake-qt5", "5.15.19", qt5)
dir6 := stubQtQueryTool(t, "qmake6", "6.11.2", qt6)
t.Setenv("PATH", dir6+string(os.PathListSeparator)+dir5)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME_QT6")
if len(results) != 1 || results[0].name != "qtengine plugin (Qt6)" || results[0].status != statusOK {
t.Fatalf("got %+v, want one OK Qt6 result", results)
}
})
t.Run("qtengine without Qt tools degrades to one info", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qtengine")
t.Setenv("PATH", t.TempDir())
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusInfo || results[0].name != "qtengine plugin (Qt6)" {
t.Fatalf("got %+v, want one Qt6 info result", results)
} }
}) })
@@ -235,3 +315,123 @@ func TestCheckQtPlatformThemePlugin(t *testing.T) {
} }
}) })
} }
func TestCheckQtengineConfig(t *testing.T) {
configHome, dataHome := t.TempDir(), t.TempDir()
t.Setenv("XDG_CONFIG_HOME", configHome)
t.Setenv("XDG_DATA_HOME", dataHome)
path := filepath.Join(configHome, "qtengine", "config.json")
scheme := filepath.Join(dataHome, "color-schemes", "DankMatugen.colors")
write := func(t *testing.T, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
tests := []struct {
name string
content string // "" leaves the file absent
status status
message string
}{
{"missing", "", statusInfo, "Not found"},
{"empty", "\n", statusInfo, "Not found"},
{"unparseable", "{", statusWarn, "Unreadable"},
{"theme is not an object", `{"theme": 1}`, statusWarn, "Unreadable"},
{"dms scheme", `{"theme": {"colorScheme": "` + scheme + `"}}`, statusOK, "Uses DMS colors"},
{"other scheme", `{"theme": {"colorScheme": "/usr/share/color-schemes/BreezeDark.colors"}}`, statusInfo, "Not using DMS colors"},
{"no scheme", `{"theme": {}}`, statusInfo, "Not using DMS colors"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
os.Remove(path)
if tc.content != "" {
write(t, tc.content)
}
results := checkQtengineConfig()
if len(results) != 1 || results[0].status != tc.status || results[0].message != tc.message {
t.Fatalf("got %+v, want %s %q", results, tc.status, tc.message)
}
})
}
}
func TestCheckXDGMenuFile(t *testing.T) {
configHome, systemDir := t.TempDir(), t.TempDir()
t.Setenv("XDG_CONFIG_HOME", configHome)
t.Setenv("XDG_CONFIG_DIRS", systemDir)
if checkXDGMenuFile("hyprland-") {
t.Fatal("no menu file anywhere should not be found")
}
for _, dir := range []string{configHome, systemDir} {
menus := filepath.Join(dir, "menus")
if err := os.MkdirAll(menus, 0o755); err != nil {
t.Fatal(err)
}
file := filepath.Join(menus, "hyprland-applications.menu")
if err := os.WriteFile(file, nil, 0o644); err != nil {
t.Fatal(err)
}
if !checkXDGMenuFile("hyprland-") {
t.Fatalf("menu file in %s should be found", menus)
}
os.Remove(file)
}
}
func TestCheckMangoConfigFlagsWrongDialectAndOverviewBinds(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "dms"), 0o755); err != nil {
t.Fatal(err)
}
_ = os.WriteFile(filepath.Join(dir, "config.conf"), []byte("exec-once=dms run\nmousebind=NONE,btn_left,toggleoverview,1\n"), 0o644)
_ = os.WriteFile(filepath.Join(dir, "dms", "layout.conf"), []byte("border_px=2\n"), 0o644)
if got := checkMangoConfig(dir, mangoconf.Legacy); len(got) != 2 || got[0].message != "Not legacy keys: layout.conf" {
t.Fatalf("legacy: %+v", got)
}
if got := checkMangoConfig(dir, mangoconf.Snake); len(got) != 2 || got[0].message != "Not snake keys: config.conf" {
t.Fatalf("snake: %+v", got)
}
}
func TestHyprlandLacksLua(t *testing.T) {
for ver, want := range map[string]bool{
"0.54.9": true,
"0.55.0": false,
"": false,
} {
if got := hyprlandLacksLua(ver); got != want {
t.Errorf("hyprlandLacksLua(%q) = %v, want %v", ver, got, want)
}
}
}
func TestCheckHyprlandConfigFormat(t *testing.T) {
for name, tc := range map[string]struct {
files []string
warn bool
}{
"lua only": {[]string{"hyprland.lua"}, false},
"lua wins": {[]string{"hyprland.lua", "hyprland.conf"}, false},
"conf only": {[]string{"hyprland.conf"}, true},
"neither": {nil, false},
} {
dir := t.TempDir()
for _, f := range tc.files {
if err := os.WriteFile(filepath.Join(dir, f), nil, 0o644); err != nil {
t.Fatal(err)
}
}
got := checkHyprlandConfigFormat(dir)
if warned := len(got) == 1 && got[0].status == statusWarn; warned != tc.warn || (!tc.warn && len(got) != 0) {
t.Errorf("%s: got %+v, want warn=%v", name, got, tc.warn)
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch" "github.com/duxlabs-dev/dux-shell/core/internal/netfetch"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+1 -1
View File
@@ -3,7 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+45 -22
View File
@@ -13,20 +13,21 @@ import (
"strings" "strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/duxlabs-dev/dux-shell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/duxlabs-dev/dux-shell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch" "github.com/duxlabs-dev/dux-shell/core/internal/netfetch"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/duxlabs-dev/dux-shell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/duxlabs-dev/dux-shell/core/internal/server/sysupdate"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version" "github.com/duxlabs-dev/dux-shell/core/internal/utils"
"github.com/duxlabs-dev/dux-shell/core/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var updateCmd = &cobra.Command{ var updateCmd = &cobra.Command{
Use: "update", Use: "update",
Short: "Update DankMaterialShell to the latest version", Short: "Update Dux Shell to the latest version",
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution", Long: "Update Dux Shell to the latest version using the appropriate package manager for your distribution",
PreRunE: shellApp.ResolveConfig, PreRunE: shellApp.ResolveConfig,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
runUpdate() runUpdate()
@@ -35,7 +36,7 @@ var updateCmd = &cobra.Command{
var updateCheckCmd = &cobra.Command{ var updateCheckCmd = &cobra.Command{
Use: "check", Use: "check",
Short: "Check if updates are available for DankMaterialShell", Short: "Check if updates are available for Dux Shell",
Long: "Check for available updates without performing the actual update", Long: "Check for available updates without performing the actual update",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
runUpdateCheck() runUpdateCheck()
@@ -43,9 +44,19 @@ var updateCheckCmd = &cobra.Command{
} }
func runUpdateCheck() { func runUpdateCheck() {
fmt.Println("Checking for DankMaterialShell updates...") fmt.Println("Checking for Dux Shell updates...")
fmt.Println() fmt.Println()
shell := sysupdate.ProbeShell(context.Background(), Version)
switch shell.InstallMethod {
case sysupdate.InstallPacman, sysupdate.InstallRPM, sysupdate.InstallDpkg, sysupdate.InstallXbps:
fmt.Printf("Installed as package %s (%s); update it with your package manager.\n", shell.PackageName, shell.InstallMethod)
os.Exit(0)
case sysupdate.InstallNix:
fmt.Println("Managed by Nix; update through your flake or channel.")
os.Exit(0)
}
versionInfo, err := version.GetDMSVersionInfo() versionInfo, err := version.GetDMSVersionInfo()
if err != nil { if err != nil {
log.Fatalf("Error checking for updates: %v", err) log.Fatalf("Error checking for updates: %v", err)
@@ -158,7 +169,7 @@ func updateArchLinux() error {
return updateOtherDistros() return updateOtherDistros()
} }
fmt.Printf("This will update DankMaterialShell using %s.\n", helper) fmt.Printf("This will update Dux Shell using %s.\n", helper)
if !confirmUpdate() { if !confirmUpdate() {
return errdefs.ErrUpdateCancelled return errdefs.ErrUpdateCancelled
} }
@@ -182,12 +193,8 @@ func updateOtherDistros() error {
} }
dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms") dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms")
_, statErr := os.Stat(dmsPath)
if _, err := os.Stat(dmsPath); os.IsNotExist(err) { hasClone := statErr == nil
return fmt.Errorf("DMS configuration directory not found at %s", dmsPath)
}
fmt.Printf("Found DMS configuration at %s\n", dmsPath)
versionInfo, err := version.GetDMSVersionInfo() versionInfo, err := version.GetDMSVersionInfo()
if err == nil && !versionInfo.HasUpdate { if err == nil && !versionInfo.HasUpdate {
@@ -199,9 +206,25 @@ func updateOtherDistros() error {
return errdefs.ErrNoUpdateNeeded return errdefs.ErrNoUpdateNeeded
} }
// Release binaries embed the shell; without a clone the binary is the whole install.
if !hasClone {
fmt.Println("\nThis will update the dms binary from GitHub releases.")
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Println("\n=== Updating dms binary ===")
if err := updateDMSBinary(); err != nil {
return fmt.Errorf("failed to update dms binary: %w", err)
}
fmt.Println("dms binary successfully updated")
return nil
}
fmt.Printf("Found DMS configuration at %s\n", dmsPath)
fmt.Println("\nThis will update:") fmt.Println("\nThis will update:")
fmt.Println(" 1. The dms binary from GitHub releases") fmt.Println(" 1. The dms binary from GitHub releases")
fmt.Println(" 2. DankMaterialShell configuration using git") fmt.Println(" 2. Dux Shell configuration using git")
if !confirmUpdate() { if !confirmUpdate() {
return errdefs.ErrUpdateCancelled return errdefs.ErrUpdateCancelled
} }
@@ -362,7 +385,7 @@ func offerReclone(dmsPath string) bool {
} }
fmt.Println("Cloning fresh copy...") fmt.Println("Cloning fresh copy...")
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath) cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/duxlabs-dev/dux-shell.git", dmsPath)
cloneCmd.Stdout = os.Stdout cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr cloneCmd.Stderr = os.Stderr
if err := cloneCmd.Run(); err != nil { if err := cloneCmd.Run(); err != nil {
@@ -426,8 +449,8 @@ func updateDMSBinary() error {
} }
defer os.RemoveAll(tempDir) defer os.RemoveAll(tempDir)
binaryURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch) binaryURL := fmt.Sprintf("https://github.com/duxlabs-dev/dux-shell/releases/download/%s/dms-cli-%s.gz", version, arch)
checksumURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz.sha256", version, arch) checksumURL := fmt.Sprintf("https://github.com/duxlabs-dev/dux-shell/releases/download/%s/dms-cli-%s.gz.sha256", version, arch)
binaryPath := filepath.Join(tempDir, "dms.gz") binaryPath := filepath.Join(tempDir, "dms.gz")
checksumPath := filepath.Join(tempDir, "dms.gz.sha256") checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
+9 -4
View File
@@ -6,7 +6,8 @@ import (
"os/exec" "os/exec"
"syscall" "syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/duxlabs-dev/dux-shell/core/internal/distros"
"github.com/duxlabs-dev/dux-shell/core/internal/site"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -52,11 +53,15 @@ func greeterPackageInstallHint() string {
return "Install package: dms-greeter" return "Install package: dms-greeter"
} }
if config.ID == "pika" {
return "Install with 'sudo apt install dms-greeter'"
}
switch config.Family { switch config.Family {
case distros.FamilyDebian: case distros.FamilyDebian:
return "Install with 'sudo apt install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#debian)" return "Install with 'sudo apt install dms-greeter' (requires Dux Labs OBS repo — see " + site.Docs + "/dms-greeter/installation#debian)"
case distros.FamilySUSE: case distros.FamilySUSE:
return "Install with 'sudo zypper install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#opensuse)" return "Install with 'sudo zypper install dms-greeter' (requires Dux Labs OBS repo — see " + site.Docs + "/dms-greeter/installation#opensuse--derivatives)"
case distros.FamilyUbuntu: case distros.FamilyUbuntu:
return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)" return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)"
case distros.FamilyFedora: case distros.FamilyFedora:
@@ -64,7 +69,7 @@ func greeterPackageInstallHint() string {
case distros.FamilyArch: case distros.FamilyArch:
return "Install from AUR with 'paru -S greetd-dms-greeter-bin' or 'yay -S greetd-dms-greeter-bin'" return "Install from AUR with 'paru -S greetd-dms-greeter-bin' or 'yay -S greetd-dms-greeter-bin'"
case distros.FamilyVoid: case distros.FamilyVoid:
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://void.danklinux.com/dms/current' | sudo tee /etc/xbps.d/dms.conf)" return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=" + distros.VoidDMSRepo + "' | sudo tee /etc/xbps.d/dms.conf)"
default: default:
return "Install the dms-greeter package for your distribution" return "Install the dms-greeter package for your distribution"
} }
+485
View File
@@ -0,0 +1,485 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/duxlabs-dev/dux-go/ipc"
"github.com/duxlabs-dev/dux-shell/core/internal/icc"
"github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/duxlabs-dev/dux-shell/core/internal/server/wayland"
"github.com/spf13/cobra"
)
var iccCmd = &cobra.Command{
Use: "icc",
Short: "Manage ICC color profiles",
Long: "Manage ICC color profile assignments for displays",
}
var iccListCmd = &cobra.Command{
Use: "list",
Short: "List available ICC profiles and current assignments",
Args: cobra.NoArgs,
Run: runICCList,
}
var iccInfoCmd = &cobra.Command{
Use: "info <file>",
Short: "Show details of an ICC profile",
Args: cobra.ExactArgs(1),
Run: runICCInfo,
}
var iccApplyCmd = &cobra.Command{
Use: "apply <output> <file>",
Short: "Apply ICC profile to a specific output",
Args: cobra.ExactArgs(2),
Run: runICCApply,
}
var iccRemoveCmd = &cobra.Command{
Use: "remove <output>",
Short: "Remove ICC profile from a specific output",
Args: cobra.ExactArgs(1),
Run: runICCRemove,
}
var iccStatusCmd = &cobra.Command{
Use: "status",
Short: "Show ICC profile status for all outputs",
Args: cobra.NoArgs,
Run: runICCStatus,
}
var iccSetTempCmd = &cobra.Command{
Use: "set-temp <output> <kelvin>",
Aliases: []string{"setTemp"},
Short: "Set the color temperature for a specific output",
Long: "Set the color temperature for a specific output, independently of the\n" +
"night light schedule: every display keeps its own value, which also makes\n" +
"the range wider than the night light's.\n\n" +
"For an output with an ICC profile the temperature is composed on top of\n" +
"it, treating the profile as measured at 6500K.\n\n" +
"Accepts 1000-10000K; 0 removes the override, so the output follows the\n" +
"night light schedule again (6500K while it is disabled).",
Args: cobra.ExactArgs(2),
Run: runICCSetTemp,
}
func init() {
iccCmd.AddCommand(iccListCmd, iccInfoCmd, iccApplyCmd, iccRemoveCmd, iccStatusCmd, iccSetTempCmd)
}
func getICCConfigDir() string {
return wayland.ICCProfilesDir()
}
func listAvailableProfiles() []string {
dir := getICCConfigDir()
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var profiles []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
ext := strings.ToLower(filepath.Ext(name))
if ext == ".icc" || ext == ".icm" {
profiles = append(profiles, filepath.Join(dir, name))
}
}
return profiles
}
func runICCList(cmd *cobra.Command, args []string) {
// Get current assignments from running server
req := ipc.Request{
Method: "wayland.icc.getStatus",
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to get ICC status: %v", err)
}
statusMap := make(map[string]map[string]any)
outputs := []string{}
if resp.Result != nil {
if resultBytes, err := json.Marshal(*resp.Result); err == nil {
var result map[string]any
if err := json.Unmarshal(resultBytes, &result); err == nil {
if outputsRaw, ok := result["outputs"].([]any); ok {
for _, o := range outputsRaw {
if s, ok := o.(string); ok {
outputs = append(outputs, s)
}
}
}
if profilesRaw, ok := result["profiles"].(map[string]any); ok {
for k, v := range profilesRaw {
if m, ok := v.(map[string]any); ok {
statusMap[k] = m
}
}
}
}
}
}
// Show existing assignments
fmt.Println("Current ICC Assignments:")
fmt.Println(strings.Repeat("─", 80))
if len(outputs) == 0 && len(statusMap) == 0 {
fmt.Println(" (no outputs found)")
} else {
shown := make(map[string]bool)
for _, output := range outputs {
shown[output] = true
if status, ok := statusMap[output]; ok {
desc := ""
if d, ok := status["description"].(string); ok {
desc = d
}
path := ""
if p, ok := status["path"].(string); ok {
path = p
}
fmt.Printf(" %-20s %-30s %s\n", output, desc, path)
} else {
fmt.Printf(" %-20s %-30s %s\n", output, "(none)", "")
}
}
// Show any outputs that are in statusMap but not in outputs list
for output, status := range statusMap {
if shown[output] {
continue
}
desc := ""
if d, ok := status["description"].(string); ok {
desc = d
}
path := ""
if p, ok := status["path"].(string); ok {
path = p
}
fmt.Printf(" %-20s %-30s %s\n", output, desc, path)
}
}
// Show available profiles in config dir
fmt.Println()
fmt.Println("Available Profiles in", getICCConfigDir()+":")
fmt.Println(strings.Repeat("─", 80))
profiles := listAvailableProfiles()
if len(profiles) == 0 {
fmt.Println(" (no .icc/.icm files found)")
} else {
for _, path := range profiles {
profile, err := icc.ParseFile(path)
if err != nil {
fmt.Printf(" %-40s (parse error: %v)\n", filepath.Base(path), err)
continue
}
// A profile without a vcgt table has no video card gamma ramp, so the
// daemon refuses to apply it: say so here instead of letting the user
// find out from a rejected `dms icc apply`.
note := ""
if !profile.HasVCGT {
note = " (no vcgt table: cannot be applied)"
}
fmt.Printf(" %-40s %s%s\n", filepath.Base(path), profile.Description, note)
}
}
}
func runICCInfo(cmd *cobra.Command, args []string) {
path := args[0]
profile, err := icc.ParseFile(path)
if err != nil {
log.Fatalf("Failed to parse ICC profile: %v", err)
}
fmt.Printf("ICC Profile: %s\n", filepath.Base(path))
fmt.Println(strings.Repeat("─", 60))
fmt.Printf(" Description: %s\n", profile.Description)
fmt.Printf(" Version: %s\n", profile.Version)
fmt.Printf(" Class: %s\n", profile.Class)
fmt.Printf(" Color Space: %s\n", profile.ColorSpace)
fmt.Println()
if profile.HasMatrix {
fmt.Println("Matrix:")
for i := 0; i < 3; i++ {
fmt.Printf(" %8.4f %8.4f %8.4f\n",
profile.Matrix[i][0], profile.Matrix[i][1], profile.Matrix[i][2])
}
fmt.Println()
}
if profile.HasTRC {
fmt.Println("TRC (Tone Reproduction Curves):")
trcNames := []string{" Red", " Green", " Blue"}
for i, name := range trcNames {
curve := profile.TRC[i]
switch curve.Type {
case icc.CurveIdentity:
fmt.Printf("%-8s identity\n", name)
case icc.CurveParametric:
fmt.Printf("%-8s gamma=%.2f\n", name, curve.Gamma)
case icc.CurveTable:
fmt.Printf("%-8s table (%d entries)\n", name, len(curve.Entries))
}
}
fmt.Println()
}
if profile.HasVCGT && profile.VCGT != nil {
fmt.Printf("VCGT: %d channels, %d entries per channel\n", profile.VCGT.Channels, profile.VCGT.Entries)
fmt.Println()
}
if x, y, ok := profile.WhitePointXY(); ok {
cct := profile.WhitePointCCT()
name := profile.WhitePointName()
switch {
case name != "" && cct > 0:
fmt.Printf("White Point: %s (%.4f, %.4f, %dK)\n", name, x, y, cct)
case cct > 0:
fmt.Printf("White Point: (%.4f, %.4f, %dK)\n", x, y, cct)
default:
fmt.Printf("White Point: (%.4f, %.4f)\n", x, y)
}
} else {
fmt.Printf("White Point: %.4f %.4f %.4f\n",
profile.WhitePoint[0], profile.WhitePoint[1], profile.WhitePoint[2])
}
}
func runICCApply(cmd *cobra.Command, args []string) {
outputName := args[0]
filePath, err := filepath.Abs(args[1])
if err != nil {
log.Fatalf("Failed to resolve path: %v", err)
}
profile, err := icc.ParseFile(filePath)
if err != nil {
log.Fatalf("Invalid ICC profile: %v", err)
}
req := ipc.Request{
Method: "wayland.icc.apply",
Params: map[string]any{
"output": outputName,
"path": filePath,
},
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to apply ICC profile: %v", err)
}
if resp.Error != "" {
log.Fatalf("Server error: %s", resp.Error)
}
fmt.Printf("Applied ICC profile '%s' to output '%s'\n", profile.Description, outputName)
}
func runICCRemove(cmd *cobra.Command, args []string) {
outputName := args[0]
req := ipc.Request{
Method: "wayland.icc.remove",
Params: map[string]any{
"output": outputName,
},
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to remove ICC profile: %v", err)
}
if resp.Error != "" {
log.Fatalf("Server error: %s", resp.Error)
}
fmt.Printf("Removed ICC profile from output '%s'\n", outputName)
}
func runICCSetTemp(cmd *cobra.Command, args []string) {
outputName := args[0]
temp, err := strconv.Atoi(args[1])
if err != nil {
log.Fatalf("Invalid temperature %q: expected a number in kelvin", args[1])
}
if temp != 0 && (temp < 1000 || temp > 10000) {
log.Fatalf("Temperature %d out of range (1000-10000, or 0 to follow the schedule)", temp)
}
req := ipc.Request{
Method: "wayland.icc.setTemp",
Params: map[string]any{
"output": outputName,
"temp": temp,
},
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to set output temperature: %v", err)
}
if resp.Error != "" {
log.Fatalf("Server error: %s", resp.Error)
}
if temp == 0 {
fmt.Printf("Output '%s' follows the night light schedule again\n", outputName)
return
}
fmt.Printf("Set output '%s' color temperature to %dK\n", outputName, temp)
}
// fetchICCOutputTemps asks the running server for the per-output overrides.
// Outputs without an override report 0.
func fetchICCOutputTemps() map[string]int {
temps := make(map[string]int)
resp, err := sendServerRequest(ipc.Request{Method: "wayland.icc.getTemps"})
if err != nil || resp.Result == nil {
return temps
}
resultBytes, err := json.Marshal(*resp.Result)
if err != nil {
return temps
}
var raw map[string]any
if err := json.Unmarshal(resultBytes, &raw); err != nil {
return temps
}
for output, value := range raw {
if f, ok := value.(float64); ok {
temps[output] = int(f)
}
}
return temps
}
// formatOutputTemp renders a per-output override, or the schedule marker.
func formatOutputTemp(temps map[string]int, output string) string {
temp, ok := temps[output]
if !ok || temp == 0 {
return "schedule"
}
return strconv.Itoa(temp) + "K"
}
func runICCStatus(cmd *cobra.Command, args []string) {
req := ipc.Request{
Method: "wayland.icc.getStatus",
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to get ICC status: %v", err)
}
statusMap := make(map[string]map[string]any)
outputs := []string{}
if resp.Result != nil {
if resultBytes, err := json.Marshal(*resp.Result); err == nil {
var result map[string]any
if err := json.Unmarshal(resultBytes, &result); err == nil {
if outputsRaw, ok := result["outputs"].([]any); ok {
for _, o := range outputsRaw {
if s, ok := o.(string); ok {
outputs = append(outputs, s)
}
}
}
if profilesRaw, ok := result["profiles"].(map[string]any); ok {
for k, v := range profilesRaw {
if m, ok := v.(map[string]any); ok {
statusMap[k] = m
}
}
}
}
}
}
temps := fetchICCOutputTemps()
fmt.Println("ICC Profile Status:")
fmt.Println(strings.Repeat("─", 100))
fmt.Printf(" %-20s %-20s %-10s %-10s %-7s %-9s\n", "Output", "Profile", "Version", "ColorSpace", "Active", "Temp")
fmt.Println(" " + strings.Repeat("─", 96))
shown := make(map[string]bool)
for _, output := range outputs {
shown[output] = true
if status, ok := statusMap[output]; ok {
active := false
if a, ok := status["active"].(bool); ok {
active = a
}
desc := "(none)"
if d, ok := status["description"].(string); ok && d != "" {
desc = d
}
version := ""
if v, ok := status["version"].(string); ok {
version = v
}
colorSpace := ""
if c, ok := status["colorSpace"].(string); ok {
colorSpace = c
}
activeStr := "no"
if active {
activeStr = "yes"
}
fmt.Printf(" %-20s %-20s %-10s %-10s %-7s %-9s\n", output, desc, version, colorSpace, activeStr, formatOutputTemp(temps, output))
} else {
fmt.Printf(" %-20s %-20s %-10s %-10s %-7s %-9s\n", output, "(none)", "", "", "no", formatOutputTemp(temps, output))
}
}
// Show any outputs that are in statusMap but not in outputs list
for output, status := range statusMap {
if shown[output] {
continue
}
active := false
if a, ok := status["active"].(bool); ok {
active = a
}
desc := "(none)"
if d, ok := status["description"].(string); ok && d != "" {
desc = d
}
version := ""
if v, ok := status["version"].(string); ok {
version = v
}
colorSpace := ""
if c, ok := status["colorSpace"].(string); ok {
colorSpace = c
}
activeStr := "no"
if active {
activeStr = "yes"
}
fmt.Printf(" %-20s %-20s %-10s %-10s %-7s %-9s\n", output, desc, version, colorSpace, activeStr, formatOutputTemp(temps, output))
}
}
+45 -4
View File
@@ -4,12 +4,16 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"maps"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strconv"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/duxlabs-dev/dux-shell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers" "github.com/duxlabs-dev/dux-shell/core/internal/keybinds/providers"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/duxlabs-dev/dux-shell/core/internal/wayland/keymap"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -66,6 +70,15 @@ var keybindsResetCmd = &cobra.Command{
Run: runKeybindsEdit, Run: runKeybindsEdit,
} }
var keybindsKeymapCmd = &cobra.Command{
Use: "keymap",
Short: "Report which keysym each physical key carries on the first level",
Long: "Print the keysyms the active keyboard layout puts on the first level of every physical key, keyed by xkb keycode.\n\n" +
"Compositors resolve bind keysyms at that level, so a keysym missing from this list cannot be bound however many modifiers are pressed. " +
"The keybind editor uses it to warn instead of storing a bind that never fires.",
Run: runKeybindsKeymap,
}
func init() { func init() {
keybindsCmd.PersistentFlags().String("expected-generation", "", "Configuration generation observed when editing Aqueous bindings") keybindsCmd.PersistentFlags().String("expected-generation", "", "Configuration generation observed when editing Aqueous bindings")
for _, command := range []*cobra.Command{keybindsSetCmd, keybindsRemoveCmd, keybindsResetCmd} { for _, command := range []*cobra.Command{keybindsSetCmd, keybindsRemoveCmd, keybindsResetCmd} {
@@ -79,13 +92,14 @@ func init() {
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat") keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
keybindsSetCmd.Flags().Bool("no-inhibiting", false, "Keep bind active when shortcuts are inhibited (allow-inhibiting=false)") keybindsSetCmd.Flags().Bool("no-inhibiting", false, "Keep bind active when shortcuts are inhibited (allow-inhibiting=false)")
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)") keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)") keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags as letters: e repeat, l locked, r release, o long-press, n non-consuming, m mouse, t transparent, i ignore-mods, u submap-universal, a auto-consuming, x allow-input-capture")
keybindsCmd.AddCommand(keybindsListCmd) keybindsCmd.AddCommand(keybindsListCmd)
keybindsCmd.AddCommand(keybindsShowCmd) keybindsCmd.AddCommand(keybindsShowCmd)
keybindsCmd.AddCommand(keybindsSetCmd) keybindsCmd.AddCommand(keybindsSetCmd)
keybindsCmd.AddCommand(keybindsRemoveCmd) keybindsCmd.AddCommand(keybindsRemoveCmd)
keybindsCmd.AddCommand(keybindsResetCmd) keybindsCmd.AddCommand(keybindsResetCmd)
keybindsCmd.AddCommand(keybindsKeymapCmd)
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) { keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
return providers.NewJSONFileProvider(filePath) return providers.NewJSONFileProvider(filePath)
@@ -163,6 +177,33 @@ func runKeybindsList(cmd *cobra.Command, _ []string) {
} }
} }
// runKeybindsKeymap answers with empty fields rather than an error when there
// is no Wayland seat to ask, so the caller can simply skip the check instead
// of having to tell "unknown layout" apart from "nothing wrong".
func runKeybindsKeymap(_ *cobra.Command, _ []string) {
keysyms := map[string][]string{}
named := []string{}
km, err := keymap.FromSeat()
if err != nil {
log.Warnf("Failed to read the seat keymap: %v", err)
} else {
for code, syms := range km.Level1ByXkbKeycode() {
keysyms[strconv.FormatUint(uint64(code), 10)] = syms
}
// The vocabulary the keysyms above are drawn from, so a caller can
// tell a keysym we could not name from one the layout is missing.
named = slices.Compact(slices.Sorted(maps.Values(keymap.KeysymNames)))
}
output, err := json.Marshal(map[string]any{"keysyms": keysyms, "named": named})
if err != nil {
log.Warnf("Failed to encode keymap: %v", err)
return
}
fmt.Fprintln(os.Stdout, string(output))
}
func makeProviderWithPath(name, path string) keybinds.Provider { func makeProviderWithPath(name, path string) keybinds.Provider {
switch name { switch name {
case "hyprland": case "hyprland":
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers" "github.com/duxlabs-dev/dux-shell/core/internal/keybinds/providers"
) )
func TestKeybindEditFailure(t *testing.T) { func TestKeybindEditFailure(t *testing.T) {
+23 -5
View File
@@ -8,9 +8,9 @@ import (
"os" "os"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-go/ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/duxlabs-dev/dux-shell/core/internal/matugen"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -70,9 +70,13 @@ func init() {
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON") cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal") cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant") cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
cmd.Flags().Int("terminal-opacity", 0, "Terminal background opacity percent, 1-99 (0 or 100 leaves the terminal's own setting)")
cmd.Flags().String("terminal-palette", "", "Terminal palette: default, low, medium or high")
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip") cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)") cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
cmd.Flags().String("source-mode", "", "Source color selection: dominant, colorful, darkness, lightness, saturation, less-saturation, value") cmd.Flags().String("source-mode", "", "Source color selection: dominant, colorful, darkness, lightness, saturation, less-saturation, value")
cmd.Flags().String("seed-color", "", "Hex color to build the palette from instead of extracting one from the wallpaper")
cmd.Flags().String("spec", "", "Material color spec: 2021 (matugen native, default), 2025 (Material 3 Expressive) or dms (2025 accents on neutral surfaces)")
} }
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion") matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
@@ -80,6 +84,7 @@ func init() {
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews") matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
matugenPreviewCmd.Flags().String("image", "", "Wallpaper image used to resolve the scheme-smart preview") matugenPreviewCmd.Flags().String("image", "", "Wallpaper image used to resolve the scheme-smart preview")
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)") matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
matugenPreviewCmd.Flags().String("spec", "", "Material color spec: 2021 (matugen native, default), 2025 (Material 3 Expressive) or dms (2025 accents on neutral surfaces)")
matugenQtengineCmd.Flags().String("config-dir", "", "User config directory") matugenQtengineCmd.Flags().String("config-dir", "", "User config directory")
matugenQtengineCmd.Flags().String("icon-theme", "", "Icon theme name") matugenQtengineCmd.Flags().String("icon-theme", "", "Icon theme name")
} }
@@ -97,9 +102,13 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
stockColors, _ := cmd.Flags().GetString("stock-colors") stockColors, _ := cmd.Flags().GetString("stock-colors")
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal") syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark") terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
terminalOpacity, _ := cmd.Flags().GetInt("terminal-opacity")
terminalPalette, _ := cmd.Flags().GetString("terminal-palette")
skipTemplates, _ := cmd.Flags().GetString("skip-templates") skipTemplates, _ := cmd.Flags().GetString("skip-templates")
contrast, _ := cmd.Flags().GetFloat64("contrast") contrast, _ := cmd.Flags().GetFloat64("contrast")
sourceMode, _ := cmd.Flags().GetString("source-mode") sourceMode, _ := cmd.Flags().GetString("source-mode")
seedColor, _ := cmd.Flags().GetString("seed-color")
spec, _ := cmd.Flags().GetString("spec")
return matugen.Options{ return matugen.Options{
StateDir: stateDir, StateDir: stateDir,
@@ -115,8 +124,12 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
StockColors: stockColors, StockColors: stockColors,
SyncModeWithPortal: syncModeWithPortal, SyncModeWithPortal: syncModeWithPortal,
TerminalsAlwaysDark: terminalsAlwaysDark, TerminalsAlwaysDark: terminalsAlwaysDark,
TerminalOpacity: terminalOpacity,
TerminalPalette: terminalPalette,
SkipTemplates: skipTemplates, SkipTemplates: skipTemplates,
SourceMode: sourceMode, SourceMode: sourceMode,
SeedColor: seedColor,
Spec: spec,
} }
} }
@@ -136,7 +149,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
wait, _ := cmd.Flags().GetBool("wait") wait, _ := cmd.Flags().GetBool("wait")
timeout, _ := cmd.Flags().GetDuration("timeout") timeout, _ := cmd.Flags().GetDuration("timeout")
request := models.Request{ request := ipc.Request{
ID: 1, ID: 1,
Method: "matugen.queue", Method: "matugen.queue",
Params: map[string]any{ Params: map[string]any{
@@ -152,9 +165,13 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
"stockColors": opts.StockColors, "stockColors": opts.StockColors,
"syncModeWithPortal": opts.SyncModeWithPortal, "syncModeWithPortal": opts.SyncModeWithPortal,
"terminalsAlwaysDark": opts.TerminalsAlwaysDark, "terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"terminalOpacity": opts.TerminalOpacity,
"terminalPalette": opts.TerminalPalette,
"skipTemplates": opts.SkipTemplates, "skipTemplates": opts.SkipTemplates,
"contrast": opts.Contrast, "contrast": opts.Contrast,
"sourceMode": opts.SourceMode, "sourceMode": opts.SourceMode,
"seedColor": opts.SeedColor,
"spec": opts.Spec,
"wait": wait, "wait": wait,
}, },
} }
@@ -228,7 +245,8 @@ func runMatugenPreview(cmd *cobra.Command, args []string) {
sourceColor, _ := cmd.Flags().GetString("source-color") sourceColor, _ := cmd.Flags().GetString("source-color")
imagePath, _ := cmd.Flags().GetString("image") imagePath, _ := cmd.Flags().GetString("image")
contrast, _ := cmd.Flags().GetFloat64("contrast") contrast, _ := cmd.Flags().GetFloat64("contrast")
previews, err := matugen.PreviewSchemes(sourceColor, contrast, imagePath) spec, _ := cmd.Flags().GetString("spec")
previews, err := matugen.PreviewSchemes(sourceColor, contrast, imagePath, spec)
if err != nil { if err != nil {
log.Fatalf("Failed to generate Matugen previews: %v", err) log.Fatalf("Failed to generate Matugen previews: %v", err)
} }
-14
View File
@@ -1,14 +0,0 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMatugenQtengineCommandAcceptsConfigDir(t *testing.T) {
flag := matugenQtengineCmd.Flags().Lookup("config-dir")
require.NotNil(t, flag)
assert.Equal(t, "", flag.DefValue)
}
+241 -30
View File
@@ -3,30 +3,50 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify" "github.com/duxlabs-dev/dux-go/ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/duxlabs-dev/dux-shell/core/internal/notify"
"github.com/godbus/dbus/v5"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var ( var (
notifyAppName string notifyAppName string
notifyIcon string notifyAppIcon string
notifyFile string notifyIcon string
notifyTimeout int notifyFile string
notifyTimeout int
notifyUrgency string
notifyCategory string
notifyTransient bool
notifyHints []string
notifyActions []string
notifyPrintID bool
notifyReplaceID uint32
notifyWait bool
notifyIDFd int
notifyActionFd int
) )
var notifyCmd = &cobra.Command{ var notifyCmd = &cobra.Command{
Use: "notify <summary> [body]", Use: "notify <summary> [body]",
Short: "Send a desktop notification", Short: "Send a desktop notification",
Long: `Send a desktop notification with optional actions. Long: `Send a desktop notification. Flags follow notify-send.
If --file is provided, the notification will have "Open" and "Open Folder" actions. If --file is provided, the notification gets "Open" and "Open Folder" actions
that open the file or its directory when clicked.
Examples: Examples:
dms notify "Hello" "World" dms notify "Hello" "World"
dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg --icon smartphone dms notify -u critical -t 0 "Disk almost full"
dms notify "Download complete" --file ~/Downloads/file.zip --app "My App"`, dms notify -a "Backup" -i drive-harddisk -c transfer.complete "Backup finished"
dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg
dms notify -A retry=Retry -A "Dismiss" "Build failed"
dms notify -h string:x-dms-tag:build -h int:value:42 "Hinted"`,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: runNotify, Run: runNotify,
} }
@@ -39,45 +59,236 @@ var genericNotifyActionCmd = &cobra.Command{
}, },
} }
var notifyFlagAliases = map[string]string{
"app": "app-name",
"timeout": "expire-time",
}
func init() { func init() {
notifyCmd.Flags().StringVar(&notifyAppName, "app", "DMS", "Application name") f := notifyCmd.Flags()
notifyCmd.Flags().StringVar(&notifyIcon, "icon", "", "Icon name or path") f.SetNormalizeFunc(func(fs *pflag.FlagSet, name string) pflag.NormalizedName {
notifyCmd.Flags().StringVar(&notifyFile, "file", "", "File path (enables Open/Open Folder actions)") if alias, ok := notifyFlagAliases[name]; ok {
notifyCmd.Flags().IntVar(&notifyTimeout, "timeout", 5000, "Timeout in milliseconds") return pflag.NormalizedName(alias)
}
return pflag.NormalizedName(name)
})
// Frees -h for --hint like notify-send; cobra skips its own help flag when one exists.
f.Bool("help", false, "Show help")
f.StringVarP(&notifyUrgency, "urgency", "u", "", "Urgency level: low, normal, critical")
f.IntVarP(&notifyTimeout, "expire-time", "t", -1, "Timeout in milliseconds (0 never expires, -1 server default)")
f.StringVarP(&notifyAppName, "app-name", "a", "DMS", "Application name")
f.StringVarP(&notifyIcon, "icon", "i", "", "Icon name or path")
f.StringVarP(&notifyAppIcon, "app-icon", "n", "", "Application icon name or path; --icon then becomes the image")
f.StringVar(&notifyCategory, "category", "", "Notification category, comma separated (-c is taken by the global --config)")
f.BoolVarP(&notifyTransient, "transient", "e", false, "Do not keep the notification in history")
f.StringArrayVarP(&notifyHints, "hint", "h", nil, "Extra hint as TYPE:NAME:VALUE (boolean, int, double, string, byte)")
f.BoolVarP(&notifyPrintID, "print-id", "p", false, "Print the notification id")
f.IntVar(&notifyIDFd, "id-fd", -1, "File descriptor to write the notification id to")
f.Uint32VarP(&notifyReplaceID, "replace-id", "r", 0, "Id of the notification to replace")
f.BoolVarP(&notifyWait, "wait", "w", false, "Wait until the notification closes")
f.StringArrayVarP(&notifyActions, "action", "A", nil, "Action as [NAME=]Text; implies --wait, prints the chosen NAME (repeatable)")
f.IntVar(&notifyActionFd, "selected-action-fd", -1, "File descriptor to write the chosen action to")
f.StringVar(&notifyFile, "file", "", "File path (enables Open/Open Folder actions)")
} }
func runNotify(cmd *cobra.Command, args []string) { func runNotify(cmd *cobra.Command, args []string) {
summary := args[0] n, err := buildNotification(args)
body := "" if err != nil {
if len(args) > 1 { fail(err)
body = args[1]
} }
n := notify.Notification{ wait := notifyWait || len(n.Actions) > 0
AppName: notifyAppName, var waiter *notify.Waiter
Icon: notifyIcon, if wait {
Summary: summary, waiter, err = notify.NewWaiter()
Body: body, if err != nil {
FilePath: notifyFile, fail(err)
Timeout: int32(notifyTimeout), }
defer waiter.Close()
} }
id, err := notify.Send(n) id, err := notify.Send(n)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fail(err)
os.Exit(1)
} }
if n.FilePath == "" { reportID(id)
if n.FilePath != "" {
watchNotificationAction(id, n.FilePath)
}
if !wait {
return return
} }
watchNotificationAction(id, n.FilePath) if id == 0 {
return
}
res := waiter.Wait(id, waitTimeout())
if res.TimedOut {
fmt.Fprintln(os.Stderr, "Wait timeout expired")
return
}
if !res.Invoked {
return
}
fmt.Println(res.Action)
writeFd(notifyActionFd, res.Action)
}
func buildNotification(args []string) (notify.Notification, error) {
n := notify.Notification{
AppName: notifyAppName,
AppIcon: notifyAppIcon,
Icon: notifyIcon,
Summary: args[0],
FilePath: notifyFile,
Timeout: int32(notifyTimeout),
ReplaceID: notifyReplaceID,
Hints: map[string]dbus.Variant{},
}
if len(args) > 1 {
n.Body = args[1]
}
if notifyFile != "" && len(notifyActions) > 0 {
return n, fmt.Errorf("--file and --action cannot be combined")
}
if notifyUrgency != "" {
level, err := parseUrgency(notifyUrgency)
if err != nil {
return n, err
}
n.Hints["urgency"] = dbus.MakeVariant(level)
}
if notifyCategory != "" {
n.Hints["category"] = dbus.MakeVariant(notifyCategory)
}
if notifyTransient {
n.Hints["transient"] = dbus.MakeVariant(true)
}
for _, spec := range notifyHints {
name, value, err := parseHint(spec)
if err != nil {
return n, err
}
n.Hints[name] = value
}
n.Actions = parseActions(notifyActions)
return n, nil
}
func parseUrgency(s string) (byte, error) {
switch strings.ToLower(s) {
case "low", "0":
return 0, nil
case "normal", "1":
return 1, nil
case "critical", "2":
return 2, nil
}
return 0, fmt.Errorf("invalid urgency %q: use low, normal or critical", s)
}
func parseHint(spec string) (string, dbus.Variant, error) {
parts := strings.SplitN(spec, ":", 3)
if len(parts) != 3 || parts[1] == "" {
return "", dbus.Variant{}, fmt.Errorf("invalid hint %q: expected TYPE:NAME:VALUE", spec)
}
typ, name, value := strings.ToLower(parts[0]), parts[1], parts[2]
switch typ {
case "string":
return name, dbus.MakeVariant(value), nil
case "int":
v, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return "", dbus.Variant{}, fmt.Errorf("hint %s: invalid int %q", name, value)
}
return name, dbus.MakeVariant(int32(v)), nil
case "double":
v, err := strconv.ParseFloat(value, 64)
if err != nil {
return "", dbus.Variant{}, fmt.Errorf("hint %s: invalid double %q", name, value)
}
return name, dbus.MakeVariant(v), nil
case "byte":
v, err := strconv.ParseUint(value, 0, 8)
if err != nil {
return "", dbus.Variant{}, fmt.Errorf("hint %s: invalid byte %q", name, value)
}
return name, dbus.MakeVariant(byte(v)), nil
case "boolean":
v, err := parseBoolean(value)
if err != nil {
return "", dbus.Variant{}, fmt.Errorf("hint %s: invalid boolean %q", name, value)
}
return name, dbus.MakeVariant(v), nil
}
return "", dbus.Variant{}, fmt.Errorf("invalid hint type %q: use boolean, int, double, string or byte", parts[0])
}
func parseBoolean(s string) (bool, error) {
switch strings.ToLower(s) {
case "true":
return true, nil
case "false":
return false, nil
}
v, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return false, err
}
return v != 0, nil
}
func parseActions(specs []string) []string {
if len(specs) == 0 {
return nil
}
actions := make([]string, 0, len(specs)*2)
for i, spec := range specs {
name, label, hasName := strings.Cut(spec, "=")
if !hasName {
name, label = strconv.Itoa(i), spec
}
actions = append(actions, name, label)
}
return actions
}
func waitTimeout() time.Duration {
if notifyTimeout <= 0 {
return 0
}
return time.Duration(notifyTimeout) * time.Millisecond
}
func reportID(id uint32) {
if notifyPrintID {
fmt.Println(id)
}
writeFd(notifyIDFd, strconv.FormatUint(uint64(id), 10))
}
func writeFd(fd int, line string) {
if fd < 0 {
return
}
f := os.NewFile(uintptr(fd), "")
if f == nil {
return
}
fmt.Fprintln(f, line)
_ = f.Sync()
}
func fail(err error) {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
func watchNotificationAction(id uint32, path string) { func watchNotificationAction(id uint32, path string) {
if id == 0 { if id == 0 {
return return
} }
resp, ok := tryServerRequest(models.Request{ resp, ok := tryServerRequest(ipc.Request{
Method: "notify.watchAction", Method: "notify.watchAction",
Params: map[string]any{"id": id, "path": path}, Params: map[string]any{"id": id, "path": path},
}) })
+3 -3
View File
@@ -8,8 +8,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-go/ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -123,7 +123,7 @@ func runOpen(target string) {
params["url"] = target params["url"] = target
} }
req := models.Request{ req := ipc.Request{
ID: 1, ID: 1,
Method: method, Method: method,
Params: params, Params: params,
+4 -4
View File
@@ -6,9 +6,9 @@ import (
"os" "os"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/duxlabs-dev/dux-go/ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/qrcode" "github.com/duxlabs-dev/dux-shell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/duxlabs-dev/dux-shell/core/internal/qrcode"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -243,7 +243,7 @@ func fatalf(format string, args ...any) {
} }
func fetchWifiQRContent(ssid string) (string, error) { func fetchWifiQRContent(ssid string) (string, error) {
resp, err := sendServerRequest(models.Request{ resp, err := sendServerRequest(ipc.Request{
ID: 1, ID: 1,
Method: "network.qrcode-content", Method: "network.qrcode-content",
Params: map[string]any{"ssid": ssid}, Params: map[string]any{"ssid": ssid},
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+3 -3
View File
@@ -3,8 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/registries" "github.com/duxlabs-dev/dux-shell/core/internal/registries"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -12,7 +12,7 @@ import (
var registryCmd = &cobra.Command{ var registryCmd = &cobra.Command{
Use: "registry", Use: "registry",
Short: "Manage plugin and theme registries", Short: "Manage plugin and theme registries",
Long: "Manage the registries DMS fetches plugins and themes from. The official registry is always active; additional registries can be added by name and git URL.", Long: "Manage the registries DMS fetches plugins and themes from. The official registry is always active and additional registries can be added by name and git URL, unless the CLI policy sets disable_registries.",
} }
var registryListCmd = &cobra.Command{ var registryListCmd = &cobra.Command{
+1 -1
View File
@@ -7,7 +7,7 @@ import (
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "dms", Use: "dms",
Short: "dms CLI", Short: "dms CLI",
Long: "dms is the DankMaterialShell management CLI and backend server.", Long: "dms is the Dux Shell management CLI and backend server.",
} }
func init() { func init() {
+115 -75
View File
@@ -9,12 +9,12 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime/debug" "runtime/debug"
"strconv"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/duxlabs-dev/dux-shell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify" "github.com/duxlabs-dev/dux-shell/core/internal/notify"
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot" "github.com/duxlabs-dev/dux-shell/core/internal/qsipc"
"github.com/duxlabs-dev/dux-shell/core/internal/screenshot"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -35,6 +35,7 @@ var (
ssJSON bool ssJSON bool
ssGeometry bool ssGeometry bool
ssAllowMulti bool ssAllowMulti bool
ssHUD string
) )
type screenshotMetadata struct { type screenshotMetadata struct {
@@ -181,6 +182,7 @@ func init() {
screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON") screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON")
screenshotCmd.PersistentFlags().BoolVarP(&ssGeometry, "geometry", "g", false, "Print selected region geometry (X,Y WxH) to stdout without capturing an image") screenshotCmd.PersistentFlags().BoolVarP(&ssGeometry, "geometry", "g", false, "Print selected region geometry (X,Y WxH) to stdout without capturing an image")
screenshotCmd.PersistentFlags().BoolVar(&ssAllowMulti, "allow-multiple", false, "Open a selector even when another one is already open") screenshotCmd.PersistentFlags().BoolVar(&ssAllowMulti, "allow-multiple", false, "Open a selector even when another one is already open")
screenshotCmd.PersistentFlags().StringVar(&ssHUD, "hud", "auto", "HUD overlay scale in region selector (auto, off/0, or integer scale 1-4)")
ssScrollCmd.Flags().IntVar(&ssScrollInterval, "interval", 45, "Capture interval in milliseconds (30-1000)") ssScrollCmd.Flags().IntVar(&ssScrollInterval, "interval", 45, "Capture interval in milliseconds (30-1000)")
@@ -212,6 +214,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config.Stdout = ssStdout config.Stdout = ssStdout
config.Geometry = ssGeometry config.Geometry = ssGeometry
config.AllowMultiple = ssAllowMulti config.AllowMultiple = ssAllowMulti
config.HUD = ssHUD
if ssGeometry { if ssGeometry {
config.Clipboard = false config.Clipboard = false
@@ -243,9 +246,29 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
} }
config.Quality = ssQuality config.Quality = ssQuality
if mode == screenshot.ModeRegion || mode == screenshot.ModeScroll {
config.SnapTargets = fetchShellSnapTargets()
}
return config return config
} }
func fetchShellSnapTargets() []screenshot.SnapTarget {
pid, ok := shellApp.SessionPID()
if !ok {
return nil
}
res, isVoid, err := qsipc.Call(qsipc.SocketPathForPID(pid), "screenshot", "getSurfaces", nil)
if err != nil || isVoid || res == "" {
return nil
}
var targets []screenshot.SnapTarget
if err := json.Unmarshal([]byte(res), &targets); err != nil {
return nil
}
return targets
}
// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select. // setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select.
// Best-effort and not awaited: qs takes ~20ms to come up, longer than the selector needs to show. // Best-effort and not awaited: qs takes ~20ms to come up, longer than the selector needs to show.
func setPopoutScreenshotMode(begin bool) { func setPopoutScreenshotMode(begin bool) {
@@ -253,18 +276,18 @@ func setPopoutScreenshotMode(begin bool) {
if begin { if begin {
fn = "begin" fn = "begin"
} }
cmdArgs := []string{"ipc"}
if pid, ok := shellApp.SessionPID(); ok { if pid, ok := shellApp.SessionPID(); ok {
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid)) _, _, _ = qsipc.Call(qsipc.SocketPathForPID(pid), "screenshot", fn, nil)
} else { return
if err := shellApp.ResolveConfig(nil, nil); err != nil {
return
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
} }
cmdArgs := []string{"ipc"}
if err := shellApp.ResolveConfig(nil, nil); err != nil {
return
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
cmdArgs = append(cmdArgs, "call", "screenshot", fn) cmdArgs = append(cmdArgs, "call", "screenshot", fn)
_ = exec.Command("qs", cmdArgs...).Start() _ = exec.Command("qs", cmdArgs...).Start()
} }
@@ -355,6 +378,12 @@ func runScreenshot(config screenshot.Config) {
} }
} }
if result.CopyOnly {
config.SaveFile = false
config.Clipboard = true
config.Stdout = false
}
if config.Stdout { if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil { if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil {
exitScreenshotError(" writing to stdout", err) exitScreenshotError(" writing to stdout", err)
@@ -385,31 +414,48 @@ func runScreenshot(config screenshot.Config) {
} }
if config.Clipboard { if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil { var err error
if filePath != "" && config.Format != screenshot.FormatPPM {
err = copyFileToClipboard(filePath)
} else {
err = copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format, result.CICP)
}
if err != nil {
exitScreenshotError(" copying to clipboard", err) exitScreenshotError(" copying to clipboard", err)
} }
if !ssJSON && !config.SaveFile { if !ssJSON && !config.SaveFile {
fmt.Println("Copied to clipboard") if ssStdout {
fmt.Fprintln(os.Stderr, "Copied to clipboard")
} else {
fmt.Println("Copied to clipboard")
}
} }
} }
if ssJSON { if ssJSON {
scale := result.Scale if result.CopyOnly {
if scale <= 0 { writeScreenshotJSON(screenshotMetadata{
scale = 1.0 Status: "aborted",
Error: "User copied to clipboard",
})
} else {
scale := result.Scale
if scale <= 0 {
scale = 1.0
}
writeScreenshotJSON(screenshotMetadata{
Status: "success",
Path: filePath,
Width: result.Buffer.Width,
Height: result.Buffer.Height,
Scale: scale,
Mime: formatMime(config.Format),
})
} }
writeScreenshotJSON(screenshotMetadata{
Status: "success",
Path: filePath,
Width: result.Buffer.Width,
Height: result.Buffer.Height,
Scale: scale,
Mime: formatMime(config.Format),
})
} }
if config.Notify { if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format) thumbData, thumbW, thumbH := bufferToRGBAThumbnail(result.Buffer, 640, result.Format)
id := screenshot.SendNotification(screenshot.NotifyResult{ id := screenshot.SendNotification(screenshot.NotifyResult{
FilePath: filePath, FilePath: filePath,
Clipboard: config.Clipboard, Clipboard: config.Clipboard,
@@ -450,28 +496,21 @@ func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, qua
} }
} }
func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) { func bufferToRGBAThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) {
srcW, srcH := buf.Width, buf.Height if buf == nil || buf.Width <= 0 || buf.Height <= 0 || maxSize <= 0 {
scale := 1.0 return nil, 0, 0
if srcW > maxSize || srcH > maxSize {
if srcW > srcH {
scale = float64(maxSize) / float64(srcW)
} else {
scale = float64(maxSize) / float64(srcH)
}
} }
dstW := int(float64(srcW) * scale) srcW, srcH := buf.Width, buf.Height
dstH := int(float64(srcH) * scale) longest := max(srcW, srcH)
if dstW < 1 { dstW, dstH := srcW, srcH
dstW = 1 if longest > maxSize {
} dstW = max(1, srcW*maxSize/longest)
if dstH < 1 { dstH = max(1, srcH*maxSize/longest)
dstH = 1
} }
data := buf.Data() data := buf.Data()
rgb := make([]byte, dstW*dstH*3) rgba := make([]byte, dstW*dstH*4)
is10Bit := screenshot.PixelFormat(pixelFormat).Is10Bit() is10Bit := screenshot.PixelFormat(pixelFormat).Is10Bit()
@@ -484,41 +523,42 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
swapRB = true swapRB = true
} }
for y := 0; y < dstH; y++ { for y := range dstH {
srcY := int(float64(y) / scale) y0, y1 := y*srcH/dstH, (y+1)*srcH/dstH
if srcY >= srcH { for x := range dstW {
srcY = srcH - 1 x0, x1 := x*srcW/dstW, (x+1)*srcW/dstW
} var r, g, b, samples uint64
for x := 0; x < dstW; x++ { for srcY := y0; srcY < y1; srcY++ {
srcX := int(float64(x) / scale) for srcX := x0; srcX < x1; srcX++ {
if srcX >= srcW { si := srcY*buf.Stride + srcX*4
srcX = srcW - 1 if si+3 >= len(data) {
continue
}
c0, c1, c2 := data[si], data[si+1], data[si+2]
if is10Bit {
v := binary.LittleEndian.Uint32(data[si:])
c0, c1, c2 = uint8(v>>2), uint8(v>>12), uint8(v>>22)
}
if swapRB {
c0, c2 = c2, c0
}
r += uint64(c0)
g += uint64(c1)
b += uint64(c2)
samples++
}
} }
si := srcY*buf.Stride + srcX*4 if samples == 0 {
di := (y*dstW + x) * 3
if si+3 >= len(data) {
continue continue
} }
switch { di := (y*dstW + x) * 4
case is10Bit: rgba[di+0] = byte((r + samples/2) / samples)
v := binary.LittleEndian.Uint32(data[si:]) rgba[di+1] = byte((g + samples/2) / samples)
c0, c1, c2 := uint8(v>>2), uint8(v>>12), uint8(v>>22) rgba[di+2] = byte((b + samples/2) / samples)
if swapRB { rgba[di+3] = 255
c0, c2 = c2, c0
}
rgb[di+0], rgb[di+1], rgb[di+2] = c0, c1, c2
case swapRB:
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
default:
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
}
} }
} }
return rgb, dstW, dstH return rgba, dstW, dstH
} }
func runScreenshotRegion(cmd *cobra.Command, args []string) { func runScreenshotRegion(cmd *cobra.Command, args []string) {
+147
View File
@@ -0,0 +1,147 @@
package main
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
"github.com/duxlabs-dev/dux-shell/core/internal/screenshot"
)
func thumbnailTestBuffer(t testing.TB, width, height int) *screenshot.ShmBuffer {
t.Helper()
buf, err := screenshot.CreateShmBuffer(width, height, width*4+16)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { buf.Close() })
return buf
}
func TestBufferToRGBAThumbnailPixels(t *testing.T) {
formats := []struct {
name string
format screenshot.PixelFormat
swapRB bool
tenBit bool
}{
{"argb", screenshot.FormatARGB8888, true, false},
{"xrgb", screenshot.FormatXRGB8888, true, false},
{"abgr", screenshot.FormatABGR8888, false, false},
{"xbgr", screenshot.FormatXBGR8888, false, false},
{"argb10", screenshot.FormatARGB2101010, true, true},
{"xrgb10", screenshot.FormatXRGB2101010, true, true},
{"abgr10", screenshot.FormatABGR2101010, false, true},
{"xbgr10", screenshot.FormatXBGR2101010, false, true},
}
for _, tc := range formats {
t.Run(tc.name, func(t *testing.T) {
buf := thumbnailTestBuffer(t, 4, 2)
data := buf.Data()
var originalRGBA []byte
for y := range buf.Height {
for x := range buf.Width {
r, g, b := byte(20), byte(40), byte(60)
if (x+y)%2 != 0 {
r, g, b = 100, 160, 220
}
originalRGBA = append(originalRGBA, r, g, b, 255)
if tc.swapRB {
r, b = b, r
}
i := y*buf.Stride + x*4
if tc.tenBit {
binary.LittleEndian.PutUint32(data[i:], uint32(r)<<2|uint32(g)<<12|uint32(b)<<22)
continue
}
data[i], data[i+1], data[i+2] = r, g, b
}
}
original := bytes.Clone(data)
got, width, height := bufferToRGBAThumbnail(buf, 2, uint32(tc.format))
want := []byte{60, 100, 140, 255, 60, 100, 140, 255}
if width != 2 || height != 1 || !bytes.Equal(got, want) {
t.Fatalf("averaged thumbnail = %dx%d %v, want 2x1 %v", width, height, got, want)
}
got, width, height = bufferToRGBAThumbnail(buf, 640, uint32(tc.format))
if width != 4 || height != 2 || !bytes.Equal(got, originalRGBA) {
t.Fatalf("unscaled thumbnail = %dx%d %v, want 4x2 %v", width, height, got, originalRGBA)
}
if !bytes.Equal(data, original) {
t.Fatal("source pixels changed")
}
})
}
}
func TestBufferToRGBAThumbnailDimensions(t *testing.T) {
for _, tc := range []struct {
width, height int
wantWidth, wantHeight int
}{
{1920, 1080, 640, 360},
{1080, 1920, 360, 640},
{1365, 767, 640, 359},
{1, 10000, 1, 640},
{10000, 1, 640, 1},
{1, 1, 1, 1},
} {
t.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func(t *testing.T) {
buf := thumbnailTestBuffer(t, tc.width, tc.height)
got, width, height := bufferToRGBAThumbnail(buf, 640, uint32(screenshot.FormatXRGB8888))
if width != tc.wantWidth || height != tc.wantHeight || len(got) != width*height*4 {
t.Fatalf("thumbnail = %dx%d, %d bytes; want %dx%d RGBA", width, height, len(got), tc.wantWidth, tc.wantHeight)
}
})
}
}
func TestBufferToRGBAThumbnailOddWidthRows(t *testing.T) {
for _, width := range []int{319, 321, 361} {
t.Run(fmt.Sprint(width), func(t *testing.T) {
buf := thumbnailTestBuffer(t, width, 64)
data := buf.Data()
for y := range buf.Height {
for x := range width {
i := y*buf.Stride + x*4
data[i], data[i+1], data[i+2] = byte(y), byte(x>>8), byte(x)
}
}
got, gotWidth, height := bufferToRGBAThumbnail(buf, 640, uint32(screenshot.FormatXRGB8888))
if gotWidth != width || height != buf.Height || len(got) != width*height*4 {
t.Fatalf("thumbnail = %dx%d, %d bytes; want %dx%d RGBA", gotWidth, height, len(got), width, buf.Height)
}
for y := range height {
for x := range width {
i := y*width*4 + x*4
want := []byte{byte(x), byte(x >> 8), byte(y), 255}
if !bytes.Equal(got[i:i+4], want) {
t.Fatalf("pixel (%d, %d) = %v, want %v", x, y, got[i:i+4], want)
}
}
}
})
}
}
func TestBufferToRGBAThumbnailEmpty(t *testing.T) {
for _, buf := range []*screenshot.ShmBuffer{nil, {}} {
data, width, height := bufferToRGBAThumbnail(buf, 640, uint32(screenshot.FormatXRGB8888))
if data != nil || width != 0 || height != 0 {
t.Fatalf("empty thumbnail = %dx%d %v", width, height, data)
}
}
}
func BenchmarkBufferToRGBAThumbnail(b *testing.B) {
for _, size := range [][2]int{{3840, 2160}, {3840, 3840}, {1920, 10000}} {
b.Run(fmt.Sprintf("%dx%d", size[0], size[1]), func(b *testing.B) {
buf := thumbnailTestBuffer(b, size[0], size[1])
b.ReportAllocs()
for b.Loop() {
bufferToRGBAThumbnail(buf, 640, uint32(screenshot.FormatXRGB8888))
}
})
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+76 -16
View File
@@ -8,12 +8,13 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/duxlabs-dev/dux-shell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/duxlabs-dev/dux-shell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/duxlabs-dev/dux-shell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/duxlabs-dev/dux-shell/core/internal/mangoconf"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/duxlabs-dev/dux-shell/core/internal/privesc"
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -129,7 +130,7 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
mangoFile: "layout.conf", mangoFile: "layout.conf",
niriContent: func(_ string) string { return config.NiriLayoutConfig }, niriContent: func(_ string) string { return config.NiriLayoutConfig },
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig }, hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
mangoContent: func(_ string) string { return config.MangoLayoutConfig }, mangoContent: func(_ string) string { return mangoconf.Detect().Translate(config.MangoLayoutConfig) },
}, },
"colors": { "colors": {
niriFile: "colors.kdl", niriFile: "colors.kdl",
@@ -137,15 +138,19 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
mangoFile: "colors.conf", mangoFile: "colors.conf",
niriContent: func(_ string) string { return config.NiriColorsConfig }, niriContent: func(_ string) string { return config.NiriColorsConfig },
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig }, hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
mangoContent: func(_ string) string { return config.MangoColorsConfig }, mangoContent: func(_ string) string { return mangoconf.Detect().Translate(config.MangoColorsConfig) },
}, },
"alttab": { "alttab": {
niriFile: "alttab.kdl", niriFile: "alttab.kdl",
niriContent: func(_ string) string { return config.NiriAlttabConfig }, niriContent: func(_ string) string { return config.NiriAlttabConfig },
}, },
"input": { "input": {
niriFile: "input.kdl", niriFile: "input.kdl",
niriContent: func(_ string) string { return config.NiriInputConfig }, hyprFile: "input.lua",
mangoFile: "input.conf",
niriContent: func(_ string) string { return config.NiriInputConfig },
hyprContent: func(_ string) string { return config.DMSInputLuaConfig },
mangoContent: func(_ string) string { return "" },
}, },
"outputs": { "outputs": {
niriFile: "outputs.kdl", niriFile: "outputs.kdl",
@@ -173,15 +178,43 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
}, },
} }
func detectTerminal() (string, error) { var knownTerminals = []string{"ghostty", "foot", "kitty", "alacritty"}
terminals := []string{"ghostty", "foot", "kitty", "alacritty"}
func terminalFromEnv() string {
return strings.TrimSpace(os.Getenv("TERMINAL"))
}
func installedTerminals() []string {
var found []string var found []string
for _, t := range terminals { for _, t := range knownTerminals {
if utils.CommandExists(t) { if utils.CommandExists(t) {
found = append(found, t) found = append(found, t)
} }
} }
return found
}
func installedHyprlandVersion() string {
_, ver := getVersionFromCommand("Hyprland", "--version", distros.ParseHyprlandVersion)
return ver
}
func defaultTerminalCommand() string {
if env := terminalFromEnv(); env != "" {
return env
}
if found := installedTerminals(); len(found) > 0 {
return found[0]
}
return "ghostty"
}
func detectTerminal() (string, error) {
if env := terminalFromEnv(); env != "" {
return env, nil
}
found := installedTerminals()
switch len(found) { switch len(found) {
case 0: case 0:
return "ghostty", nil return "ghostty", nil
@@ -276,7 +309,7 @@ func runSetupDmsConfig(name string) error {
case "hyprland": case "hyprland":
dmsDir = filepath.Join(utils.XDGConfigHome(), "hypr", "dms") dmsDir = filepath.Join(utils.XDGConfigHome(), "hypr", "dms")
case "mango", "mangowc": case "mango", "mangowc":
dmsDir = filepath.Join(utils.XDGConfigHome(), "mango", "dms") dmsDir = filepath.Join(mangoconf.Dir(), "dms")
} }
if err := os.MkdirAll(dmsDir, 0o755); err != nil { if err := os.MkdirAll(dmsDir, 0o755); err != nil {
@@ -314,8 +347,9 @@ func runSetup() error {
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := true useSystemd := true
if wmSelected { if wmSelected {
if wm == deps.WindowManagerMango { if wm == deps.WindowManagerMango && !mangoconf.SessionTargetInstalled() {
useSystemd = false useSystemd = false
fmt.Println("\nThis Mango has no mango-session.target (added in 0.17.1); DMS will start from the Mango config.")
} else if isVoidSetup() { } else if isVoidSetup() {
useSystemd = false useSystemd = false
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.") fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
@@ -329,6 +363,13 @@ func runSetup() error {
return nil return nil
} }
replaceBinds := false
if wmSelected {
if path, ok := config.ExistingBinds(wm); ok {
replaceBinds = promptBinds(path)
}
}
if wmSelected || terminalSelected { if wmSelected || terminalSelected {
willBackup := checkExistingConfigs(wm, wmSelected, terminal, terminalSelected) willBackup := checkExistingConfigs(wm, wmSelected, terminal, terminalSelected)
if willBackup { if willBackup {
@@ -349,6 +390,7 @@ func runSetup() error {
fmt.Println("\nDeploying configurations...") fmt.Println("\nDeploying configurations...")
logChan := make(chan string, 100) logChan := make(chan string, 100)
deployer := config.NewConfigDeployer(logChan) deployer := config.NewConfigDeployer(logChan)
deployer.SetReplaceBinds(replaceBinds)
go func() { go func() {
for msg := range logChan { for msg := range logChan {
@@ -360,8 +402,15 @@ func runSetup() error {
var err error var err error
if wmSelected { if wmSelected {
terminalCommand := defaultTerminalCommand()
if terminalSelected {
terminalCommand = terminal.Command()
}
if wm == deps.WindowManagerHyprland {
deployer.SetHyprlandVersion(installedHyprlandVersion())
}
var result config.DeploymentResult var result config.DeploymentResult
result, err = deployer.DeployCompositor(wm, terminal, useSystemd) result, err = deployer.DeployCompositor(wm, terminalCommand, useSystemd)
results = append(results, result) results = append(results, result)
} }
if err == nil && terminalSelected { if err == nil && terminalSelected {
@@ -477,6 +526,17 @@ func promptTerminal() (deps.Terminal, bool) {
} }
} }
func promptBinds(path string) bool {
fmt.Printf("\nFound your keybinds in %s.\n", path)
fmt.Println(" 1) Keep my binds as they are")
fmt.Println(" 2) Start from the DMS stock binds")
fmt.Println("Either way a timestamped backup is kept.")
fmt.Print("Choice (1-2) [1]: ")
var response string
fmt.Scanln(&response)
return strings.TrimSpace(response) == "2"
}
func promptSystemd() bool { func promptSystemd() bool {
fmt.Println("\nUse systemd for session management?") fmt.Println("\nUse systemd for session management?")
fmt.Println("1) Yes (recommended for most distros)") fmt.Println("1) Yes (recommended for most distros)")
+17 -7
View File
@@ -3,8 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/duxlabs-dev/dux-shell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/duxlabs-dev/dux-shell/core/internal/deps"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -19,22 +19,24 @@ var setupHeadlessCmd = &cobra.Command{
noSystemd, _ := cmd.Flags().GetBool("no-systemd") noSystemd, _ := cmd.Flags().GetBool("no-systemd")
force, _ := cmd.Flags().GetBool("force") force, _ := cmd.Flags().GetBool("force")
skipExisting, _ := cmd.Flags().GetBool("skip-existing") skipExisting, _ := cmd.Flags().GetBool("skip-existing")
stockBinds, _ := cmd.Flags().GetBool("stock-binds")
cmd.SilenceUsage = true cmd.SilenceUsage = true
return runSetupHeadless(compositor, terminal, noSystemd, force, skipExisting) return runSetupHeadless(compositor, terminal, noSystemd, force, skipExisting, stockBinds)
}, },
} }
func init() { func init() {
setupHeadlessCmd.Flags().String("compositor", "", "Compositor to configure: niri, hyprland, or mango") setupHeadlessCmd.Flags().String("compositor", "", "Compositor to configure: niri, hyprland, or mango")
setupHeadlessCmd.Flags().String("terminal", "", "Also deploy a terminal config: ghostty, kitty, or alacritty") setupHeadlessCmd.Flags().String("terminal", "", "Also deploy a terminal config: ghostty, kitty, or alacritty (binds otherwise use $TERMINAL or the first installed terminal)")
setupHeadlessCmd.Flags().Bool("no-systemd", false, "Deploy session config without systemd integration") setupHeadlessCmd.Flags().Bool("no-systemd", false, "Deploy session config without systemd integration")
setupHeadlessCmd.Flags().Bool("force", false, "Overwrite existing configs (timestamped backups are created)") setupHeadlessCmd.Flags().Bool("force", false, "Overwrite existing configs (timestamped backups are created)")
setupHeadlessCmd.Flags().Bool("skip-existing", false, "Warn and skip instead of failing when a config already exists") setupHeadlessCmd.Flags().Bool("skip-existing", false, "Warn and skip instead of failing when a config already exists")
setupHeadlessCmd.Flags().Bool("stock-binds", false, "Replace existing keybinds with the DMS stock set (a backup is kept)")
_ = setupHeadlessCmd.MarkFlagRequired("compositor") _ = setupHeadlessCmd.MarkFlagRequired("compositor")
setupHeadlessCmd.MarkFlagsMutuallyExclusive("force", "skip-existing") setupHeadlessCmd.MarkFlagsMutuallyExclusive("force", "skip-existing")
} }
func runSetupHeadless(compositor, terminal string, noSystemd, force, skipExisting bool) error { func runSetupHeadless(compositor, terminal string, noSystemd, force, skipExisting, stockBinds bool) error {
wm, err := parseHeadlessCompositor(compositor) wm, err := parseHeadlessCompositor(compositor)
if err != nil { if err != nil {
return err return err
@@ -79,11 +81,19 @@ func runSetupHeadless(compositor, terminal string, noSystemd, force, skipExistin
close(logDone) close(logDone)
}() }()
deployer := config.NewConfigDeployer(logChan) deployer := config.NewConfigDeployer(logChan)
deployer.SetReplaceBinds(stockBinds)
var results []config.DeploymentResult var results []config.DeploymentResult
var deployErr error var deployErr error
if deployCompositor { if deployCompositor {
result, err := deployer.DeployCompositor(wm, term, headlessUseSystemd(wm, noSystemd)) terminalCommand := defaultTerminalCommand()
if termSelected {
terminalCommand = term.Command()
}
if wm == deps.WindowManagerHyprland {
deployer.SetHyprlandVersion(installedHyprlandVersion())
}
result, err := deployer.DeployCompositor(wm, terminalCommand, headlessUseSystemd(wm, noSystemd))
results = append(results, result) results = append(results, result)
deployErr = err deployErr = err
} }
@@ -141,7 +151,7 @@ func parseHeadlessTerminal(name string) (deps.Terminal, bool, error) {
} }
func headlessUseSystemd(wm deps.WindowManager, noSystemd bool) bool { func headlessUseSystemd(wm deps.WindowManager, noSystemd bool) bool {
if noSystemd || wm == deps.WindowManagerMango { if noSystemd {
return false return false
} }
return !isVoidSetup() return !isVoidSetup()
+282
View File
@@ -0,0 +1,282 @@
package main
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"time"
"github.com/duxlabs-dev/dux-shell/core/internal/backup"
"github.com/duxlabs-dev/dux-shell/core/internal/deps"
"github.com/duxlabs-dev/dux-shell/core/internal/distros"
"github.com/duxlabs-dev/dux-shell/core/internal/privesc"
"github.com/spf13/cobra"
)
const switchDMSDep = "dms (Dux Shell)"
// The greeter runs the shell's QML, so it moves with dms or the login screen can break.
var switchFamily = []string{switchDMSDep, "quickshell", "dms-greeter"}
var switchAliases = map[string]string{
"dms": switchDMSDep,
"quickshell": "quickshell",
"dms-greeter": "dms-greeter",
"greeter": "dms-greeter",
"dankcalendar": "dankcalendar",
"danksearch": "danksearch",
}
var switchAssumeYes bool
var switchCmd = &cobra.Command{
Use: "switch <stable|beta> [package...]",
Short: "Switch DMS packages between stable and beta builds",
Long: `Switch installed DMS packages between stable releases and beta builds.
With no packages, dms, quickshell and dms-greeter switch together. Name packages
(dms, quickshell, greeter, dankcalendar, danksearch) to switch only those.
Settings, plugins, themes and session state are backed up before anything changes.
Settings written by beta can lose options on stable, so switching back to stable
offers to restore the backup taken the last time you left stable.`,
Args: cobra.MinimumNArgs(1),
ValidArgs: []string{"stable", "beta"},
Run: func(cmd *cobra.Command, args []string) {
if err := runSwitch(args[0], args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
func init() {
switchCmd.Flags().BoolVarP(&switchAssumeYes, "yes", "y", false, "Do not ask for confirmation; keeps current settings when switching to stable")
}
func runSwitch(channel string, packages []string) error {
var target deps.PackageVariant
switch channel {
case "stable":
target = deps.VariantStable
case "beta":
target = deps.VariantGit
default:
return fmt.Errorf("unknown channel %q: use stable or beta", channel)
}
explicit := len(packages) > 0
names := switchFamily
if explicit {
names = nil
for _, pkg := range packages {
name, ok := switchAliases[strings.ToLower(pkg)]
if !ok {
return fmt.Errorf("unknown package %q", pkg)
}
names = append(names, name)
}
}
osInfo, err := distros.GetOSInfo()
if err != nil {
return fmt.Errorf("detecting distribution: %w", err)
}
distro, err := distros.NewDistribution(osInfo.Distribution.ID, nil)
if err != nil {
return err
}
// The package names are compositor independent, so any window manager works here.
var wm deps.WindowManager
var swaps []distros.VariantSwap
for _, name := range names {
swap, status := distros.PlanVariantSwap(distro, wm, name, target)
switch status {
case distros.SwapNeeded:
swaps = append(swaps, swap)
case distros.SwapUnsupported:
return fmt.Errorf("switching packages is not supported on %s", osInfo.PrettyName)
case distros.SwapAlreadyOnTarget:
fmt.Printf("%s is already on %s\n", name, channel)
case distros.SwapNoVariant:
if explicit {
fmt.Printf("%s has no separate beta build on %s\n", name, osInfo.PrettyName)
}
case distros.SwapNotInstalled:
if explicit {
fmt.Printf("%s is not installed from a package\n", name)
}
}
}
if len(swaps) == 0 {
fmt.Println("Nothing to switch.")
return nil
}
shellChanges := false
snapshotLabel := ""
fmt.Printf("\nSwitching to %s:\n", channel)
for _, swap := range swaps {
fmt.Printf(" %s → %s\n", switchLabel(swap.From), switchLabel(swap.To))
switch swap.Dep {
case switchDMSDep:
shellChanges = true
snapshotLabel = "stable"
if target == deps.VariantStable {
snapshotLabel = "beta"
}
case "quickshell":
shellChanges = true
}
}
if snapshotLabel != "" {
dir, _ := backup.SnapshotDir()
fmt.Printf("\nYour DMS settings will be backed up to %s first.\n", dir)
}
if shellChanges {
fmt.Println("The shell restarts when the switch is done.")
}
if !switchAssumeYes && !askSwitch("\nContinue? [y/N] ") {
return errors.New("cancelled")
}
if snapshotLabel != "" {
path, err := backup.Snapshot(snapshotLabel)
if err != nil {
return fmt.Errorf("backing up settings, nothing was changed: %w", err)
}
if path != "" {
fmt.Printf("Backed up settings to %s\n", path)
}
}
ctx := context.Background()
if privesc.CheckCached(ctx) != nil {
if err := privesc.Run(ctx, "", "true"); err != nil {
return fmt.Errorf("authentication failed: %w", err)
}
}
if err := runSwaps(ctx, distro.(distros.VariantSwapper), swaps); err != nil {
return err
}
restore := ""
if snapshotLabel == "beta" && !switchAssumeYes {
if path, ok := backup.LatestSnapshot("stable"); ok {
fmt.Printf("\nSettings saved by beta may lose options on stable. Restore the backup from your last\nstable run (%s)? Your beta settings stay in the beta backup. [y/N] ", path)
if askSwitch("") {
restore = path
}
}
}
if shellChanges || restore != "" {
return restartShellAfterSwitch(restore)
}
fmt.Println("Done.")
return nil
}
func runSwaps(ctx context.Context, swapper distros.VariantSwapper, swaps []distros.VariantSwap) error {
progressChan := make(chan distros.InstallProgressMsg, 100)
swapErr := make(chan error, 1)
go func() {
defer close(progressChan)
swapErr <- swapper.SwapVariants(ctx, swaps, "", progressChan)
}()
// runWithProgress repeats the step on every output line, so only print it when it changes.
lastStep := ""
for msg := range progressChan {
switch {
case msg.Error != nil:
fmt.Fprintf(os.Stderr, "Error: %v\n", msg.Error)
case msg.Step != "" && msg.Step != lastStep:
fmt.Println(msg.Step)
lastStep = msg.Step
}
if msg.LogOutput != "" {
fmt.Printf(" %s\n", msg.LogOutput)
}
}
return <-swapErr
}
// restartShellAfterSwitch stops the shell before restoring, or the running one would migrate the
// restored settings and save them back. A plain restart would also reload the old binary's QML.
func restartShellAfterSwitch(restore string) error {
managed := runSystemctl("--user", "is-active", "--quiet", dmsSystemdUnit) == nil
if managed {
if err := runSystemctl("--user", "stop", dmsSystemdUnit); err != nil {
return fmt.Errorf("stopping the shell: %w", err)
}
} else {
stopShellForSwitch()
}
if restore != "" {
if err := backup.RestoreSnapshot(restore); err != nil {
fmt.Fprintf(os.Stderr, "Restoring settings failed: %v\n", err)
} else {
fmt.Printf("Restored settings from %s\n", restore)
}
}
if managed {
if err := runSystemctl("--user", "start", dmsSystemdUnit); err != nil {
return fmt.Errorf("starting the shell: %w", err)
}
} else {
dmsPath, err := exec.LookPath("dms")
if err != nil {
return fmt.Errorf("switch done, but dms was not found to start the shell: %w", err)
}
if err := exec.Command(dmsPath, "run", "-d").Run(); err != nil {
return fmt.Errorf("switch done, but starting the shell failed (run 'dms run -d'): %w", err)
}
}
fmt.Println("Done. The shell has restarted.")
return nil
}
// stopShellForSwitch terminates the supervisors and waits for them: Kill() would SIGKILL them,
// orphaning quickshell in its own session, which then keeps saving settings over the restore.
func stopShellForSwitch() {
for _, pid := range shellApp.PIDs() {
if proc, err := os.FindProcess(pid); err == nil {
proc.Signal(syscall.SIGTERM)
}
}
deadline := time.Now().Add(5 * time.Second)
for len(shellApp.PIDs()) > 0 && time.Now().Before(deadline) {
time.Sleep(100 * time.Millisecond)
}
shellApp.Kill()
}
func switchLabel(m distros.PackageMapping) string {
if m.RepoURL == "" {
return m.Name
}
return m.RepoURL + "/" + m.Name
}
func askSwitch(prompt string) bool {
if !stdinIsTTY() {
return false
}
fmt.Print(prompt)
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return false
}
answer := strings.ToLower(strings.TrimSpace(line))
return answer == "y" || answer == "yes"
}
+33 -26
View File
@@ -6,13 +6,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/duxlabs-dev/dux-go/ipc"
"github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/duxlabs-dev/dux-shell/core/internal/lowprio"
"github.com/duxlabs-dev/dux-shell/core/internal/server/sysupdate"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -34,6 +36,7 @@ Examples:
dms system update --check # list available updates dms system update --check # list available updates
dms system update # apply updates (interactive prompt) dms system update # apply updates (interactive prompt)
dms system update --noconfirm # apply updates without prompting dms system update --noconfirm # apply updates without prompting
dms system update --interactive # let the package manager ask its own questions
dms system update --dry # simulate without changing anything dms system update --dry # simulate without changing anything
dms system update --no-flatpak --noconfirm # apply system updates only dms system update --no-flatpak --noconfirm # apply system updates only
dms system update --interval 3600 # set the server poll interval to 1h`, dms system update --interval 3600 # set the server poll interval to 1h`,
@@ -41,24 +44,26 @@ Examples:
} }
var ( var (
sysUpdateCheck bool sysUpdateCheck bool
sysUpdateNoConfirm bool sysUpdateNoConfirm bool
sysUpdateDry bool sysUpdateInteractive bool
sysUpdateJSON bool sysUpdateDry bool
sysUpdateNoFlatpak bool sysUpdateJSON bool
sysUpdateNoAUR bool sysUpdateNoFlatpak bool
sysUpdateIgnore []string sysUpdateNoAUR bool
sysUpdateIntervalS int sysUpdateIgnore []string
sysUpdateListPmTime = 5 * time.Minute sysUpdateIntervalS int
sysUpdateListPmTime = 5 * time.Minute
) )
func init() { func init() {
systemUpdateCmd.Flags().BoolVar(&sysUpdateCheck, "check", false, "List available updates without applying") systemUpdateCmd.Flags().BoolVar(&sysUpdateCheck, "check", false, "List available updates without applying")
systemUpdateCmd.Flags().BoolVarP(&sysUpdateNoConfirm, "noconfirm", "y", false, "Apply updates without prompting") systemUpdateCmd.Flags().BoolVarP(&sysUpdateNoConfirm, "noconfirm", "y", false, "Apply updates without prompting")
systemUpdateCmd.Flags().BoolVar(&sysUpdateInteractive, "interactive", false, "Let the package manager ask its own questions instead of answering yes")
systemUpdateCmd.Flags().BoolVar(&sysUpdateDry, "dry", false, "Simulate the upgrade without applying changes") systemUpdateCmd.Flags().BoolVar(&sysUpdateDry, "dry", false, "Simulate the upgrade without applying changes")
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)") systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay") systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)") systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip AUR updates")
systemUpdateCmd.Flags().StringSliceVar(&sysUpdateIgnore, "ignore", nil, "Skip specific packages (repeatable or comma-separated)") systemUpdateCmd.Flags().StringSliceVar(&sysUpdateIgnore, "ignore", nil, "Skip specific packages (repeatable or comma-separated)")
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)") systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
@@ -66,14 +71,19 @@ func init() {
} }
func runSystemUpdate(cmd *cobra.Command, args []string) { func runSystemUpdate(cmd *cobra.Command, args []string) {
switch { if sysUpdateIntervalS >= 0 {
case sysUpdateIntervalS >= 0:
runSystemUpdateSetInterval(sysUpdateIntervalS) runSystemUpdateSetInterval(sysUpdateIntervalS)
case sysUpdateCheck: return
runSystemUpdateCheck()
default:
runSystemUpdateApply()
} }
// Package managers started from this thread inherit its priority.
runtime.LockOSThread()
lowprio.LowerThreadPriority()
if sysUpdateCheck {
runSystemUpdateCheck()
return
}
runSystemUpdateApply()
} }
func selectBackends(ctx context.Context) []sysupdate.Backend { func selectBackends(ctx context.Context) []sysupdate.Backend {
@@ -180,7 +190,7 @@ func runSystemUpdateApply() {
} }
fmt.Println() fmt.Println()
if !sysUpdateNoConfirm && !sysUpdateDry { if !sysUpdateNoConfirm && !sysUpdateInteractive && !sysUpdateDry {
if !promptYesNo("Proceed with upgrade? [Y/n]: ") { if !promptYesNo("Proceed with upgrade? [Y/n]: ") {
fmt.Println("Aborted.") fmt.Println("Aborted.")
return return
@@ -199,6 +209,7 @@ func runSystemUpdateApply() {
UseSudo: true, UseSudo: true,
} }
opts.AttachStdio = sysupdate.UpgradeNeedsPrivilege(backends, pkgs, opts) opts.AttachStdio = sysupdate.UpgradeNeedsPrivilege(backends, pkgs, opts)
opts.Interactive = sysUpdateInteractive && opts.AttachStdio
onLine := func(line string) { fmt.Println(line) } onLine := func(line string) { fmt.Println(line) }
ran := false ran := false
@@ -240,16 +251,12 @@ func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 { if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 {
return pkgs return pkgs
} }
ignored := make(map[string]bool, len(sysUpdateIgnore))
for _, name := range sysUpdateIgnore {
ignored[name] = true
}
out := pkgs[:0] out := pkgs[:0]
for _, p := range pkgs { for _, p := range pkgs {
if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR { if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR {
continue continue
} }
if ignored[p.Name] { if sysupdate.IsPackageIgnored(p.Name, sysUpdateIgnore) {
continue continue
} }
out = append(out, p) out = append(out, p)
@@ -258,7 +265,7 @@ func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
} }
func runSystemUpdateSetInterval(seconds int) { func runSystemUpdateSetInterval(seconds int) {
resp, err := sendServerRequest(models.Request{ resp, err := sendServerRequest(ipc.Request{
ID: 1, ID: 1,
Method: "sysupdate.setInterval", Method: "sysupdate.setInterval",
Params: map[string]any{"seconds": float64(seconds)}, Params: map[string]any{"seconds": float64(seconds)},
+134
View File
@@ -0,0 +1,134 @@
package main
import (
"fmt"
"strings"
"github.com/duxlabs-dev/dux-shell/core/internal/config"
"github.com/duxlabs-dev/dux-shell/core/internal/deps"
"github.com/spf13/cobra"
)
var systemdCompositorFlag string
var systemdCmd = &cobra.Command{
Use: "systemd",
Short: "Start DMS as a systemd user service with the compositor session",
Long: `Manage dms.service for the compositor session.
enable makes the compositor's own session unit want dms.service, so DMS starts
with that compositor only: hyprland-session.target, niri.service or
mango-session.target. The compositor is the running one, or --compositor.`,
}
var systemdEnableCmd = &cobra.Command{
Use: "enable",
Short: "Start dms.service with the compositor session",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
wm, err := systemdCompositor()
if err != nil {
return err
}
unit, err := config.EnableSession(wm)
if err != nil {
return err
}
fmt.Printf("dms.service starts with %s\n", unit)
now, _ := cmd.Flags().GetBool("now")
if !now {
return nil
}
if err := runSystemctl("--user", "start", dmsSystemdUnit); err != nil {
return err
}
fmt.Println("dms.service started")
return nil
},
}
var systemdDisableCmd = &cobra.Command{
Use: "disable",
Short: "Stop starting dms.service with any compositor session",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
removed, err := config.DisableSession()
if err != nil {
return err
}
if len(removed) == 0 {
fmt.Println("dms.service was not wanted by any compositor session")
} else {
fmt.Printf("dms.service no longer starts with %s\n", strings.Join(removed, ", "))
}
now, _ := cmd.Flags().GetBool("now")
if !now {
return nil
}
return runSystemctl("--user", "stop", dmsSystemdUnit)
},
}
var systemdStatusCmd = &cobra.Command{
Use: "status",
Short: "Show which compositor sessions start dms.service",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
state := getServiceState("dms", true)
if !state.exists {
fmt.Println("dms.service: not installed")
return
}
fmt.Printf("dms.service: %s", state.enabled)
if state.active != "" {
fmt.Printf(", %s", state.active)
}
fmt.Println()
wanted := config.SessionWanted()
if len(wanted) == 0 {
fmt.Println("Starts with: nothing (run 'dms systemd enable')")
return
}
fmt.Printf("Starts with: %s\n", strings.Join(wanted, ", "))
},
}
func systemdLifecycleCmd(verb string) *cobra.Command {
return &cobra.Command{
Use: verb,
Short: strings.ToUpper(verb[:1]) + verb[1:] + " dms.service",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runSystemctl("--user", verb, dmsSystemdUnit)
},
}
}
func systemdCompositor() (deps.WindowManager, error) {
if systemdCompositorFlag == "" {
if wm, ok := config.RunningCompositor(); ok {
return wm, nil
}
name, err := detectCompositorForSetup()
if err != nil {
return 0, err
}
systemdCompositorFlag = name
}
switch strings.ToLower(systemdCompositorFlag) {
case "hyprland":
return deps.WindowManagerHyprland, nil
case "niri":
return deps.WindowManagerNiri, nil
case "mango", "mangowc":
return deps.WindowManagerMango, nil
}
return 0, fmt.Errorf("unknown compositor %q (hyprland, niri or mango)", systemdCompositorFlag)
}
func init() {
systemdEnableCmd.Flags().StringVar(&systemdCompositorFlag, "compositor", "", "hyprland, niri or mango (default: the running one)")
systemdEnableCmd.Flags().Bool("now", false, "Also start dms.service")
systemdDisableCmd.Flags().Bool("now", false, "Also stop dms.service")
systemdCmd.AddCommand(systemdEnableCmd, systemdDisableCmd, systemdStatusCmd, systemdLifecycleCmd("start"), systemdLifecycleCmd("stop"), systemdLifecycleCmd("restart"))
}
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-go/trash"
"github.com/AvengeMedia/DankMaterialShell/core/internal/trash" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+7 -6
View File
@@ -8,10 +8,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/duxlabs-dev/dux-shell/core/internal/mangoconf"
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules" "github.com/duxlabs-dev/dux-shell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules/providers" "github.com/duxlabs-dev/dux-shell/core/internal/windowrules"
"github.com/duxlabs-dev/dux-shell/core/internal/windowrules/providers"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -216,7 +217,7 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
result.DMSStatus = parseResult.DMSStatus result.DMSStatus = parseResult.DMSStatus
case "mango", "mangowc": case "mango", "mangowc":
configDir := filepath.Join(utils.XDGConfigHome(), "mango") configDir := mangoconf.Dir()
parseResult, err := providers.ParseMangoWindowRules(configDir) parseResult, err := providers.ParseMangoWindowRules(configDir)
if err != nil { if err != nil {
@@ -351,7 +352,7 @@ func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
configDir := filepath.Join(utils.XDGConfigHome(), "hypr") configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
return providers.NewHyprlandWritableProvider(configDir) return providers.NewHyprlandWritableProvider(configDir)
case "mango", "mangowc": case "mango", "mangowc":
configDir := filepath.Join(utils.XDGConfigHome(), "mango") configDir := mangoconf.Dir()
return providers.NewMangoWritableProvider(configDir) return providers.NewMangoWritableProvider(configDir)
default: default:
return nil return nil
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power" wlclient "github.com/duxlabs-dev/dux-go/wayland/client"
wlclient "github.com/AvengeMedia/dankgo/wayland/client" "github.com/duxlabs-dev/dux-shell/core/internal/proto/wlr_output_power"
) )
type cmd struct { type cmd struct {
+8 -35
View File
@@ -9,15 +9,12 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/duxlabs-dev/dux-shell/core/internal/clipolicy"
"github.com/duxlabs-dev/dux-shell/core/internal/privesc"
"github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const (
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
)
var ( var (
immutablePolicyOnce sync.Once immutablePolicyOnce sync.Once
immutablePolicy immutableCommandPolicy immutablePolicy immutableCommandPolicy
@@ -34,13 +31,6 @@ type immutableCommandPolicy struct {
Message string Message string
} }
type cliPolicyFile struct {
PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"`
}
func normalizeCommandSpec(raw string) string { func normalizeCommandSpec(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw)) normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.TrimPrefix(normalized, "dms ") normalized = strings.TrimPrefix(normalized, "dms ")
@@ -82,25 +72,8 @@ func commandBlockedByPolicy(commandPath string, blocked []string) bool {
return false return false
} }
func loadPolicyFile(path string) (*cliPolicyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var policy cliPolicyFile
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
}
return &policy, nil
}
func mergePolicyFile(base *immutableCommandPolicy, path string) error { func mergePolicyFile(base *immutableCommandPolicy, path string) error {
policyFile, err := loadPolicyFile(path) policyFile, err := clipolicy.LoadFile(afero.NewOsFs(), path)
if err != nil { if err != nil {
return err return err
} }
@@ -218,7 +191,7 @@ func getImmutablePolicy() (*immutableCommandPolicy, error) {
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.", Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
} }
var defaultPolicy cliPolicyFile var defaultPolicy clipolicy.File
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil { if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err) immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return return
@@ -233,11 +206,11 @@ func getImmutablePolicy() (*immutableCommandPolicy, error) {
} }
} }
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil { if err := mergePolicyFile(&immutablePolicy, clipolicy.PackagedPath); err != nil {
immutablePolicyErr = err immutablePolicyErr = err
return return
} }
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil { if err := mergePolicyFile(&immutablePolicy, clipolicy.AdminPath); err != nil {
immutablePolicyErr = err immutablePolicyErr = err
return return
} }
@@ -268,7 +241,7 @@ func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n" reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
} }
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath) return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, clipolicy.PackagedPath, clipolicy.AdminPath)
} }
// preRunPrivileged combines the immutable-system check with a privesc tool // preRunPrivileged combines the immutable-system check with a privesc tool
+3 -1
View File
@@ -3,10 +3,12 @@ package main
import ( import (
"encoding/json" "encoding/json"
"testing" "testing"
"github.com/duxlabs-dev/dux-shell/core/internal/clipolicy"
) )
func TestDefaultImmutablePolicyAllowsSyncButBlocksEnable(t *testing.T) { func TestDefaultImmutablePolicyAllowsSyncButBlocksEnable(t *testing.T) {
var policyFile cliPolicyFile var policyFile clipolicy.File
if err := json.Unmarshal(defaultCLIPolicyJSON, &policyFile); err != nil { if err := json.Unmarshal(defaultCLIPolicyJSON, &policyFile); err != nil {
t.Fatalf("failed to parse embedded CLI policy: %v", err) t.Fatalf("failed to parse embedded CLI policy: %v", err)
} }
+10 -2
View File
@@ -5,8 +5,9 @@ package main
import ( import (
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/duxlabs-dev/dux-shell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
) )
var Version = "dev" var Version = "dev"
@@ -33,6 +34,13 @@ func main() {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
if os.Geteuid() != 0 {
for _, err := range utils.MigrateLegacyDirs() {
log.Warnf("dms dir migration: %v", err)
}
}
os.Args = normalizeDmenuArgv(os.Args)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
+10 -2
View File
@@ -5,8 +5,9 @@ package main
import ( import (
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/duxlabs-dev/dux-shell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/log"
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
) )
var Version = "dev" var Version = "dev"
@@ -29,6 +30,13 @@ func main() {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
if os.Geteuid() != 0 {
for _, err := range utils.MigrateLegacyDirs() {
log.Warnf("dms dir migration: %v", err)
}
}
os.Args = normalizeDmenuArgv(os.Args)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
+2 -2
View File
@@ -3,8 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" wlclient "github.com/duxlabs-dev/dux-go/wayland/client"
wlclient "github.com/AvengeMedia/dankgo/wayland/client" "github.com/duxlabs-dev/dux-shell/core/internal/proto/wlr_output_management"
) )
type randrOutput struct { type randrOutput struct {

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