Compare commits

..
1561 Commits
Author SHA1 Message Date
dms-ci[bot] 4a87e8227d bump VERSION to v1.6.3 2026-10-07 17:47:03 +00:00
bbedward c0b26caf2b dank-qml-common: classify scroll input by phase instead of pixelDelta
stable-1.6 branch of the submodule carries dank-qml-common 1ae7222 (port of d630451), so DankFlickable, DankListView and DankGridView stop treating Qt 6.12 mouse wheels as touchpads. The flake input, flake.lock and .gitmodules follow that branch.
2026-10-07 13:15:39 -04:00
Vladimir Kostelei c6c40dbced 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.

* 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.

---------

(cherry picked from commit d7074a53eb)
2026-10-07 13:05:09 -04:00
Satoshoe 02ddd010a0 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.

(cherry picked from commit 84594e6449)
2026-10-07 13:03:42 -04:00
bbedward 6eb9df87cf network: set routing preference only from settings
fixes #3068

stable-1.6: core part only, the settings and network popout changes are master-only

(cherry picked from commit 089dc98166)
2026-10-07 13:03:41 -04:00
Artem Timofeev 0adbdfdde1 fix(power): reconcile refresh rate after resume (#3459)
(cherry picked from commit 8fedfae1dc)
2026-10-07 13:01:32 -04:00
LemonMon fb002d1977 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

(cherry picked from commit 60342b7416)
2026-10-07 13:01:06 -04:00
Marlon Kranz 8b7c9085ca 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.

stable-1.6: ported the rule edits and the sound/mute policy only, the Copy menu item depends on NotificationActions which stable does not have

(cherry picked from commit 51f115f11e)
2026-10-07 13:00:11 -04:00
Marcus Ramberg 8b34bbf71a fix: scope bluez dbus match to bluezService (#3573)
Ensure the watcher only seees bluez messages

(cherry picked from commit 05377e690a)
2026-10-07 12:58:50 -04:00
bbedward fd468a6b78 brightness: ddcutil-parity ddc probing, bounded rereads
fixes #3616

(cherry picked from commit 0b9d931ea3)
2026-10-07 12:58:37 -04:00
Pedro Jose Soto Rivera 7c04b61bcc 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.

(cherry picked from commit f886789835)
2026-10-07 12:58:37 -04:00
Pedro Jose Soto Rivera bc4916a132 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

(cherry picked from commit ff50b492b1)
2026-10-07 12:58:37 -04:00
Stephanos Kamprogiannis 58a356bb05 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.

(cherry picked from commit 22fa08f0ff)
2026-10-07 12:58:37 -04:00
arfan 3061e1301d 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

(cherry picked from commit c1818dee2c)
2026-10-07 12:58:37 -04:00
arfan e333da2013 fix(theme): update focus highlight colors for list and quick input components (#3579)
(cherry picked from commit 814620a934)
2026-10-07 12:58:37 -04:00
bbedward 307d05d76c matugen: fix kitty cursor color
fixes #3520

(cherry picked from commit f3af870d93)
2026-10-07 12:58:37 -04:00
bbedward 10dbe73335 matugen: fix kde color scheme templates ignoring the accent
(cherry picked from commit f102a83d9f)
2026-10-07 12:58:37 -04:00
Huỳnh Thiện Lộc b6fee3a98f fix(island): avoid bottom clipping in media expanded sheet (#3593)
(cherry picked from commit 0717908d53)
2026-10-07 12:58:37 -04:00
Thomas Kroll 6166d51c7c 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.

Signed-off-by: Thomas Kroll <[email protected]>
(cherry picked from commit eb0cb80884)
2026-10-07 12:58:37 -04:00
Huỳnh Thiện Lộc d06fcccff9 fix(notepad): hide placeholder during IME composition (#3646)
(cherry picked from commit 340f2d179c)
2026-10-07 12:58:37 -04:00
Marlon Kranz d9cfeded47 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.

(cherry picked from commit 6a320399c9)
2026-10-07 12:58:37 -04:00
bbedward c4ced3ac9d disable fanout 2026-10-07 12:36:06 -04:00
bbedward ce2ec80d80 bar: detect touchpads by scroll phase instead of pixelDelta 2026-10-07 12:34:11 -04:00
bbedward a340eba493 config: drop Before=graphical-session.target from the generated hyprland target
(cherry picked from commit 507b818a55)
2026-10-06 11:05:47 -04:00
dms-ci[bot] 2db7646fe3 bump VERSION to v1.6.2 2026-09-17 15:07:40 +00:00
bbedward 097de493bb displayconfig: fix custom scale option 2026-09-17 10:33:09 -04:00
bbedward a7db0354ae niri: fix workspace updates never applying
fixes #3462
port 1.6

(cherry picked from commit 36bb62ac66)
2026-09-17 14:03:55 +00:00
bbedward 2d2502cc67 qt: write real newlines in new qt5ct/qt6ct config
fixes #3457
port 1.6

(cherry picked from commit 49a639a358)
2026-09-17 14:03:53 +00:00
Huỳnh Thiện Lộc 402a8515ee 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

(cherry picked from commit 13192370f5)
2026-09-17 09:44:00 -04:00
phoropter 4cf0f0c898 Update vesktop.css (#3410)
Fix text being the same color as mention badge background.

(cherry picked from commit b4f6da25ee)
2026-09-17 09:43:51 -04:00
Domen Kožar fd0564a3ec tray: use available width for menu labels (#3456)
(cherry picked from commit 1bf35426ea)
2026-09-17 09:43:22 -04:00
Marlon Kranz 07f8675a7b 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.

(cherry picked from commit 579d6f7b5c)
2026-09-17 09:41:33 -04:00
Alexander cab2a1a3bc dock: fix overview visibility on niri (#3460)
(cherry picked from commit 81d5962650)
2026-09-17 09:41:21 -04:00
bbedward 41dd91fe75 dank16: use the real primary_container and fix light-mode derivation
fixes #3405
fixes #3406
ref #3408
port 1.6

(cherry picked from commit 641d65cbd9)
2026-09-16 14:06:18 +00:00
dms-ci[bot] c074b220ee nix: update vendorHash for go.mod changes 2026-09-16 09:32:22 -04:00
bbedward 243d716703 core: bump dankgop
port 1.6

(cherry picked from commit c9506bc847)
2026-09-16 13:28:09 +00:00
Giacomo Di Clerico fde2181c3b 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

(cherry picked from commit 218f7f752f)
2026-09-15 16:46:47 -04:00
Harsh Kumar a5ffd18596 display config: read live layout from hyprland instead of the cached wlr snapshot (#3418)
(cherry picked from commit d7b5812561)
2026-09-15 16:46:47 -04:00
bbedward e0aa1f0177 frame: close hairline gap at the screen edge
ref #3389

(cherry picked from commit 4d5bac28c9)
2026-09-15 16:46:47 -04:00
bbedward e73b9c3ce1 evdev: open input devices read-only
go-evdev default is O_RDWR but we dont need to open evdev devices RW

(cherry picked from commit 6e9a657132)
2026-09-15 16:46:47 -04:00
Mattias 69955286a7 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.

(cherry picked from commit 6ecdd6d45d)
2026-09-15 16:46:47 -04:00
Timofey d3a02f6342 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

(cherry picked from commit 05c807b7cd)
2026-09-15 16:46:47 -04:00
gibbert 86234e5b09 fix(audio): avoid false negative when some event sounds can't be found (#3437)
(cherry picked from commit 6af2b1fbc3)
2026-09-15 16:46:47 -04:00
deepfriedret 938fabe27d fix(media widget): prevent title from truncating with scroll song title on (#3436)
(cherry picked from commit 0133962936)
2026-09-15 16:46:47 -04:00
Mario Mohar 1b34aa0ce1 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.

(cherry picked from commit 6b50696b8a)
2026-09-15 16:46:47 -04:00
Huỳnh Thiện Lộc 76ed5d68fa 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

(cherry picked from commit 4141089e63)
2026-09-15 16:46:47 -04:00
Mario Mohar b69496dbcd fix(settings): size the plugin update spinner to fit its row (#3383)
(cherry picked from commit cf74c10561)
2026-09-15 16:46:46 -04:00
bbedward ef179f9c92 disable dcal from release dispatch 2026-09-15 16:41:16 -04:00
bbedward b451d4d8e0 doctor: add aqueous and scroll
port 1.6

(cherry picked from commit 09ddd6da82)
2026-09-11 13:47:33 +00:00
purian23 12f7675950 refactor(island): add island satellite hover option
fixes #3296
port 1.6

(cherry picked from commit 60675ecb39)
2026-09-10 03:36:41 +00:00
dms-ci[bot] 06cd88e990 nix: update vendorHash for go.mod changes 2026-09-09 09:10:51 -04:00
dms-ci[bot] aa4b99def4 bump VERSION to v1.6.1 2026-09-08 17:14:24 +00:00
Zoey Bauer 4015f634c4 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

(cherry picked from commit 8fe918ff24)
2026-09-08 15:50:35 +00:00
bbedward b2d1ec6690 systray: dont wrap context menu title
port 1.6

(cherry picked from commit 2bf2754ad2)
2026-09-08 14:19:25 +00:00
Huỳnh Thiện Lộc 1efc10dc7e 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

(cherry picked from commit 0f058468a5)
2026-09-08 09:24:09 -04:00
BB 94710cf643 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]>
(cherry picked from commit b9365610c8)
2026-09-08 09:23:54 -04:00
bbedward 3711cb11fa core: bump dankgo
fixes #3361
port 1.6

(cherry picked from commit 8232144bb7)
2026-09-08 01:19:29 +00:00
bbedward 40a0eb6841 workspace: keep hit area out of adjacent spacers
fixes #3360
port 1.6

(cherry picked from commit fb07a91108)
2026-09-08 01:19:27 +00:00
bbedward 0a643028b3 hyprland: stop overwriting the config backup during lua migration
fixes #3359
port 1.6

(cherry picked from commit 24e17a5957)
2026-09-08 01:02:22 +00:00
bbedward 0189eb0dbb frame: draw panel backgrounds when fullscreen hides the frame
port 1.6

(cherry picked from commit 17d4ea6f88)
2026-09-08 00:45:35 +00:00
bbedward ed26bdebdc i18n: sync, change plugin workflow
port 1.6

(cherry picked from commit 8c42700a4c)
2026-09-08 00:45:32 +00:00
bbedward 2e2c74e355 i18n: sync
port 1.6

(cherry picked from commit 9bf8174984)
2026-09-07 17:51:21 +00:00
bbedward c1a3be60c3 fix(launcher): stop injectnig plugins into app categories
port 1.6

(cherry picked from commit 0f56536142)
2026-09-07 14:37:09 +00:00
bbedward f8260513b1 frame: keep overlay backgrounds with their content
fixes #3350
port 1.6

(cherry picked from commit de27853c46)
2026-09-07 14:29:53 +00:00
bbedward ed1855946f ci: disable qml-common from release fanout 2026-09-07 10:01:55 -04:00
bbedward c0bc1d8885 display: watch the session store for active profile changes
port 1.6

(cherry picked from commit 0890db93db)
2026-09-07 13:55:38 +00:00
Huỳnh Thiện Lộc 3f09d7b1c8 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

(cherry picked from commit a9fce46fec)
2026-09-07 13:11:50 +00:00
bbedward 710f20d085 i18n: make sync script auto-pull plugins for translation
port 1.6

(cherry picked from commit c1e53dbc33)
2026-09-06 21:05:10 +00:00
bbedward 6261e580aa bar: refresh blur region after widget layout settles
ref #3348
port 1.6

(cherry picked from commit 62ad4c2cb7)
2026-09-06 17:54:19 +00:00
bbedward 42912ebe74 launcher: stop rebuilding the category map per app and index i18n misses
fixes #3345
port 1.6

(cherry picked from commit c76c29139f)
2026-09-06 17:40:35 +00:00
bbedward e8a755b85d controlcenter: release dgop refs while the popout is closed
ref #3349

(cherry picked from commit 654c6dce8f)
2026-09-06 13:40:26 -04:00
bbedward 6713f05e02 audio: add output card selection
port 1.6

(cherry picked from commit 8188f53da5)
2026-09-06 17:15:06 +00:00
bbedward 5d39702136 lock: keep the password cursor inside the field and follow it through overflow and wrapped reveals
fixes #3319
port 1.6

(cherry picked from commit 5baef07048)
2026-09-05 16:26:30 +00:00
bbedward 166b82df1b network: reject weak hotspot passwords before handing them to NetworkManager
fixes #3318
port 1.6

(cherry picked from commit f402731ef2)
2026-09-05 16:21:54 +00:00
bbedward bca1466162 dash, notifications, island: fix calendar banner overflow, expand chevron, and player row clipping
fixes #3300
fixes #3301
fixes #3317
port 1.6

(cherry picked from commit 59f6f1fa71)
2026-09-05 16:21:52 +00:00
bbedward 75b8932b20 network: keep saved wifi profiles when NM fails with no-secrets
fixes #3313
port 1.6

(cherry picked from commit c0f775ae49)
2026-09-05 16:21:49 +00:00
bbedward a1b51b191f bar: focus workspace icons on any workspace and wire island overview loader
fixes #3309
fixes #3310

port 1.6

(cherry picked from commit 3d6e45f430)
2026-09-05 15:40:15 +00:00
bbedward 5defc26929 display config: judge profile outputs by live heads, not stored make/model
fixes #3306
port 1.6

(cherry picked from commit 85bc7604c2)
2026-09-05 15:40:12 +00:00
bbedward 71e8a85cd1 screenshot: resolve region and picker keys through the seat keymap
fixes #3311
port 1.6

(cherry picked from commit 3674d0f480)
2026-09-05 15:19:16 +00:00
Huỳnh Thiện Lộc 95b2750e5f fix(screenshot): scale HUD and selection label for HiDPI displays (#3308)
port 1.6

(cherry picked from commit 5f124de8d4)
2026-09-05 15:10:48 +00:00
bbedward 7f1a423174 theme: read the dynamic error color per mode
fixes #3298
port 1.6

(cherry picked from commit 59a03f450d)
2026-09-05 14:47:12 +00:00
bbedward 33e599100c systemd: keep the inherited hard file limit in dms.service
fixes #3312
port 1.6

(cherry picked from commit 3797422832)
2026-09-05 14:23:00 +00:00
Adolfo Ochagavía cb85620777 Fix repeated day in calendar (#3302)
Root cause: we were creating a `Date` that did not exist in my timezone
(Santiago, Chile) because of DST. The JS engine decided to pick the
previous day, so I ended up with two times 5 September.

Fix: introduce a `DateOnly` object that internally uses UTC (to prevent
DST issues). Other parts of the system still use `Date`, which
probably should be refactored to use `DateOnly` as well (maybe in an
upcoming PR?). In the meantime, we provide a function to export a
`DateOnly` as a `Date`, using noon as the time instead of midnight
(because there are no DST changes at noon AFAIK).

We tested this manually (see the attached screenshots), but didn't find
a suitable place for automated tests.

port 1.6

(cherry picked from commit e4793327b8)
2026-09-04 23:42:17 +00:00
purian23 faf25e98a5 feat(island): vertical orientation layout management support
Port 1.6

(cherry picked from commit e18931353b)
2026-09-04 21:57:39 +00:00
bbedward d7b264701e bar: keep the widget font size the user asked for
ref #3295
port 1.6

(cherry picked from commit cf3c76a79f)
2026-09-04 17:11:05 +00:00
bbedward c929a8952b screenshot: add instancelock on runtimedir/wayland-display to prevent
multiple instances from running

fixes #3293
port 1.6

(cherry picked from commit 7e18a68b82)
2026-09-04 17:04:14 +00:00
bbedward 82089c588a launcher: revalidate the cached launcher sections on load
fixes #3292
port 1.6

(cherry picked from commit 96a21fbe08)
2026-09-04 16:02:55 +00:00
bbedward 19a56d1835 clock: align the horizontal time block to whole pixels
fixes #3291
port 1.6

(cherry picked from commit fbffa410af)
2026-09-04 16:02:53 +00:00
bbedward 8371fa8ef5 audio: resolve the sound theme before the login sound plays
fixes #3290
port 1.6

(cherry picked from commit 936b42b4e7)
2026-09-04 16:02:51 +00:00
Huỳnh Thiện Lộc acca7fae68 fix(dash): dim today button when on current month (#3294)
* fix(dash): dim today button when on current month and hide tooltip on click

* fix(dash): use Theme.shortDuration for calendar today button opacity animation

* refactor(dash): use standard DankActionButton tooltipText for calendar today button

* chore(dash): remove unused todayButton id

* fix(dash): allow returning to today when another day in current month is selected

(cherry picked from commit 07d8e28668)
2026-09-04 10:38:44 -04:00
bbedward a478c27fd0 bar: make the global shadow toggles authoritative
(cherry picked from commit 4e2afb53cc)
2026-09-04 10:38:37 -04:00
bbedward 094dcdd035 matugen: stop embedding the adw-gtk3 body in the gtk light template
fixes #3282
port 1.6

(cherry picked from commit eb53d8983e)
2026-09-04 14:38:01 +00:00
bbedward 344c1e610c theme: write desktop settings through dconf instead of gsettings
fixes #3280
port 1.6

(cherry picked from commit a665ba8ca5)
2026-09-04 14:37:58 +00:00
bbedward fdf8722818 matugen: give dry runs their own config file
fixes #3279
port 1.6

(cherry picked from commit d57e32c5cb)
2026-09-04 14:37:56 +00:00
usoaraway0 f2114c1a01 brightness: retry initial ddc read (#3287)
port 1.6

(cherry picked from commit ec22e9e594)
2026-09-04 13:51:54 +00:00
枫莹 6ed79d06df fix(plugins): use plugin ids for management actions (#3284)
(cherry picked from commit 433b13cb0e)
2026-09-04 09:51:29 -04:00
usoaraway0 8ff677d2eb niri: create screenshot directory on demand (#3286)
port 1.6

(cherry picked from commit cbe572eea6)
2026-09-04 13:46:32 +00:00
purian23 700f43a194 feat(island): allow hiding clock or date options in compact pill
Port 1.6

(cherry picked from commit c1f1da1d26)
2026-09-04 04:47:35 +00:00
bbedward 53a5582858 build: remove install-shell from install in Makefile
(cherry picked from commit 2008c39af3)
2026-09-03 18:28:38 -04:00
bbedward 0bbe83380c distros/void: fix stable template 2026-09-03 12:53:53 -04:00
bbedward 6a4ce613bb colorpicker/popout: add instant-close machinery for eye dropper 2026-09-03 12:51:15 -04:00
bbedward ea0b3159dd bump VERSION to v1.6.0 2026-09-03 12:20:48 -04:00
bbedward ad30c2155a settings: add migration for battery style change 2026-09-03 12:17:24 -04:00
Mario Moharandbbedward 5142d576f5 fix(bar): centre widget content on whole pixels (#3263)
* fix(bar): keep widget content on whole pixels

Bar widgets centre their content inside a box of
widgetThickness - horizontalPadding * 2, and Qt rounds the offset of a
centred anchor to whole pixels. When the content and that box have
different parity the exact offset falls on a half pixel, Qt rounds it
away from zero and the content renders half a pixel above the centre.
Widgets drawn from rectangles, such as the workspace switcher, compute
their own snapped height and stay exactly centred, which is why they
look right next to everything else in #3074.

With the stock configuration this is the normal case: the bar is 40 px,
the box is 14 px and barIconSize returns round((40 / 48) * 18) = 15.

Two sizes now round to whole pairs of pixels. The icon size, so that it
matches the parity of the box, and widgetThickness, because the box
inherits its parity from there (the padding is doubled, so it never
changes it). Going through every innerPadding from 0 to 16, nine of the
seventeen configurations currently put the icon on a half pixel and none
of them do afterwards. The 48 px default bar is untouched, the sizes
that change do so by one pixel.

Measured in a nested Hyprland at scale 1, ink centroid of the widget
content relative to the centre of its pill, before and after:

  launcher      -0.50   +0.00
  weather       -0.81   -0.52
  clipboard     -1.49   -0.93
  notifications +0.03   +0.46
  battery       -0.63   -0.49
  bluetooth     -0.53   -0.03

Every icon moves down by half a pixel. What remains matches the ink
asymmetry of the individual glyphs, measured separately at an exact
position, and is not a placement error.

Two things this does not cover. On a fractional or HiDPI scale
horizontalPadding can land on a half pixel, which puts the box back
between the grid lines, and a text only widget such as the clock has a
line box of 15 px at the default font size, which cannot sit exactly on
the centre of a 28 px pill no matter how it is rounded.

Closes #3074

* fix(bar): give bar text a line box that can be centred

A widget without an icon is centred as one line box, and the same parity
rule applies to it as to the icon in the commit before this one. At
fontSizeSmall the line box of the bar font is 15 px tall, so on a 28 px
pill it cannot sit on the centre no matter which way the offset is
rounded, and the clock reads about half a pixel high next to everything
else. One step up the box is 16 px and it lands on the grid.

Only the branch the standard bar uses changes. The 0.9 branch below a
scale of 0.75 already comes out at 14 px, and the fontSizeMedium branch
above 1.25 is left alone because there the next even box is two steps
away, which would be a bigger change than this is worth.

Same session and same measurement as the previous commit, ink centroid
of the widget content relative to the centre of its pill:

  clock and date  -0.58   +0.05
  battery         -0.63   -0.05
  weather         -0.82   -0.28

The bar font goes from 12 px to 13 px, which is the visible part of this
and the reason it is a separate commit. It can be dropped without
touching the icon fix.

* bar: snap widget thickness to even device pixels and pick text sizes by line box parity

* bar: snap bar thickness to even so the pill centres on whole pixels

* bar: share the bar thickness formula so dock, osd and popouts stay in step

* dock: read the bar clearance at the compositor screen scale

---------

Co-authored-by: bbedward <[email protected]>
2026-09-03 12:13:09 -04:00
bbedward 592ca3ee8f island: follow island opacity in every state and drop covered widget blur 2026-09-03 10:40:34 -04:00
bbedward 6c54855fdd floating window: drop client side borders 2026-09-03 09:57:32 -04:00
bbedward 41c01a9319 floating windows: dont apply cornerRadius
blame 9013464312

fixes #3271
2026-09-03 09:42:41 -04:00
bbedward f9a9e9baf8 weather: stop labeling auto-location coords with the configured city name
fixes #3272
2026-09-03 09:11:10 -04:00
dms-ci[bot] 053337d9b8 nix: update vendorHash for go.mod changes 2026-09-03 13:09:40 +00:00
Marlon Kranz 4693178345 feat(theme): add a selectable source color mode for wallpaper palettes (#3020)
* feat(theme): add a selectable source color mode for wallpaper palettes

DMS always seeds the palette from matugen's most dominant candidate
(--source-color-index 0, hardcoded at both call sites). When a large
muted region outvotes a smaller vivid one, a visibly warm wallpaper
produces a cold palette: one image here goes to #8ad0ee where the warm
subject is at hue 46.

Adds a Source Color setting with seven values. Dominant is the default
and is byte-identical to today. Colorful is computed in Go before
matugen runs: quantize the image (Wu for the starting clusters, then a
weighted k-means in Lab), score the result the way caelestia-cli does,
and hand matugen the resulting hex. Its scoring walks a chroma/tone bar
down from 20 until something clears it, which biases the pick toward a
colorful, well-lit color rather than whichever muted color covers the
most pixels. The remaining five map to matugen's --prefer.

- core/internal/matugen/sourcecolor.go: extraction, scoring, and the
  one helper both matugen call sites now share
- buildOnce resolves the seed once, before the StockColors branch, so
  the dry run and the real run cannot disagree about it
- extraction failure logs a warning and falls through to matugen's own
  extraction; it never fails a theme build
- --prefer arrived in matugen 4.1, so it is gated separately from the
  existing isV4 check; on 4.0.x those five modes degrade to the
  dominant color instead of aborting on an unknown argument
- --source-mode on generate and queue, plus sourceMode on the socket
  handler
- Source Color dropdown in Theme & Colors, defaulting to Dominant

Adds github.com/Nadim147c/material/v3 (Apache-2.0, no dependencies) for
HCT conversions, Wu quantization and the dislike analyzer. Its
QuantizeCelebi is not used: it returns after one unconverged iteration
and its Lab.DistanceSquared is a dot product rather than a distance, so
it collapses most of an image into a single cluster.

* fix(theme): address review feedback on source color mode

Two points from review on #3020.

The quantize comment pointed at a FOLLOWUPS file that does not exist in
this repository. Replaced the reference with the measurement it was
standing in for, so the claim is self-contained.

setMatugenSourceMode called Theme.generateSystemThemesFromCurrentTheme()
directly on top of the regenSystemThemes onChange hook that Spec.set
already dispatches. Both feed a 100ms debounce so it was one generation
either way, but the direct call was redundant and is gone.

* fix(theme): keep {{image}} resolvable in colorful mode

Colorful resolves the seed itself and calls matugen with a hex source, which
leaves matugen's {{image}} unset: the builtin pywalfox template and any user
template using the keyword render the literal string "Null".

Pass the wallpaper path alongside dank16 in --import-json-string, as suggested
in review, so those templates keep working unedited. The path is made absolute
to match what matugen renders for {{image}} in the other modes.

* fix(theme): cut translator terms on the source color dropdown

Per-mode descriptions were seven of the fifteen new terms this branch adds,
and five of them differed by a single adjective. Drop them: the dropdown row
already describes what the setting does, and the docs cover each mode.
2026-09-03 09:07:21 -04:00
Huỳnh Thiện Lộc 0fc4aac964 feat(screenshot): add --geometry flag to selection coordinates (#3274)
* feat(screenshot): add -g/--geometry flag to print selection coordinates

- Add -g/--geometry persistent flag to print region geometry (X,Y WxH) to stdout.
- Skip image buffer allocation, cropping, and file/clipboard/notification actions when in geometry mode.
- Support standard Enter/Space confirmation or mouse release via --no-confirm.
- Exit with code 1 when selection is cancelled, matching slurp behavior for script integrations.
- Support printing geometry across last, window, output, full, and all modes.
- Add unit tests in region_geometry_test.go.

* refactor(screenshot): deduplicate output bounds and clean geometry result helpers

- Add bounds() method on WaylandOutput to deduplicate Hyprland geometry lookup across output capture and search.
- Add geometryResult() helper in screenshot package to eliminate redundant CaptureResult literals.
- Add GeometryString() method on Region to unify geometry formatting.
- Refactor selectedLogicalGeometry() to return Region directly.
- Clean up flag validation in runScreenshot.

* fix(screenshot): address PR review on geometry coordinates and JSON output

- Derive selectedLogicalGeometry from selectionExtent and ext.logical to keep geometry in sync with inclusive selection bounds.
- Use pointer int for X and Y in screenshotMetadata so (0,0) origins are preserved in JSON output.
- Divide output dimensions by effectiveScale in bounds() for non-Hyprland compositors and handle transform rotation.
- Convert stored device coordinates to logical units in captureLastRegion geometry mode.
- Update and expand unit tests for inclusive bounds, preselection, and scaled outputs.

* fix(screenshot): prevent geometry query from clobbering last region and handle niri window geometry

- Guard SaveLastRegion in captureRegion with !s.config.Geometry to prevent overwriting saved region with logical coordinates.
- Return explicit error when attempting window -g on niri instead of printing bogus 0,0 0x0 rect.
- Populate Region dimensions in captureNiriWindow.
2026-09-03 08:45:36 -04:00
purian23 ab1e57dda5 refactor(media): re-add wallpaper blur to island media & misc tweaks
- New Wallpaper on/off toggle in the Media Player tab for Dashboard and Island Media
2026-09-03 01:28:05 -04:00
purian23 1d2ba798b0 fix(widgets): update island drag & drop targets 2026-09-03 00:02:47 -04:00
purian23 78e3308f40 feat(battery): circle battery option per/bar instance & theme/step colors
- New settings located in Bar > Widgets and Island > Appearance
- Theme & Step Colors located in Bar > Appearance
2026-09-02 23:14:02 -04:00
Huỳnh Thiện Lộc 99e2368b1c feat(screenshot): add live preview panel for scroll capture (#3269)
* feat(screenshot): add live preview panel for scroll capture

- Port scrolling live preview from dms-quick-capture backend to native screenshot.
- Calculate dynamic preview panel geometry placed beside the capture hole on the side with most available space.
- Render downscaled live stitched canvas with dark translucent background and white border in drawScrollOverlay.
- Include preview panel in Wayland input regions and allow completing capture by clicking the preview panel.
- Update cursor shape to pointer hand when hovering over preview or control buttons.
- Add unit tests for layout, hit testing, and canvas rendering.

* fix(screenshot): bound scroll preview aspect ratio to recent canvas tail

- Derive preview aspect ratio from a bounded tail window of the canvas (maxPreviewRows = frameW * maxHeight / minImageW).
- Prevent preview panel width from collapsing below minImageW and disappearing when stitched rows exceed ~4.5x width.
- Render the recent window rows in drawScrollPreview to maintain a stable panel size and display live incoming content.
- Add unit test for long scrolling capture preview bounds.
2026-09-02 21:28:29 -04:00
bbedward c6c14576eb bar: support multiple bars in island and frame modes 2026-09-02 19:12:49 -04:00
bbedward f022aa6796 changelog: fix since async loading chnage 2026-09-02 17:05:40 -04:00
bbedward 0fe56c4bfa dash: align user tile right edge when weather is disabled 2026-09-02 16:49:08 -04:00
bbedward 9378dfc44e frame: keep connected chrome attached to the bar during spring overshoot 2026-09-02 16:48:33 -04:00
bbedward f65e5ef2f2 settings: quick simplification and cleanup prior to 1.6
the full overhaul/redesign will come later
2026-09-02 16:20:16 -04:00
purian23 81cbcef3bc refactor(island): hide & collapse inactive activities 2026-09-02 15:42:15 -04:00
bbedward e8082ad75d island: drag and drop home face layout 2026-09-02 15:25:29 -04:00
bbedward 5ed5845eaf i18n: sync 2026-09-02 15:07:13 -04:00
bbedward 13fafa1dff island: simplify volume and brightness home slots 2026-09-02 14:59:43 -04:00
bbedward 6df71083ec island: redesign the separate media player variant 2026-09-02 14:47:01 -04:00
bbedward 863152a441 island: re-add different player style 2026-09-02 14:47:01 -04:00
purian23 65ee46e330 feat(island): dedicated volume/brightness activities & placement options 2026-09-02 13:44:29 -04:00
bbedward 0a46f84aaa update qml-common 2026-09-02 13:38:02 -04:00
bbedward de24b1515a island: re-use media controls from dankdash 2026-09-02 13:36:09 -04:00
bbedward a781b7b236 settings: use dropdowns for option rows that overflow in translation
fixes #3265
2026-09-02 12:58:06 -04:00
bbedward 3effe6e7db widgets: add DankRefreshButton and use it 2026-09-02 09:59:51 -04:00
bbedward 875d32176d wayland/gamma: expose contrasta and gamma settings
fixes #3255
2026-09-02 09:50:47 -04:00
YewFence 63ec4bd945 feat(controlcenter): add wifi refresh button (#3245)
Add a refresh action to the Network detail header that calls
NetworkService.scanWifi(), mirroring the existing button in the Settings
WiFi tab. The button is hidden while WiFi is toggling or a scan is in
progress, and sits left of the connection-type tabs so the tab group
does not shift when switching between Ethernet, WiFi and Cellular.
2026-09-02 09:50:09 -04:00
Huỳnh Thiện Lộc 9a7177f929 fix(bar): prevent focused app widget from blanking and live-sync (#3258)
* fix(bar): prevent focused app widget from blanking and live-sync popout on focus switch

* fix(bar): use per-output is_active flag for Niri workspace resolution

* fix(bar): gate Niri lastFocusedWindowId fallback on active popout visibility
2026-09-02 09:41:48 -04:00
Huỳnh Thiện Lộc 4f0cbf5b03 fix(powermenu): close on escape key, dedup action shortcuts (#3262)
* fix(powermenu): close on escape key and deduplicate action shortcuts

- Centralize Escape key handling in PowerMenuContent via closeRequested signal,
  cancelling any active hold and enabling Esc to close both modal and popout variants.
- Remove redundant Keys.onEscapePressed wrapper in PowerMenuPopout.
- Extract handleActionShortcut and handleKeyRelease helpers to eliminate ~50 lines
  of duplicated action shortcut dispatch between list and grid navigation.

* fix(powermenu): drop duplicate Keys.onEscapePressed handler

- Remove redundant Keys.onEscapePressed in PowerMenuContent, keeping Escape
  handling unified under Keys.onPressed to preserve isAutoRepeat and
  committedAction guards.
2026-09-02 09:24:58 -04:00
Huỳnh Thiện Lộc 95ee917e37 feat(screenshot): support 4-corner resizing while holding Ctrl (#3259)
* feat(screenshot): support 4-corner resizing while holding Ctrl

- Hit-test selection corners when Ctrl is held to allow resizing.
- Set opposite corner as fixed anchor and update dynamic cursor shape
  (NwseResize/NeswResize).
- Draw corner handle indicators when Ctrl is held or during active resize.
- Add 'Ctrl resize/move' to overlay HUD and provide complete glyph
  coverage for HUD labels.
- Add comprehensive unit tests covering hit testing, inverted dragging,
  incremental handle damage, and glyph coverage.

* fix(screenshot): invalidate background cache on handle and shift modifier changes

- Track backgroundHandles and backgroundShift in RenderSlot.cacheValid.
- Rebuild background slot and HUD when handle or shift modifier states
  change with an existing selection, preventing incremental re-dimming
  from punching holes in the HUD.
- Fix trailing newline in region_resize_test.go for golangci-lint.

* fix(screenshot): gate redraw and handle dimming on render modifier changes

- Only trigger redrawSurface when Shift or Ctrl modifier states actually
  change, preventing redundant redraws when tapping Alt, Super, or Locks.
- Only re-dim and damage corner handle bounding boxes in overlayDelta and
  overlayDamage when handles move or their visibility changes, preventing
  unnecessary re-dimming of stationary handles over the HUD.
- Add TestOverlayDeltaIdenticalOverlayNoHandleDimming test.

* fix(screenshot): measure hud text advance accurately to prevent pill overflow

- Use (1+len(item.desc))*(charW+1) when calculating totalW in drawHUD,
  matching the full 9px advance used when drawing space and description.
- Add TestHUDDimensionsContainText to verify HUD pill dimensions enclose
  all text items with correct padding across all variant combinations.

* fix(screenshot): compare handle rect sets in overlay delta and damage

- Use slices.Equal to compare prev and cur handleRects in overlayDelta
  and overlayDamage instead of interior equality.
- Properly re-dim and damage handles when selection corners cross monitor
  boundaries where clamped interior is unchanged but top/bottom/left/right
  flags change.
- Add TestOverlayDeltaHandlesClampedMonitorEdge test.

* test(screenshot): verify drawHUD output buffer pixels stay within pill bounds

- Extract hudDimensions helper on RegionSelector used by drawHUD.
- Update TestHUDDimensionsContainText to drive drawHUD directly into a
  zeroed buffer and assert no lit pixel falls outside the computed
  HUD pill boundary across all configuration variants.
2026-09-02 09:23:37 -04:00
fromelicksandfromelicks 4d6d7e0d5f fix(lint-qml): explain how to actually create the tooling VFS config (#3249)
Co-authored-by: fromelicks <[email protected]>
2026-09-02 09:20:38 -04:00
purian23 ed9e01e438 dock: fix tooltip guard referencing 2026-09-02 00:45:56 -04:00
Huỳnh Thiện Lộc 226868b6f0 fix(dock): layout & indicator misalignment on orientation switch (#3256)
* fix(dock): fix solid mask regression and geometry distortion when switching orientation in frame mode

* fix(dock): fix running app indicator misalignment on orientation change

* fix(dock): center multi-window running indicators around app icon

* fix(dock): fix vertical hit area collapse and transient background flicker on position change
2026-09-02 00:39:23 -04:00
purian23 dd068dbd13 icons: honor XDG base dir order & resolve standalone icons
- User icon dirs now sort ahead of system ones so overrides in
~/.icons and ~/.local/share/icons

- Desktop entry changes
refresh that index and drop cached misses instead of clearing
everything

Closes #2360
Closes #3246
2026-09-02 00:15:01 -04:00
dms-ci[bot] 5f4ba58acb nix: update vendorHash for go.mod changes 2026-09-02 01:01:17 +00:00
bbedward 881cfd2fd3 core: bump dankgo 2026-09-01 20:59:03 -04:00
bbedward 4f6841d8d2 core: load only the mesa egl vendor when the compositor renders on it 2026-09-01 20:42:21 -04:00
bbedward 823f5f85d5 wallpaper: release decoded image once the frozen texture is grabbed 2026-09-01 20:42:21 -04:00
dms-ci[bot] 7fa9c42f46 nix: update vendorHash for go.mod changes 2026-09-02 00:12:44 +00:00
purian23 c0135b11e5 core: update dankgo dep 2026-09-01 20:10:25 -04:00
purian23 5bb6058751 feat(clipboard): add a copy-as-text action for entries with a text alternates
- New Entries captured from a mixed image+text offer keep the image as the primary mime i.e. bitmap
- Copy Text context-menu item on atlernates

Closes #2849
2026-09-01 19:33:23 -04:00
dms-ci[bot] 0c51946f4f nix: update vendorHash for go.mod changes 2026-09-01 23:27:26 +00:00
bbedward ef63de44b4 core: bump dankgo 2026-09-01 19:24:46 -04:00
bbedward 0b87df2608 island: keep hover intent when a destination expands after its face loads 2026-09-01 19:09:42 -04:00
bbedward ba04356d64 island: decouple blank-click policy from destinations, centralize escape 2026-09-01 18:57:20 -04:00
bbedward 8fb83d251c island: collapse weather face on blank click
fixes #3254
2026-09-01 18:46:12 -04:00
bbedward 3b990694c2 i18n: add Bulgarian 2026-09-01 12:42:13 -04:00
bbedward 0540091a04 modals/welcome: tweaks to welcome page layout 2026-09-01 12:42:13 -04:00
Huỳnh Thiện Lộc b32fe1e806 feat(dock): sync background and icons in frame overview (#3252)
* feat(dock): sync background and icons in frame overview

- Keep FrameDockHost pre-instantiated in FrameWindow when dockOpenOnOverview is enabled to eliminate QML Loader instantiation lag on overview toggle.
- Add overviewReveal property to DockBody and snap slide spring positions instantly on overview enter.
- Enforce showDock check in DockBody reveal property so dock stays hidden outside overview when showDock is disabled.

* fix(dock): handle fullscreen in overview and disable hover

- Evaluate overviewReveal before hiddenForFullscreen so dock can open in overview over fullscreen windows.
- Return false when showDock is disabled to prevent hover reveals outside overview.
2026-09-01 11:33:09 -04:00
bbedward ae7ed119a6 changelog: enable 1.6 changelog 2026-09-01 11:15:06 -04:00
bbedward fa445388e4 island/bar: square blur region corners where goth paths paint square
ref #3191
2026-09-01 10:50:33 -04:00
bbedward d23226f3ea ci: update release job 2026-09-01 10:28:46 -04:00
bbedward 99d36e9421 bar: rework hover popout state machine and cover missing widgets
fixes #3248
2026-09-01 10:22:40 -04:00
bbedward f273a64703 session: run custom power commands outside the shell's cgroup
fixes #3250
2026-09-01 09:43:08 -04:00
bbedward 9ea8d2015b polkit: authenticate as the current user when multiple admins are candidates
fixes #3251
2026-09-01 09:13:12 -04:00
purian23 0604de881e fix(cursor): adjust env variable settings for cursor size handling
Closes #1269
2026-08-31 22:52:23 -04:00
Bernardo Gomesandclaude[bot] 16c098d09b fix(doctor): validate QT_QPA_PLATFORMTHEME instead of trusting its presence (#3240)
* fix(doctor): validate QT_QPA_PLATFORMTHEME instead of trusting its presence

dms doctor reported any non-empty QT_QPA_PLATFORMTHEME as healthy, so a
wrong value passed with "All checks passed!" while every Qt app silently
ran without a platform theme: palettes stayed unstyled and QIcon lookups
returned nothing, which is how Quickshell ends up drawing its missing-
icon placeholder for every icon an SNI app such as blueman publishes in
tray menus. The real-world trap is qt6ct-kde — the AUR package name —
set as the theme value; the settings UI toast already documents that
only the qt6ct theme requires the qt6ct-kde package, but the doctor,
the tool meant to catch exactly this, blessed the broken value.

- checkQtPlatformThemePlugin validates both QT_QPA_PLATFORMTHEME and
  QT_QPA_PLATFORMTHEME_QT6 (the latter was not reported at all):
  qt6ct-kde gets a targeted warning naming the fix, and any other value
  has its plugin probed the way the qtengine check does — qmake/qtpaths
  are asked for QT_INSTALL_PLUGINS and the conventional file names are
  looked up, warning with the exact expected path on a miss
- a miss never warns when it cannot be sure: no Qt build tools and the
  version-dependent plasma-integration file name both degrade to info,
  mirroring the qtengine check's no-false-alarm policy
- the plugin-root interrogation moves into qtPluginRoots, shared with
  the qtengine check

On an affected system:

  before: ○ QT_QPA_PLATFORMTHEME · qt6ct-kde
          ✓ All checks passed!

  after:  ○ QT_QPA_PLATFORMTHEME · qt6ct-kde
          ● qt6ct-kde            Package name, not a theme

* fix(doctor): probe qtengine plugin with a bare file name

qtenginePluginFile("", major) returns a path with a directory
component, and qtPluginPath joins platformthemes/ again, so the
qtengine check stat'd
<root>/platformthemes/platformthemes/libqt6engine-plugin.so and warned
"Not found" on every correctly installed system. Pass the bare file
name instead, and pin the resolved path with a regression test that
fails on the double join.

Found in review by claude[bot].

* Update core/cmd/dms/commands_doctor.go

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-08-31 21:04:56 -04:00
bbedward 99c5f96575 bluetooth: toggle power from the adapter's live state,
fixes #3242
2026-08-31 19:55:31 -04:00
bbedward 7cc0955358 i18n: sync, new polish terms from @raikiu-source 2026-08-31 19:37:13 -04:00
bbedward 285ce66d99 update README video 2026-08-31 14:20:45 -04:00
Mario Mohar 887509548b fix(theme): take sun times from the local calendar day, not the UTC one (#3237)
* fix(theme): take sun times from the local calendar day, not the UTC one

CalculateSunTimesWithTwilight read its reference day off date.UTC(). The
event seconds it computes are local mean solar time and lonOffset converts
them to UTC, so the instant of a local day's sunrise lands on the previous
UTC day east of Greenwich and a local sunset on the next one west of it.
Whenever the local and UTC calendars disagree, the function therefore
returned a neighbouring day's times.

Everything around it already works in local calendar days: the polar
branches of CalculateSunTimes build their day from date.Year/Month/Day,
thememode's startOfNextDay uses next.Location(), and the gamma scheduler
keys its calcDay cache on now.Location(). Only the reference day inside
the calculation disagreed.

Effect at UTC+8 (Kunming, 25.04N 102.72E): between local sunrise and 08:00
local, computeLocationSchedule got yesterday's sunrise and sunset, both
already past, fell through to the next-day branch and got today's sunrise,
also past. It reported dark with nextTransition in the past, which
schedulerLoop clamps to a one-second wait, so it re-evaluated every second
until 08:00 local and then flipped to light on its own. West of UTC the
mirror image: at UTC-7 the mode went dark from 17:00 local, when the UTC
date rolls over, until the real sunset.

The same SunTimes drive the gamma/night-light scheduler, so night light
shifted by a day the same way.

Fix: derive the reference day from date's own calendar day, keeping
dayStart anchored at UTC midnight so the lonOffset arithmetic is
unchanged, and take the orbit angle from the same day. Callers whose date
is already in UTC are unaffected.

Tests: suncalc_refday_test.go covers the reference day east and west of
UTC, stability across UTC midnight, and that UTC callers do not move.
manager_location_test.go is new to the thememode package and covers
computeLocationSchedule at both offsets, including that nextTransition is
never in the past.

* fix(theme): pick the reference day from the solar day, not the civil one

Taking the reference day straight off the caller's calendar day is right
for nearly every zone, but not for one shifted a full day away from its
own meridian. Apia, Kiritimati, Tongatapu and the Chathams all sit on
west longitudes while keeping a UTC+12:45..+14 offset, so their mean
solar time and their civil time are a day apart. Every sun event landed
on the following local day there, and the location schedule reported
dark around the clock.

Anchor on the caller's local noon and convert that to mean solar time to
pick the reference day. For every other zone this resolves to the same
date as before, so Kunming, Los Angeles and London are unchanged to the
second. dateOrbitAngle now takes the same reference day, so declination
and the equation of time no longer belong to a different day than the
events computed from them.

Covered at both levels: three antimeridian zones in the suncalc test,
and two cases in the location schedule test for the symptom itself.
Both fail without this change.
2026-08-31 13:26:39 -04:00
bbedward c580aa26c0 shell: fix media text collapse, stale plugin cc detail, theme browser open 2026-08-31 13:19:58 -04:00
Mario Mohar e1b2279a70 fix(keybinds): show hl.unbind + hl.bind overrides from user Lua modules (#3238)
In Hyprland a second bind on an already bound key does not replace the
first, both stay live, which is why an override is written as hl.unbind
followed by hl.bind. The parser dropped that pair on the floor when it
came from a user's own Lua module:

- parseLuaLines discarded the unbind whenever the same file rebound the
  key, so the DMS default was never removed
- addBind then treated the rebind as a config bind conflicting with a DMS
  bind, counted it in bindsAfterDMS and returned false, so it never
  reached the section

The keybinds modal therefore showed the DMS default for that key and the
user's actual action was nowhere, which is what #3145 reports.

The discard existed to protect dms/binds-user.lua, the file the settings
modal writes, which uses the same unbind+bind pair: recording its unbind
would have deleted its own rebind too, since that file is also a DMS bind
source. The guard was just too coarse, suppressing the unbind entirely
instead of exempting the one bind it belongs to.

Now the unbind is always recorded, and rebindSources remembers which file
unbound and rebound a key so removeUnboundDMSBinds spares that file's own
bind. addBind treats a bind as an override, not a conflict, when the same
file unbound the key earlier — tracked per file in fileUnbinds, saved and
restored around each parsed file so an include cannot leak into its
parent.

A config bind without a preceding unbind is unchanged: both binds really
are live in Hyprland, so it stays a conflict and keeps counting towards
"Some DMS binds may be overridden by config binds".

No other provider has an unbind concept; mangowc, niri and sway parse
formats without one, so this is Hyprland-only.

Tests: hyprland_parser_override_test.go covers the reported case, the
conflict case without an unbind, a bare unbind with no rebind, and the
dms/binds-user.lua round trip. Only the first fails without the fix; the
other three pass either way and guard against over-correcting.
2026-08-31 10:40:41 -04:00
Mario Moharandbbedward 9eaaed3203 feat(clipboard): clear only the filtered entries (#3235)
* feat(clipboard): clear only the filtered entries

With a search query or type filter active, the clear button deleted
every unpinned entry regardless of what the list was showing, so
pruning a subset meant deleting entries one by one from the context
menu.

When the recents list is filtered, the button now clears exactly the
entries it shows and the modal stays open on that filter; with no
filter the previous behaviour is untouched. A filter that matches
nothing does nothing at all, rather than falling back to clearing
everything. The tooltip says "Clear Filtered" while a filter is
active, and the confirmation names the count.

Deleting one id at a time would have cost a round trip and a full
state broadcast per entry, so this adds clipboard.deleteEntries: one
bolt transaction, one state update, and it skips pinned entries
server-side so a bulk delete can never take out saved items.

Fixes #3198

* fix(clipboard): add translator context and list deleteEntries in IPC help

Review follow-up.

The two strings this branch adds carried no translator context: the
"Clear Filtered" tooltip in ClipboardHeader and the confirmation body for
a filtered clear in ClipboardHistoryContent. Both now say where they
appear, and the second one names what %1 is, per CONTRIBUTING.

"Clear All" on the same line is deliberately left alone. It is an
existing shared term with four other call sites, so a context there would
attach a clipboard-specific comment to a term the printer tab also uses.

clipboard.deleteEntries was handled but absent from the IPC method
listing that `dms debug-srv` prints, which left it undiscoverable right
next to clipboard.deleteEntry.

Catalogs untouched.

* chore: re-trigger CI

The lint-and-test failure on 21cefbf is an unrelated flake:
TestStoreEntry_CorruptDBReturnsErrorNotPanic died with SIGBUS in the
bbolt mmap. That test comes from master (c68b7e5) and passes there;
this branch only adds TestDeleteEntries_*/TestHandleDeleteEntries_*
and touches manager.go additively.

* clipboard: survive mmap faults from corrupted db pages

---------

Co-authored-by: bbedward <[email protected]>
2026-08-31 10:04:26 -04:00
bbedward 5c552f97e4 theme: add widgetInactiveIconColor token, apply to volume 0 2026-08-31 09:36:51 -04:00
Spyros Psarras 212271291c feat(battery): show charge/discharge rate in the bar widget (#3210)
The battery popout already surfaces the UPower energy rate, but the bar
widget could only show percentage and remaining time, so telling how fast
the battery is charging or draining meant opening the popout (or running a
third-party plugin that polls /sys/class/power_supply).

Add two independent, opt-in toggles to the battery widget - "Show Charge
Rate" and "Show Discharge Rate" - that render the signed rate next to the
existing text, e.g. "+28W" while charging and "77% (2h 11m) -19W" while
draining. Both default to off, and follow the established pattern: a global
default in SettingsData plus a per-widget-instance override.

The value comes from BatteryService.changeRate, so there is no extra process
or polling. It hides itself when the rate is below 0.05W (idle, fully
charged, or sitting at a charge limit) so it costs no bar space unless it
has something to say. Precision adapts to keep the pill narrow: one decimal
below 10W, whole watts above; vertical bars get a whole-watt line.
2026-08-31 09:16:28 -04:00
bbedward 905726547d setup: add headless subcommand for non-interactive installs
ref #2851
2026-08-31 09:15:59 -04:00
bbedward 4624ceb8f0 firststart/greeter: add a step for plugins 2026-08-31 09:15:59 -04:00
purian23 7372b79c2d feat(lockscreen): allow to enable/disable of weather
Closes #3153
2026-08-30 21:45:02 -04:00
dms-ci[bot] 9522049881 nix: update vendorHash for go.mod changes 2026-08-31 01:16:07 +00:00
bbedward ac46c7dd1b dankinstall: refactor tui package, single-source selection state 2026-08-30 21:13:53 -04:00
Huỳnh Thiện Lộc 4cdfc53dcd fix(dock): hide overlay dock over fullscreen (#3224)
* fix(dock): hide overlay dock over fullscreen

* fix(dock): track fullscreen state declaratively

* fix(dock): clear overlay mask during fullscreen

* feat(dock): add fullscreen visibility option

* fix(dock): preserve auto-hide fullscreen behavior

* fix(dock): keep fullscreen mask interactive in overview

* fix(frame): host dock on overview without show dock
2026-08-30 14:36:34 -04:00
Scott Steelyandclaude[bot] 393a54f7eb feat(mpris): pin preferred media source (#3150)
* feat(mpris): pin preferred media source

Add a minimal "pin media source" option so the bar media widget and OSD
keep showing a preferred player (e.g. a music player) even when another
source (e.g. YouTube in the browser) starts playing.
 - MprisController: new pinnedIdentity property + setPinned(identity);
  _bestPlayingPlayer() prefers a playing pinned player (exact identity
  match, then case-insensitive substring fallback). Existing behavior
  is unchanged when no pin is set.
- Persistence via SessionData.pinnedPlayerIdentity (spec entry added so
  SessionData.set() accepts the key); empty string clears the pin.
- MediaDropdownOverlay (Media Players panel): each player row gets a
  theme-colored circle indicator on the right. Empty circle = no pin
  (initial state). Right-click a row to make it the default source
  (circle fills); right-click another row to move the pin; right-click
  the pinned row again to clear (back to empty). Left-click behavior
  unchanged (selects the player).

* Update quickshell/Services/MprisController.qml

Concur with Claude's suggestion.

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

* Update quickshell/Services/MprisController.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-08-30 14:30:23 -04:00
bbedward 151aedbb68 display: merge missing hyprland fields into existing session entries
fixes #3221
2026-08-30 14:19:54 -04:00
bbedward 9628628d38 fix(island): click through empty strip space when floating
fixes #3227
2026-08-30 11:30:07 -04:00
bbedward c68b7e5333 clipboard: survive corrupted bbolt db
fixes #3232
2026-08-30 11:16:36 -04:00
purian23 9bd31e30f4 fix(battery): keep last good charge through unknown/zero readings
Closes #3084
2026-08-29 20:50:35 -04:00
Nat Fletcher 7f73a81c59 updated unit for edge spacing, exclusive zone offset, and size to 'px' (#3228) 2026-08-29 18:48:06 -04:00
bbedward b8cceb7b3a gamma: configurable transition duration for time-based night mode 2026-08-29 16:23:48 -04:00
bbedward ce879a6219 i18n: sync 2026-08-29 15:55:16 -04:00
shalevc1098andshalevc1098 22ff24b0e4 keyboard layout widget: refresh once compositor detection lands (#3225)
CompositorService detects the compositor asynchronously: compositorInitTimer
waits 100ms before calling detectCompositor(), which shells out to identify
the Wayland socket owner and only sets the compositor flags in its callback.

KeyboardLayoutName gates its initial updateLayout() on CompositorService.isHyprland
in Component.onCompleted, which always runs before that lands, so the call never
happens and the widget renders with an empty label until the first activelayout
event, i.e. until the user switches layout by hand.

Re-run updateLayout() when the detected compositor changes. updateLayout()
already branches on the compositor internally, so one handler covers every
backend.

Co-authored-by: shalevc1098 <[email protected]>
2026-08-29 15:44:30 -04:00
bbedward e99813b07d network: classify AP security by the privacy bit
fixes #3218
2026-08-29 15:40:53 -04:00
08dde7f79a add: Cellular Network support (rebase of #2765) (#3096)
* feat: add cellular support

* fix(settings): resolve cellular tab index collision

* fix: address review feedback for cellular support

Bump the network API version, drop the deleted term freeze file,
regenerate the settings search index, and reuse existing translation
terms where they already cover the new cellular UI.

* fix: do not assign readonly userPreference when switching network type

Assigning SettingsData.networkPreference through the readonly alias
threw on click, so the Cellular segment in Control Center did nothing.

* fix: make Cellular selectable and the modem row clickable

The Control Center type switcher was bound only to preference, so
Cellular snapped back to Ethernet. The settings modem row highlighted
on hover but had no click handler. Show the modem as a connect target
when no APN profiles exist.

* fix: create a GSM profile when connecting cellular with none saved

Clicking Mobile broadband only activated an existing profile, so a
modem with no APN did nothing useful. Fall back to AddAndActivate
like ethernet connect.

* fix: address second round of cellular review feedback

- Guard cellularDevices map with devMutex via snapshot/by-iface/set/remove
  helpers, mirroring the existing ethernet and wifi accessors
- Regenerate mocks with mockery v3 instead of hand-editing mock_Backend.go
- Reuse refreshCellularState() in SetCellularEnabled, DisconnectCellular,
  DisconnectCellularDevice, ActivateCellularConnection and device removal
- Wrap retryErr rather than the first error when the auto-config retry fails
- Use Theme.surfaceTextSecondary/primaryHover/primaryHoverLight in the
  cellular-off state instead of hand-rolled Qt.rgba values
- Drop the duplicate "No devices found" label so it can no longer render
  twice with no modem and no saved config

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

* fix: address remaining cellular review comments

- Toast "Failed to toggle cellular" instead of always "enable"
- Remove unused enableCellularRadio() wrappers
- Store Control Center network type as a value, not a list index

* fix: pass nmcli connection selector as separate argument

`nmcli con mod uuid:<UUID>` is not valid syntax -- nmcli expects the
selector keyword as its own argument, so every priority update failed
with "no such connection profile". Because practically every NM profile
has a UUID, this silently disabled autoconnect-priority and route-metric
updates for ethernet and WiFi as well as cellular.

Also fix the Cellular settings status text, which said "Connecting..."
for a flag that only tracks radio enable/disable, and add the seven
cellular terms that were missing from the translation catalogs.

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

* apply review comments

* fix lints

* update debug doc

* fix style inconsistencies

---------

Co-authored-by: Yechiel Worenklein <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: bbedward <[email protected]>
2026-08-29 15:37:36 -04:00
bbedward 6ce86a07a5 displays: round-trip hyprland HDR metadata and dedupe whitespace-duplicated outputs
fixes #3221
fixes #3219
2026-08-29 15:24:34 -04:00
Huỳnh Thiện Lộc f29a48f681 fix(popouts): close hover surface on click (#3223)
* fix(popouts): close hover surface on click

* fix(hover): preserve popout retargeting
2026-08-29 13:00:42 -04:00
Huỳnh Thiện Lộc feab79df83 feat(settings): improve battery status layout (#3103)
* feat(settings): improve battery status layout

* fix(settings): shorten battery power label

* refactor(settings): remove duplicate power source

* chore(settings): update search index

* fix(settings): combine battery estimate time

* fix(settings): address battery layout review

* fix(settings): keep battery fallback translatable

* fix(settings): mirror battery progress fill
2026-08-29 13:00:19 -04:00
Huỳnh Thiện Lộc 754040ca42 feat(lock): add vertical clock style (#3220)
* feat(lock): add vertical clock style

* fix(lock): center single-digit vertical hours
2026-08-29 12:28:32 -04:00
bbedward d272e7d9da greetd: re-add embedded greeter for archinstall compatibility 2026-08-29 12:28:05 -04:00
purian23 104bbebbc2 ipc: stop cups capability churn from re-dialing the subscribe socket
ref #3140
2026-08-29 00:46:50 -04:00
purian23 3306772f97 fix(island): fix ghost widgets & extended clicks on near island/screen edges 2026-08-28 21:17:36 -04:00
bbedward 8354113c34 popout: resize on bezier curves instead of springs for already open
content
2026-08-28 16:15:05 -04:00
bbedward bada28ac4b nix: restore pre-wrap qt env for launched apps
fixes #2039
2026-08-28 16:15:05 -04:00
JDKamalakar f0867798e6 dankinstall: add non-interactive install script (#3063)
fixes #3043
2026-08-28 15:23:46 -04:00
bbedward 2f19ccf707 ipc: add dumpSession command to settings target 2026-08-28 14:58:42 -04:00
bbedward ae3acb2462 settings: remove dead nightModeEnabled key 2026-08-28 14:58:42 -04:00
Huỳnh Thiện Lộc f1bbb97b2a feat(clipboard): accept dropped content (#3211)
* feat(clipboard): accept dropped content

* fix(clipboard): address drag drop review

* fix(clipboard): preserve multi-file drops
2026-08-28 14:41:28 -04:00
bbedward 97b5641f67 audio: search the whole sound theme tree for event sounds
fixes #2211
2026-08-28 14:08:52 -04:00
bbedward 9f136da2e1 network: disconnect wifi from the associated adapter
ref #1921

looks like the rest of 1921 is more or less addressed on master, just
fixed a dissconnect gap
2026-08-28 14:03:40 -04:00
bbedward 93a7c17e59 launcher: add built-in power options plugin to launcher 2026-08-28 13:57:29 -04:00
bbedward 618ad1135d perf(screenshot): add parallel JPEG encoder 2026-08-28 12:02:14 -04:00
bbedward 7aa1b806d2 weather: show fetch fail reasons 2026-08-28 10:44:50 -04:00
bbedward 4aac96c845 popouts: snap geometry during entrance for all popouts 2026-08-28 10:40:51 -04:00
Huỳnh Thiện Lộc fdabf949f1 fix(clipboard): stack clear dialog above modal (#3207) 2026-08-28 10:22:02 -04:00
bbedward 4c90a36ecf ci: remove symlinks
closes #3208
2026-08-28 10:21:37 -04:00
Huỳnh Thiện Lộc f0753dcfd8 fix(settings): keep search result visible (#3209)
* fix(settings): keep search result visible

* fix(settings): keep search field visible
2026-08-28 10:17:59 -04:00
bbedward 2c471e1c9e bar/focused-app: clip title overflow horizontally only
fixes #3203
2026-08-28 10:17:26 -04:00
bbedward cef1eb962e powermenu: add popout variant for bar widget
fixes #3199
2026-08-28 10:17:26 -04:00
bbedward 00f7db98b5 proc: forward owner and release through the wrapper
fixes #3201
2026-08-28 10:17:26 -04:00
bbedward 07f89e314f audio: optional stream nodes in device lists
fixes #1672
2026-08-28 10:17:26 -04:00
bbedward 54a64eb0b2 plugins: don't warn for directories without a manifest
fixes #3112
2026-08-28 10:17:26 -04:00
bbedward 3604fc83a2 brightness/ddc: add an option to disable DDC probing 2026-08-28 10:17:26 -04:00
bbedward 3538bdb9cd theme: disambiguate duplicate matugen scheme labels
fixes #3154
2026-08-28 10:17:26 -04:00
bbedward 1486eafa75 lock: reset fingerprint retry backoff on resume
fixes #3171
2026-08-28 10:17:26 -04:00
Huỳnh Thiện Lộc d8ceb12164 feat(spotlight): align tab navigation with full launcher (#3205)
* feat(spotlight): align tab navigation

* fix(launcher): wrap reverse action navigation

* fix(launcher): reset spotlight actions on selection
2026-08-28 01:18:27 -04:00
Huỳnh Thiện Lộc 23faed0bd4 feat(launcher): cycle modes with ctrl-tab (#3204) 2026-08-28 01:00:43 -04:00
purian23 95e617fa95 fix(matugen): detect nix profile bins & write pywalfox colors.json
Closes #2821
2026-08-27 23:57:39 -04:00
purian23 e620677f97 fix(weather): skip reverse geocoding when a location name is configured
Fixes #2430
2026-08-27 21:26:58 -04:00
bbedward 030974ce75 config: write hyprland session target when deploying systemd configs
fixes #3110
2026-08-27 16:19:56 -04:00
bbedward 9255596b08 widgets/keyboard layout: use NumericText
fixes #2679
2026-08-27 16:09:14 -04:00
bbedward 48e4709eb5 calendar: add default task list setting for dash quick-add
fixes #3134
2026-08-27 14:57:16 -04:00
bbedward b97c9b0b9a plugins: load desktop widget settings via file url
fixes #3165
2026-08-27 14:24:18 -04:00
bbedward 17d535b7a4 settings: rebuild modal when window dies
systemd: add LimitNoFile to dms.service

ref #3140
2026-08-27 14:14:04 -04:00
bbedward 52f393d821 displays: floor logical output sizes on niri
ref #2526
2026-08-27 13:03:17 -04:00
bbedward c0c2c73e8f wallpaper: don't drop a change back to the displayed image mid-transition 2026-08-27 12:48:49 -04:00
bbedward 460013a77d gtk: copy adw-gtk3 into user themes for dynamic theming
ref #2523
i with '#' will be ignored, and an empty message aborts the commit.
2026-08-27 12:28:05 -04:00
bbedward 8fabf22e3d i18n: sync 2026-08-27 12:07:44 -04:00
bbedward f8cf761fa6 notifications: prefer the icon theme for path app_icons
fixes #2746
2026-08-27 11:28:18 -04:00
bbedward 78352fd1fc re-handwrite AGENTS.md instead of the generated junk 2026-08-27 10:55:59 -04:00
bbedward dca1bd3591 network: report wifi state from the associated adapter
fixes #2460
2026-08-27 10:37:57 -04:00
bbedward 35aa1329e8 network: treat link aggregates as ethernet in the status mapping
fixes #1581
2026-08-27 10:28:13 -04:00
bbedward ed60fb7e4f trayrecovery: rescan when the StatusNotifierWatcher changes hands
fixes #1142
2026-08-27 10:07:27 -04:00
bbedward 2e8257661c doctor: flag config files that fail to parse 2026-08-27 10:01:12 -04:00
bbedward fdffc61d39 icons: probe the system theme when DMS has none set
fixes #1132
2026-08-27 09:56:32 -04:00
bbedward 530c58b928 island: scale satellite background to content in near mode
fixes #3191
2026-08-27 09:49:31 -04:00
bbedward 12df4bdb8f feat(screenshot): allow dragging region across outputs, add alt modifier
to restrict to one output
2026-08-27 09:49:31 -04:00
bbedward 80c85bf929 frame: keep the frame on Hyprland special workspaces
fixes #3156
2026-08-27 09:49:31 -04:00
bbedward fc42786fec settings: scroll the sidebar with the keyboard highlight
fixes #3175
2026-08-27 09:49:31 -04:00
bbedward be9aeaefbc plugins: persist ColorSetting as a color string
fixes #3164
2026-08-27 09:49:31 -04:00
bbedward 8d36628df1 desktop-widgets/sysmon: track option changes in dgop module refs
ref #3107
2026-08-27 09:49:31 -04:00
bbedward 422dd2c9b0 auth: write the "other" PAM fallback
fixes #3062
2026-08-27 09:49:31 -04:00
bbedward d81aca468e bar: release the input region while hidden for the niri overview
fixes #3000
2026-08-27 09:49:31 -04:00
bbedward 9ed4acd4f8 network: connect to OWE and OWE transition-mode networks
fixes #2458
2026-08-27 09:49:31 -04:00
haikalllp 42080db61b feat(theme): add support for matugen smart mode and type (#3109)
* feat(theme): add support for matugen smart mode and type

* add another dry run to get smart type preview

* fix too much keywords in search index

* revert dont extract translations

* feat gate "Auto From Wallpaper" toggle row and the available matugen scheme for the pallete drop down

* re-extract settings search index

* make portal sync no longer disables matugen smart mode

* make sure cosmic theme mode also follow change from smart mode

* add translator context for Auto (Light Mode) / Auto (Dark Mode) / Auto From Wallpaper

* fix: gate matugen smart mode checks on the dynamic theme so stock themes keep portal sync and fix tile labels in control center
2026-08-27 09:40:23 -04:00
Vladimir b7034fcadb fix(bar): prevent focused app overlap (#3190) 2026-08-27 09:28:44 -04:00
elel-code d01a6d87d5 fix(matugen): honor config dir for qtengine sync (#3195) 2026-08-27 09:14:10 -04:00
2934188296 fix(suncalc): parenthesise the sunrise-equation denominator (up to 24 min error at high latitude) (#3192)
* fix(suncalc): parenthesise the sunrise-equation denominator

`sunHourAngle` computed

    cos(z) / cos(lat) * cos(dec) - tan(lat) * tan(dec)

but the sunrise equation is

    cos(H) = cos(z) / (cos(lat) * cos(dec)) - tan(lat) * tan(dec)

In Go `a / b * c` is `(a / b) * c`, so `cos(dec)` multiplied where it should divide.

The new test asserts a property rather than a formula: feeding the returned hour
angle back through the definition of the zenith angle,

    cos(z) = sin(lat)*sin(dec) + cos(lat)*cos(dec)*cos(H)

has to reproduce the zenith angle that was requested. It fails on the old expression
and passes on the corrected one, over a grid of latitudes from the equator to the
polar circle, declinations across a full year, and both zenith targets the callers
use (elevTwilight -6, elevDaylight 3).

Worst-case error before the fix, in clock time:

    Singapore    lat  1.35     285 s
    Mexico City  lat 19.43     312 s
    Cairo        lat 30.04     354 s
    New York     lat 40.71     439 s
    Trieste      lat 45.69     512 s
    London       lat 51.51     671 s
    Oslo         lat 59.91     654 s
    Tromso       lat 69.65    1443 s

The error grows with latitude and with |declination|, and is largest at the civil
twilight zenith — which is where the gamma ramp starts and ends.

It also moves the boundary at which Acos leaves its domain, i.e. where the code
decides midnight sun / polar night. Swept over 505062 (lat, dec, zenith) points, the
classification differs at 961 of them (0.190%), all beyond 88 degrees of latitude.

The existing suncalc tests keep passing: they assert sunrise and sunset to the hour,
which is two orders of magnitude coarser than this error.

* fix(suncalc): classify each solar threshold independently

---------

Co-authored-by: Mark2Mac <[email protected]>
Co-authored-by: bbedward <[email protected]>
2026-08-27 09:13:58 -04:00
purian23 25e95d597c feat(dankisland): render notification severity as a border accent 2026-08-27 01:26:10 -04:00
purian23 0091e703a3 fix(BatteryService): send distinct D-Bus urgency for low vs critical battery 2026-08-27 01:25:50 -04:00
dms-ci[bot] 64d0e40baf nix: update vendorHash for go.mod changes 2026-08-27 03:46:06 +00:00
purian23 c653378db7 imp(systemd): support successful managed restarts
- Improve overall systemd handling and add further BSD support

Fixes #3193
2026-08-26 23:43:18 -04:00
Tássio Virgínio 3b527f1f7f SystemTrayBar: add configurable icon spacing (#3183)
Adds a user-configurable spacing between system tray icons in the bar,
available per widget via the tray context menu in Settings > Widgets
("Icon Spacing" stepper, 0-20px), with a global default fallback.

- SettingsData/SettingsSpec: new trayIconSpacing setting (default 0)
- SystemTrayBar: apply configuredIconSpacing to the horizontal Row and
  vertical Column layouts, and account for it in auto-overflow item
  estimation and drag-and-drop slot math (shift offsets and target
  index calculation)
- WidgetsTabSection: "Icon Spacing" stepper in the tray widget context
  menu, following the existing "Max Visible" pattern
- WidgetsTab: keep trayIconSpacing when restoring per-widget data
2026-08-26 14:59:09 -04:00
bbedward 662dffc141 core: drop curl dependencies, weather: bump API timeouts (#2430) 2026-08-26 14:58:17 -04:00
bbedward 67968aef79 notifications: add option to execute default action on body click
instead of expanding text

fixes #2425
2026-08-26 14:58:17 -04:00
bbedward f91b43b29d shellcore: skip surface recovery while only a placeholder screen exists
related #3057
2026-08-26 14:58:17 -04:00
Vladimir 5b8f083fe5 fix(tray): prefer tooltip title in menu (#3181) 2026-08-26 08:01:48 -04:00
bbedward ea0b158eb8 bar/island: fix click targets on satellites 2026-08-25 20:54:39 -04:00
bbedward 6feac1f79e bar/island: tweak spacing of satellite widgets 2026-08-25 17:48:32 -04:00
Vladimir 34cf6e6346 feat(control-center): show Bluetooth battery in quick tile (#3160)
Display the selected device as `name • N%` when battery information is available, while preserving the existing name-only fallback.
2026-08-25 17:43:09 -04:00
bbedward c8ec045a3c bar/island: add background options for satellites, transparency, blur.
Bigger click targets
2026-08-25 15:54:57 -04:00
bbedward d9462c5484 battery: more style adjustmetns to horizontal pill
fixes  #3177
2026-08-25 13:35:04 -04:00
bbedward 778efb832e qml: bump qml-common reference 2026-08-25 09:02:09 -04:00
purian23 195ec3bf6e feat(motion): spring-based physics & shell hybrid material animations
- New Spring Motion settings (bounce character, reduce motion)
- Reorganize Typography & Motion tab
2026-08-24 22:41:02 -04:00
zion 7802572193 fix(launcher): support app id and genericName matching in DankLauncherV2 scorer (#3122)
Scorer was dropping apps that only matched on id or genericName, even though AppSearchService.searchApplications already matches both. Add id (350) and genericName (prefix 800, contains 400) tiers so the V2 launcher ranks those apps the same way.
2026-08-24 18:53:48 -04:00
Huỳnh Thiện Lộcandbbedward 0bee6a2844 feat(screenshot): drag region with ctrl (#3157)
* feat(screenshot): drag region with ctrl

* fix(screenshot): rehome moved regions

* fix(screenshot): clamp region to active output

* fix(screenshot): fall back to output union when moved region exceeds active output

---------

Co-authored-by: bbedward <[email protected]>
2026-08-24 18:49:29 -04:00
bbedward fb96661092 fix(network): repair 802.1x credential persistence, add EAP method selection 2026-08-24 14:31:59 -04:00
bbedward aabfb080e1 dankbar: set tnum: 1 in Numerictext for CPU/GPU/Ram/Battery widgets
related #3161
2026-08-24 12:41:15 -04:00
purian23 d951dbddbd refactor(island): updated hover/click targets - fitt's law
- New option to dismiss the notfication badge
- Updated dynamic notification widths
2026-08-23 23:51:52 -04:00
bbedward 4cf375f2fd island: add notification counter 2026-08-23 20:25:58 -04:00
bbedward d4069a778b island: pace the face crossfade from the spring, not DMS animations 2026-08-23 19:48:22 -04:00
bbedward ca7b165eb8 battery: use theme font weight in horizontal mode 2026-08-23 18:39:30 -04:00
bbedward c9e6144de8 battery: fix meter legibility with percent sign on the bar 2026-08-23 18:29:55 -04:00
bbedward 4f9b17503e island: stage expanded faces at target size, reserve numeric widths 2026-08-23 18:17:33 -04:00
1065bf07b6 fix(processlist): stop polling when the popout is closed (#3159)
* fix(processlist): stop polling when the popout is closed

The popout's content loader deliberately stays warm across close so
re-opening is instant, and PopoutService only defers the actual unload
until session lock or monitors-off. But both DgopService references were
bound to component lifetime rather than presentation, so after closing
the popout refCount stayed above zero: dgop kept sampling the full
process list every 3s until the shell restarted or the session was
locked and unlocked.

Gate both references on shouldBeVisible instead. Ref grows an `active`
property (defaulting to true, so every other call site is unchanged) and
ProcessesView grows an `active` property that hosts keeping it loaded
while hidden can drive. ProcessListModal is unaffected — its tab loaders
already destroy the view when the window hides.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GTGf29PYTp1kpAv5HNHBzM

* Update quickshell/Modules/ProcessList/ProcessListPopout.qml

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

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: BB <[email protected]>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-08-23 18:11:17 -04:00
bbedward 20b86057d4 island: fix hover-expand getting stuck after slot hover 2026-08-23 18:02:30 -04:00
Huỳnh Thiện Lộc c31fad6381 fix(frame): consume gutter clicks (#3158) 2026-08-23 18:01:54 -04:00
bbedward eadb8cf9c7 island: fix transition regression 2026-08-23 16:10:02 -04:00
bbedward 744fa54ae0 island: resize to notifications based on title size 2026-08-23 14:34:38 -04:00
Huỳnh Thiện Lộc 4d9d3422d2 feat(screenshot): use native cursor shapes (#3155)
* feat(screenshot): use native cursor shapes

* fix(screenshot): reuse cursor shape device

* refactor(proto): generate cursor shape binding
2026-08-23 13:49:02 -04:00
dms-ci[bot] 2ce790bcfe nix: update vendorHash for go.mod changes 2026-08-23 17:24:15 +00:00
bbedward 44e82c6e42 island: break out more stuff into commonality helpers, redesign
horizontal battery, tweak spacing
2026-08-23 13:20:40 -04:00
bbedward 8bff5eb4fc core: use dankgo go-wayland-scanner and update README for proto
generation
2026-08-23 12:51:33 -04:00
bbedward 2c44b794f1 island: general optimizations and cleanups 2026-08-23 11:46:02 -04:00
purian23 5c9dd50c2e add(dankisland): lower min thresholds for compact & reserved heights 2026-08-23 02:57:35 -04:00
purian23 7109a52ed1 fix(DMSShell): cast LazyLoader items so lint-qml can resolve modal methods 2026-08-23 02:30:12 -04:00
purian23 36cfd5faa1 distro(spec): update Go toolchains for Fedora & openSUSE 2026-08-23 01:58:51 -04:00
Peter Fern 60a8310002 feat(dank16): Support full matugen color format output (#3148) 2026-08-23 00:24:17 -04:00
purian23 c57fd7aad1 refactor(Dank Island): update hover delay property defaults
- fix volume pills while notifications are live
2026-08-22 23:58:43 -04:00
purian23 4447d77ba6 feat(Dank Island): add new island bar instance 2026-08-22 22:06:13 -04:00
bbedward f89b21a0ef inputs: consistent usage of Dank* variants, bump qml-common with blink
timeout
2026-08-22 11:53:20 -04:00
bbedward 4a021a73bf fix(shell): re-commit layer surfaces on screen reconnect instead of rebuilding bars 2026-08-22 11:12:47 -04:00
Connor Welsh b482e9b898 test(plugins): isolate git config in the real git client test (#3144) 2026-08-21 22:50:18 -04:00
Connor Welsh 26fb129887 fix(tailscale): back off when the IPN bus read fails (#3142) 2026-08-21 22:45:30 -04:00
Connor Welsh 92d3550dc4 deps: bump tailscale.com to v1.102.0 (#3143) 2026-08-21 22:42:48 -04:00
bbedward c6083d6177 colorpicker: add popout variant for bar widget 2026-08-21 20:24:36 -04:00
bbedward 45cd00bed3 perf(dms): cut daemon idle RAM, threads, and goroutines. Add DMS_PPROF
for performance profiling
2026-08-21 20:24:36 -04:00
bbedward 523e0232d2 keybinds: use dms screenshot in binds configurator 2026-08-21 20:24:36 -04:00
0xNA d22dae1494 feat(wallpaper): add folder-based wallpaper selection and shuffle (#2990)
* feat(wallpaper): add folder-based wallpaper selection and shuffle

* fix(wallpaper): bump 'dank-qml-common' and handle single-wallpaper folders

* fix(wallpaper): address review feedback for folder selection

* fix(wallpaper): persist folder selection across restarts

* fix(wallpaper): pick first wallpaper on folder select and persist folder path

* fix: resolve wallpaper cycling folder persistence issues
2026-08-21 19:09:13 -04:00
Huỳnh Thiện Lộc a669253c58 feat(bar): add focused window details popout (#3139)
* feat(bar): add focused window details popout

* fix(bar): address focused window review

* fix(bar): resolve focused window pid by identity

* fix(bar): refine focused window translations
2026-08-21 14:54:58 -04:00
Huỳnh Thiện Lộcandbbedward 124578b695 fix(screenshot): support multi-monitor region capture (#3132)
* fix(screenshot): support multi-monitor region capture

* fix(screenshot): skip disjoint output overlays

* fix(screenshot): address multi-monitor review

* fix(screenshot): handle cross-output review

* fix(screenshot): snap cross-output selection to one device grid

---------

Co-authored-by: bbedward <[email protected]>
2026-08-21 14:46:36 -04:00
bbedward 3630b46ee3 perf(shell): cut idle RAM by a lot
wallpaper: drop redundant FBOs

startup: create QtMultimedia players lazily on-demand instead of at
startup

launcher: fixed animated images in plugins that use them

dms: set MALLOC_CONF=thp:never,narenas:4,dirty_decay_ms:3000 for
quickshell when unset. jemalloc extets no longer round up to 2 MB
huge  pages

Worth keeping an eye on any potential regressions people encounter
2026-08-21 14:12:39 -04:00
Huỳnh Thiện Lộc 20aafebd87 feat(settings): search runtime plugins (#3136)
* feat(settings): search runtime plugins

* fix(settings): handle plugin search metadata
2026-08-21 11:01:28 -04:00
bbedward 90c5f65b0f fix(workspaces/hyprland): recover workspace mapping on monitor hotplug
fixes #3133
2026-08-21 10:17:26 -04:00
bbedward 589737e318 fix(shell): recover bar surfaces on screen removal and geometry change
fixes #3135
2026-08-21 10:11:11 -04:00
Bernardo Gomes 2f4afeb889 fix(doctor): explain Qt private API mismatch (#3131) 2026-08-20 22:10:07 -04:00
bbedward e6a5fae190 perf(colorpicker): incremental magnifier redraws, frame-paced commits 2026-08-20 19:17:25 -04:00
bbedward 52244c01f7 core: bump go-runewidth to 0.0.28
startup regression has been fixed upstream so dont need to pin the
downgrade anymore
2026-08-20 18:50:17 -04:00
bbedward 44bae0934d perf(cli): lazily init chroma registries
related #3056
2026-08-20 18:47:29 -04:00
bbedward c90b0cddab perf(screenshot): frame-paced, incremental region selector
related #3056
2026-08-20 17:33:35 -04:00
bbedward 486febd4ce perf(screenshot): encode PNG straight from the capture buffer
related #3056
2026-08-20 15:55:03 -04:00
bbedward 47fe1eb512 fix(keybinds): record Shift+digit from the physical key
fixes #3120
2026-08-20 15:03:56 -04:00
bbedward 6a0197d252 fix(accounts): stage profile icon through /tmp before SetIconFile
fixes #3130
2026-08-20 14:59:29 -04:00
bbedward 7f7155b5d5 systemd/dms: add TimeoutStartSec to dms.service
fixes #3125
port 1.5
2026-08-20 14:57:51 -04:00
bbedward 9b04b47e98 notifications: add DnD bypass option globally and per-app
fixes #2870
closes #3016
2026-08-20 14:29:39 -04:00
Callum Wong c694d9938c feat(audio): add output port selection (#2779)
* feat(audio): add output port selection

* fix(audio): handle onShowPortSelector in AudioOutputDetail

* i18n: add translator context for all audio port selector strings

* fix(audio): set isLoading = false in the callback of refreshSinkPorts

* chore: remove unused property in AudioPortSelector

* fix(audio): parse availability correctly from `pactl list sinks`

* fix(audio): address output port review feedback
2026-08-20 13:40:48 -04:00
Kilian Mio b6436196be fix(display): enforce at least one real monitor in profile application (#3106)
ensureEnabledOutput previously checked if ANY output was enabled,
including virtual fallback outputs. A profile with eDP-1 disabled and
FALLBACK enabled was treated as valid, causing the only real monitor
to be turned off during hotplug transitions.

Now ensureEnabledOutput only considers outputs with make+model (real
monitors) when checking for enabled outputs. When all real outputs are
disabled, it enables the first real one. When no real outputs exist in
the profile at all, applyConfigEntry writes an empty config and lets
the compositor handle fallback via its built-in default rules.

Fixes the case where unplugging an external monitor leaves the laptop
display disabled because a stale FALLBACK auto-profile was applied.
2026-08-20 13:07:36 -04:00
fluxwrk 0253f9daa5 fix: update mango named scratchpad keybind (#3118) 2026-08-20 13:06:42 -04:00
Marco MacrìandMark2Mac 521b56f1e1 fix(session): persist the idle inhibitor across shell restarts (#3123)
The idle inhibitor ("Keep Awake") lives only in memory: SessionService holds it
as a plain `property bool idleInhibited: false` and nothing ever writes it out.
Every restart of the shell silently clears it — the user's explicit choice is
dropped with no message and no trace, and the pill just disappears from the bar.

Restarting is not an edge case. `dms restart` is a first-class CLI command, and
the unit shipped in this repo (assets/systemd/dms.service) sets
`Restart=on-failure` with `TimeoutStopSec=10`, so a shell that is slow to stop
is killed and brought back automatically. Each of those paths turns Keep Awake
off without saying so.

Every other Control Center toggle already survives a restart. Measured on
upstream master, over the state read by Modules/ControlCenter/Components/
DragDropGrid.qml and its neighbours:

  doNotDisturb             in SessionSpec: yes
  isLightMode              in SessionSpec: yes
  nightModeEnabled         in SessionSpec: yes
  wallpaperCyclingEnabled  in SessionSpec: yes
  idleInhibited            in SessionSpec: NO

So this follows the doNotDisturb idiom exactly rather than inventing a
mechanism: one spec entry, one property, one setter that saves.

Restore happens through two entry points because the singletons are lazily
created and their order is not fixed: `onLoaded` covers "service instantiated
before the session file is read", `Component.onCompleted` covers the opposite.
enableIdleInhibit() is idempotent, so whichever runs second is a no-op.

The default stays false and SessionStore.toJson omits defaults, so an untouched
session.json gains no new key.

Related: #1483 asks for a configurable timer. The field added here is the same
shape doNotDisturb uses with doNotDisturbUntil, so a later `idleInhibitedUntil`
slots in beside it without another migration.

Co-authored-by: Mark2Mac <[email protected]>
2026-08-20 13:06:26 -04:00
Scott McKendry 0daf24095c fix(plugin): support for symlinked configs (#3090) 2026-08-20 12:36:08 -04:00
Sunlight 7ebe072255 feat(resize-api): add runtime resize functionality for desktop widgets (#3114)
It would be nice to see it documented in the plugin development guide: https://danklinux.com/docs/dankmaterialshell/plugin-development#desktop-plugins
2026-08-20 12:34:25 -04:00
bbedward 7028ea323d ci: add R2 prune job 2026-08-20 12:30:06 -04:00
bbedward 4147d81f01 ci: make review do new comments 2026-08-20 12:28:36 -04:00
Huỳnh Thiện Lộc 0d2215e18c fix(displays): restore scale after revert (#3124) 2026-08-20 12:28:01 -04:00
Huỳnh Thiện Lộc f6259d0ff7 fix(dankbar): consume empty-area right clicks (#3127)
* fix(dankbar): consume empty-area right clicks

* fix(dankbar): preserve click-through reveal
2026-08-20 12:27:06 -04:00
bbedward d10ca38651 ci: add pr write to review action 2026-08-20 10:28:57 -04:00
bbedward 148d8440ec ci: add claude-review CI 2026-08-20 10:19:45 -04:00
bbedward c67450cd2e perf(screenshot): parallel PNG encoder, 4x faster captures
Replace image/png with a hand-rolled writer over klauspost/compress

related #3056
2026-08-20 10:19:38 -04:00
Huỳnh Thiện Lộc 754ebab48d fix(launcher): clip tile labels to rounded corners (#3126) 2026-08-20 08:46:25 -04:00
Huỳnh Thiện Lộc fde8bb6487 feat(battery): show estimated end time (#3117) 2026-08-19 20:44:58 -04:00
bbedward afe0dbb104 CONTRIBUTING: document to not update translation files 2026-08-19 16:25:42 -04:00
bbedward 396d490979 feat(screenshot+colorpicker): support 10-bit colors via
wp_color_manager_v1

Also fix a bunch of unrelated go vet issues
2026-08-19 14:34:56 -04:00
dms-ci[bot] 8bebd68068 nix: update vendorHash for go.mod changes 2026-08-19 16:05:57 +00:00
Connor Welshandbbedward 101841585f fix(dgop): stop the process list opening blank (#3108)
* fix(dgop): merge child processes by default

* fix(dgop): show the process list on first open

parseData threw away the first processes payload and waited 1s for a
second, on the grounds that the first carries no CPU deltas. That holds
for the cpu module, whose prime stays, but the processes sample is
usable as it arrives, so the wait only left the list blank.

* fix(dgop): refetch when the module set changes mid-request

updateAllStats returned early while a request was in flight, and that
request was built from the modules as they stood when it went out.
Opening the process list during the bar's periodic poll got back a
response with no process data, then a full updateInterval of waiting.
Queue a refetch and reissue when the response lands.

* fix(dgop): clear pendingUpdate when no request is sent

* fix(dgop): prime the queued refetch so the cpu cursor spans an interval

* core: bump dgop version

---------

Co-authored-by: bbedward <[email protected]>
2026-08-19 12:03:41 -04:00
bbedward c30ee19ec6 ci: fix caching 2026-08-19 11:59:56 -04:00
bbedward e78951d3c3 plugins/i18n: create mechanism for plugins to define their own
translations. Introduce I18n.trFor() function

fixes #2593
2026-08-19 11:52:32 -04:00
Huỳnh Thiện Lộc 161c597c55 fix(settings): restore control center menu (#3113) 2026-08-19 11:45:06 -04:00
purian23 327aad210c fix(frame): restore overlay connected chrome
- Frame chrome stays on the overlay layer with connected popouts and modals
2026-08-18 17:53:21 -04:00
FaintFlower 084e6ce608 readme: switch star history chart to working mirror (#3088)
The star history chart in the README is currently broken because the upstream provider is affected by GitHub stargazer API restrictions. Point the chart and its link at the star-history.dera.page mirror, which uses a data source that requires no API token, restoring the chart.
2026-08-18 15:52:19 -04:00
Huỳnh Thiện Lộc 262acda336 fix(lock): handle ctrl-backspace (#3087) 2026-08-18 15:51:48 -04:00
Thomas Kroll 5ca033d839 fix(system-update): apply saved updaterIntervalSeconds on daemon connect (#3093)
The dms daemon always initializes IntervalSeconds to its 30-minute
default and has no persistence of its own; the saved SettingsData
value was only ever pushed on manual interaction with the Settings UI
dropdown. Result: every daemon (re)start silently reverts to checking
every 30 minutes regardless of the user's configured interval.
2026-08-18 13:50:45 -04:00
Huỳnh Thiện Lộc d83a0037f3 feat(chooser): edit and copy target (#3094)
* feat(chooser): edit and copy target

* fix(chooser): avoid copy shortcut conflict

* fix(chooser): address review feedback
2026-08-18 13:50:17 -04:00
Marlon Kranz f44b7b9851 fix(trash): keep tests off the machine's real per-mount trash dirs (#3100)
setupHomeTrash redirects HOME and XDG_DATA_HOME, but allTrashDirs also
walks readMountPoints() looking for <mount>/.Trash/<uid> and
<mount>/.Trash-<uid>, and no environment variable affects that walk. So
Count() and List() included whatever the developer had trashed on a
second filesystem, and Empty() deleted it for real: with an orphaned
file under a mount trash (no matching .trashinfo, so the count assert
still saw 3), TestEmptyClearsAll passed and removed the file.

Route the walk through a package-level mountPoints variable and have
setupHomeTrash swap in an empty list, restoring it via t.Cleanup.
Production behavior is unchanged.
2026-08-18 12:46:09 -04:00
dms-ci[bot] 978203cd52 nix: update vendorHash for go.mod changes 2026-08-18 15:53:39 +00:00
bbedward dce1095f8d perf(core): pin go-runewidth to v0.0.25
v0.0.27 builds unicode width LUTs in init() which slows every dms
invocation by ~15ms

Pinning to pre-0.0.27 makes dms ipc invocations  even faster than
invoking the quuickshell binary itself after #3081
2026-08-18 11:49:48 -04:00
Huỳnh Thiện Lộcandbbedward c3fd526698 perf(ipc): call Quickshell directly (#3081)
* perf(ipc): call Quickshell directly

* fix(ipc): handle indexed responses

* fix(ipc): follow error string lint

* fix(ipc): fall back to qs when the direct socket call fails

---------

Co-authored-by: bbedward <[email protected]>
2026-08-18 10:38:06 -04:00
Huỳnh Thiện Lộc d7b19809aa fix(frame): preserve connected popout chrome (#3101) 2026-08-17 17:46:55 -04:00
Huỳnh Thiện Lộc a503e869cf fix(notifications): keep popup above frame chrome (#3099) 2026-08-17 17:28:55 -04:00
Alexanderandbbedward 0100c64d7a feat(niri): close overview on window focus (#3030)
* feat(niri): close overview on window focus

* feat(niri): close overview on window focus

---------

Co-authored-by: bbedward <[email protected]>
2026-08-17 13:35:10 -04:00
Marlon Kranzandbbedward a56360d8ed feat(theme): support qtengine as a Qt platform theme (#3019)
* feat(theme): support qtengine as a Qt platform theme

qtengine is a single platform theme plugin serving both Qt5 and Qt6 from one JSON config, so it needs neither the qt5ct/qt6ct split nor qt6ct-kde.
It reads a KDE .colors scheme by path, which the kcolorscheme template already generates, so no new matugen template is required.

DMS now merges theme.colorScheme and theme.iconTheme into $XDG_CONFIG_HOME/qtengine/config.json after each generate, on icon theme changes, and from Apply Qt Colors.
The merge preserves every other key,refuses to touch a config it cannot parse, and only names a color scheme that exists.
The write is atomic, and its mtime bump is what trips qtengine's watcher and repaints running apps.

QT_QPA_PLATFORMTHEME=qtengine is accepted without the missing-env-var toast, Apply Qt Colors no longer fails on a machine without qt5ct or qt6ct, and dms doctor reports whether the plugin is installed where Qt actually searches, per Qt major.

* fix(theme): resolve qtengine config via XDG, tighten doctor probing

---------

Co-authored-by: bbedward <[email protected]>
2026-08-17 13:23:38 -04:00
Huỳnh Thiện Lộcandbbedward 9abaf4051e feat(dankbar): corner style & bar length padding (#3085)
* feat(dock): attach bar to screen edge

* feat(dock): add bar length padding

* feat(dock): expand bar padding range

* refactor(dock): group bar corner settings

* fix(i18n): avoid attached term variant

* fix(settings): group corner style with goth corners

* fix(dankbar): attach flush bars to the screen edge

---------

Co-authored-by: bbedward <[email protected]>
2026-08-17 13:22:09 -04:00
bbedward 8485d66139 CONTRIBUTING: document mocks + mockery usage 2026-08-17 12:45:03 -04:00
Huỳnh Thiện Lộcandbbedward fc1c9f6c54 fix(frame): align connected chrome layer (#3039)
* fix(frame): align connected chrome layer

Connected chrome now follows overlay modal/popout descriptors so the frame-rendered background stays on the same Wayland layer as the content. This currently moves the frame window as a whole, so hosted bars may follow the overlay layer while such surfaces are presented.

* fix(frame): keep overlay-opted surfaces out of connected chrome

---------

Co-authored-by: bbedward <[email protected]>
2026-08-17 12:42:01 -04:00
bbedward 070090c35a bluetooth: multi-adapter support 2026-08-16 11:36:32 -04:00
Al Amin d067959f1c control-center: route Bluetooth power through backend (#3082) 2026-08-16 11:06:28 -04:00
bbedward 52abc81011 media: gate dash visualizer and position timers, reclaim dash popout
related #2863
2026-08-16 11:02:15 -04:00
purian23 d4fecfcd5c feat(settings): finalize dms settings user theme options 2026-08-15 16:54:00 -04:00
Huỳnh Thiện Lộcandbbedward ed8fbf47b0 perf(screenshot): cache overlay background (#3061)
* perf(screenshot): cache overlay background

* screenshot: restore exact dirty rect, draw HUD at cache rebuild

---------

Co-authored-by: bbedward <[email protected]>
2026-08-15 14:22:48 -04:00
bbedward ba360213dd i18n: sync 2026-08-15 12:32:22 -04:00
Huỳnh Thiện Lộc 214de4a570 fix(dock): close grouped app on middle click (#3064) 2026-08-15 12:28:22 -04:00
Huỳnh Thiện Lộc 8527b5ed0e fix(dock): animate overview reveal in frame mode (#3065) 2026-08-15 12:28:00 -04:00
Huỳnh Thiện Lộc 2ca2e53b6b fix(osd): clamp vertical brightness slider (#3067)
* fix(osd): clamp vertical brightness slider

* fix(osd): keep brightness slider value in sync
2026-08-15 12:27:21 -04:00
Huỳnh Thiện Lộc c93767fdfe fix(launcher): hide spotlight placeholder during IME input (#3071) 2026-08-15 12:26:55 -04:00
Huỳnh Thiện Lộc cb67a5c51a fix(matugen): support Vesktop Flatpak themes (#3075) 2026-08-15 12:26:39 -04:00
Huỳnh Thiện Lộc 7d8532b075 feat(matugen): add Fcitx5 theme template (#3076)
* feat(matugen): add Fcitx5 theme template

* fix(matugen): reload Fcitx5 theme
2026-08-15 12:26:18 -04:00
bbedward fdf0d73b30 displays: apply refresh rate changes without compositor config reload
related #3073
2026-08-15 12:24:10 -04:00
bbedward ef05dbed9b clipboard: release virtual keyboard keys on paste error paths
related #3073
2026-08-15 12:23:25 -04:00
bbedward fb9f00a616 default apps: add mime-type options for file browser 2026-08-14 18:09:39 -04:00
purian23 209cfe664f fix(distros): add Debian OBS targeted version logic
Port 1.5
2026-08-14 17:37:42 -04:00
purian23 faeced373f fix(dankinstaller): update OBS repo handling for Debian distro variants
Fixes #3070
Port 1.5
2026-08-14 17:17:02 -04:00
purian23 6f1bcfd2c2 fix(copr): fix dms-git fedora copr packaging format
Fixes #3059
Port 1.5
2026-08-14 16:45:42 -04:00
Huỳnh Thiện Lộc 8d9c961605 feat(clock): add widget date order option (#3028)
* feat(clock): add widget date order option

* refactor(settings): reuse widget setting signal
2026-08-13 20:38:05 -04:00
Curious 70d0dd8f1d feat(settings): echo location name in location search (#3040)
* feat(settings): persist named location for theme & gamma auto modes

* chore: bump dank-qml-common for location search support

* fix(settings): keep weather location name when search fills coordinates

* style(settings): unify manual coordinates layout with weather
2026-08-13 17:59:37 -04:00
Huỳnh Thiện Lộc 7b9b34b262 feat(dock): separate pinned and running apps (#3025) 2026-08-12 23:30:12 -04:00
Huỳnh Thiện Lộc f83d6c7778 fix(dock): cycle niri overview groups (#3038) 2026-08-12 23:28:40 -04:00
Al Amin 47311e9af7 dankdash: add inline wallpaper search (#3048) 2026-08-12 23:21:06 -04:00
dms-ci[bot] b75a5f2bc1 nix: update vendorHash for go.mod changes 2026-08-12 20:02:19 +00:00
bbedward 87dc1b1ff4 core: bump dankgo version 2026-08-12 16:00:00 -04:00
dms-ci[bot] f6d26251df nix: update vendorHash for go.mod changes 2026-08-12 19:48:19 +00:00
bbedward 6481f61028 core: migrate to wayland client and wlclipboard from dankgo 2026-08-12 15:45:39 -04:00
bbedward 2945b0d6f1 gamma: allow lower values than 2500k
fixes #3026
2026-08-12 12:03:37 -04:00
bbedward 7974887295 settings: diff-only configs, machine-state split, backup CLI
- settings.json/session.json now store only values that differ from
  spec defaults
- Machine-specific state moves out of settings.json into session.json
- Add dms backup create/restore: tar.gz of ~/.config/DankMaterialShell

fixes #3027
2026-08-11 15:32:01 -04:00
bbedward 373a3d3136 power profile: hide when ppd not available
fixes #3031
2026-08-11 10:31:46 -04:00
bbedward 2f27eab949 fix(ipc): widget status distinguishes missing popouts from closed ones
fixes #3032
port 1.5
2026-08-10 18:35:09 -04:00
dms-ci[bot] c2faa0ea1c nix: update vendorHash for go.mod changes 2026-08-10 22:32:53 +00:00
bbedward da664565b1 dgop: drop dgop as an external dependency and use dgop/gops internally
fixes #3033
2026-08-10 18:29:59 -04:00
Huỳnh Thiện Lộc c3c9b13ef6 fix(dock): preserve overview-only visibility (#3029) 2026-08-10 15:24:50 -04:00
Huỳnh Thiện Lộc 422406267e fix(brightness): initialize display state (#3035) 2026-08-10 15:24:35 -04:00
Huỳnh Thiện Lộc ed4cc1da7b feat(clipboard): add overlay layer option (#3037) 2026-08-10 14:57:14 -04:00
dms-ci[bot] db1ab5b7b4 nix: sync flake.lock to dank-qml-common submodule 2026-08-09 21:10:44 +00:00
bbedward b0b9b61593 dock/apps: support minimize 2026-08-09 17:08:43 -04:00
Huỳnh Thiện Lộc 80bad610ce feat(dock): cycle grouped app windows (#3023) 2026-08-08 18:46:48 -04:00
Huỳnh Thiện Lộc b6d051a61f feat(filebrowser): expose folder mode on surface modal (#3022) 2026-08-08 18:14:50 -04:00
bbedward 8e2168ecf8 fix(windowrules): preserve niri exclude clauses
fixes #2996
port 1.5
2026-08-08 13:12:08 -04:00
bbedward 0b526d9b05 fix(hyprland): don't restore focus to a toplevel on another workspace when a popout closes
fixes #2963
port 1.5
2026-08-08 10:58:26 -04:00
Huỳnh Thiện Lộc bde4decc5d feat(theme): add extract dynamic theme to JSON file (#3009)
* feat(theme): add extract current theme to JSON file

Adds an 'Extract' button in Settings > Theme Color that exports the
current theme (generic/dynamic/custom/registry) to a dual-mode dark+light
JSON file via the DMS file save dialog.

* refactor(theme): simplify extract button to single DankButton

* refactor(theme): move extract button inside dynamic theme section

* fix(theme): address review feedback on extract theme save

* refactor: utilize dankActionButton

---------
2026-08-08 00:31:59 -04:00
purian23 3c817dc0d0 fix(doctor): correctly report built-in font status
Port 1.5
2026-08-07 23:36:11 -04:00
bbedward fc90bddad1 fix(frame): honor showOnLastDisplay fallback in bar screen-coverage checks
fixes #2998
2026-08-07 19:59:52 -04:00
bbedward a713485599 fix(geolocation): fetch IP location lazily so startup never contacts ip-api.com
fixes #3008
port 1.5
2026-08-07 19:54:21 -04:00
bbedward f16bdda969 fix(displays): auto-select restores named profiles matching the connected output set
fixes #3012
port 1.5
2026-08-07 18:56:59 -04:00
bbedward cf4f48069f fix(bluetooth): derive codec from locale-independent profile name in bluez-card-profile.lua
fixes #3004
port 1.5
2026-08-07 18:39:23 -04:00
Huỳnh Thiện Lộc 38cca3195f fix(dock): skip creating dock layers when dock is disabled (#3014) 2026-08-07 15:43:38 -04:00
Curious 40a9ca348c feat(settings): add location search to theme & gamma coordinates (#3010)
* feat(settings): add location search to theme & gamma coordinates

* fix(settings): refresh coordinate fields after manual edits
2026-08-07 13:00:37 -04:00
dms-ci[bot] a1ba5cf9ad nix: sync flake.lock to dank-qml-common submodule 2026-08-07 16:51:35 +00:00
purian23 915db2dce7 fix: dank-qml-common for dropdown close 2026-08-07 12:51:06 -04:00
bbedward 1dc0ceea1f i18n: sync 2026-08-07 11:13:48 -04:00
Maxim Levchenko 1a621c3f88 translation (#2980) 2026-08-07 11:12:15 -04:00
Artrixandbbedward b7ec57a534 feat(plugins): add portable plugin lockfile (#2976)
* feat(plugins): add portable plugin lockfile

* plugins/lockfile: allow ssh remotes

---------

Co-authored-by: bbedward <[email protected]>
2026-08-07 10:55:57 -04:00
purian23 a575363f89 fix(notifWidget): restore hand cursor on hover
Port 1.5
2026-08-06 23:44:41 -04:00
David Mirelesandbbedward 3f6cd0b579 feat(registry): support multiple plugin and theme registries via env vars (#2972)
feat(registry): support multiple plugin/theme registries

closes #2763

---------

Co-authored-by: bbedward <[email protected]>
2026-08-06 22:43:44 -04:00
dms-ci[bot] 630e9bd3cd nix: sync flake.lock to dank-qml-common submodule 2026-08-07 00:52:37 +00:00
purian23 9013464312 feat(theme/settings): dedicated floating window styling with shared chrome
- Refactor Widget Styling into Surface Styling & Floating
  Windows
- New sync per-host opacity & foreground layer control
- Shared DankFloatingWindow chrome
- Settings fix to destroy Settings window on-hide so niri floating
  rules match subsequent runs
Related: #2709
2026-08-06 20:52:14 -04:00
Huỳnh Thiện Lộcandbbedward dade416ca0 feat(settings): add keyboard configuration tab for Niri (#2937)
* feat(settings): add dedicated keyboard configuration tab

* chore(settings): configure default keyboard settings and clean up embedded niri config

* settings/keyboard: emit niri keyboard config only when set, dedup setup banner, index tab

---------

Co-authored-by: bbedward <[email protected]>
2026-08-06 18:40:17 -04:00
FormalSnakeandbbedward 401872881e media: add opt-in Apple Music animated album covers (#2918)
* media: add opt-in Apple Music animated album covers

* media: validate itunes match, touch files for cache

---------

Co-authored-by: bbedward <[email protected]>
2026-08-06 14:47:51 -04:00
Huỳnh Thiện Lộc 7cb97f4462 fix(launcher): honor source badge setting (#3002) 2026-08-06 00:33:39 -04:00
Huỳnh Thiện Lộc 82b1b36b1a fix(settings): hide removed widget tooltip (#3003) 2026-08-06 00:33:17 -04:00
FormalSnake e203e76ccc powermenu: support user-defined custom power buttons (#2916) 2026-08-05 20:46:42 -04:00
FormalSnake adcafbcc8e location: add missing subscribe fan-out for location events (#2914)
handleSubscribe forwards every service except location, so neither
subscribe-all connections nor explicit location subscriptions ever
receive location events, and the shell's only source of coordinates
is a single getState pull that races the geoclue TimeThreshold and
loses. Wire location into the fan-out like loginctl/sysupdate:
initial state on subscribe, then manager pushes.
2026-08-05 19:28:33 -04:00
Huỳnh Thiện Lộc 9981fcf529 fix(slideout): yield keyboard focus when modal is open on same screen (#2902)
DankSlideout always held WlrKeyboardFocus.OnDemand regardless of modals,
causing Notepad to steal focus back when Launcher was opened.

ModalManager: clone currentModalsByScreen on mutation so QML bindings
re-evaluate when modals open/close.
DankSlideout: set keyboardFocus to None while a modal is active on the
same screen, letting the modal (launcher) claim focus exclusively.
2026-08-05 19:19:47 -04:00
lankong0522andbbedward 2baf048293 feat(tray): add IPC action to open tray menus (#2701)
* feat(tray): add IPC action to open tray menus

* tray: open IPC-requested menus on the focused screen only

---------

Co-authored-by: bbedward <[email protected]>
2026-08-05 17:31:25 -04:00
bbedward cffc33e14a display config/niri: fix nested kdl blocks as being attributed to output
state

fixes #2966
port 1.5
2026-08-05 17:02:38 -04:00
bbedward 0c811e3417 gamma: validate config before write and publish state every scheduler
wakeup

fixes #2967
port 1.5
2026-08-05 16:55:34 -04:00
bbedward 6ad46cf2c2 bar: fix shadow from tinting goth corners
fixes #2975
port 1.5
2026-08-04 22:15:34 -04:00
Thomas KrollandClaude Opus 5 34626070af fix(tray): don't dismiss click-opened tray menus from hover controller (#2979)
closeHoverSurfaces() called TrayMenuManager.closeAllMenus(), tearing down
every registered tray menu including ones opened by right-click. With
hoverPopouts enabled, moving the pointer after a right-click re-ran the
hit test and closed the menu before it could be reached.

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

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

Fixes #2978


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

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-08-04 21:25:22 -04:00
bbedward 80b27b9a6a settings: remove dead keys
fixes #2977
2026-08-04 21:25:06 -04:00
jack-mil 49f968d26b fix(mpris): remove required non-null artist/title from idle condition (#2993)
The current isIdle detection only works if the player has a title _and_ artist metadata,
which is not appropriate for all cases, e.g. MPV playing a video does not set artist.

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

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

See: https://specifications.freedesktop.org/mpris/latest/Player_Interface.html#Enum:Playback_Status
2026-08-04 21:22:22 -04:00
Huỳnh Thiện Lộc 99b0dc596d feat(wallpaper): support random cycling order (#2936) 2026-08-04 21:21:52 -04:00
rredlip 158c0c12d8 Changed margin of the rectangle so the pixel of the half circle are completely covered. (#2989) 2026-08-04 21:20:53 -04:00
bbedward e089948225 powermenu: add soft reboot option for systemd systems
fixes #2986
port 1.5
2026-08-04 18:04:19 -04:00
bbedward 7f2ba56e06 dank16: correct DPS lab lightness scale
fixes #2987
port 1.5
2026-08-04 17:55:53 -04:00
nozsh 6de5593216 feat(keybinds): add keybind for PAM U2F on lock screen (#2930)
* feat(keybinds): add keybind for PAM U2F on lock screen

* add: tooltip, settings json & error handling

---------
2026-08-03 22:09:53 -04:00
bbedward 365474b0d9 launcher: dispatch plugin-category items as plugin, not app
fixes #2988
port 1.5
2026-08-03 20:05:23 -04:00
bbedward dc8a47644a evdev: resolve capslock state from any available keyboard
fixes #2991
port 1.5
2026-08-03 19:59:12 -04:00
bbedward 27483e68dc dankinstall/gentoo: perform autounmask on install
fixes #2992
port 1.5
2026-08-03 19:40:00 -04:00
purian23 400a18a8ed fix(battery): reliably apply configured power profiles at shell startup
Port 1.5
2026-08-02 19:01:39 -04:00
purian23 32ddf614c3 refactor(media OSD): add previous/next UI controls
Port 1.5
2026-08-02 18:10:12 -04:00
purian23 19d919ed5c refactor(mpris): consolidate equivalent-player resolution & update track matching
Port 1.5
2026-08-02 18:09:18 -04:00
Rubén GarcíaandClaude Fable 5 594a2cde19 refactor(mpris): enrich metadata across equivalent players (#2928)
* fix(mpris): enrich metadata across equivalent players

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

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

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

---------

Port 1.5
2026-08-02 17:34:19 -04:00
Connor Welsh 11287459c3 popout: re-apply surface geometry after shouldBeVisible (#2983)
Popouts sized from their content sometimes open too short and clip what
they render, then come back correct on the next open.

open() snapshots the surface geometry before setting shouldBeVisible,
and the onAligned{X,Y,Width,Height}Changed handlers are all gated on
that flag. Mapping contentWindow happens in between, and that's what
finally runs the layout Qt deferred while the window was hidden.
Content-sized popouts pick up their real height in that gap, so the
correction is dropped and the surface stays short. Re-snapshot once the
flag is set.
2026-08-02 01:30:21 -04:00
bbedward ef191babb7 tailscale: add missing ID
related #2969
port 1.5
2026-07-31 09:37:01 -04:00
bbedward fe64a342f9 network: rescan when network panel is open 2026-07-31 09:33:17 -04:00
bbedward e54be7d12d i18n: sync 2026-07-30 17:11:29 -04:00
bbedward 81c886784b lock: add separate text input for IMEs
related #2950
2026-07-30 17:08:59 -04:00
bbedward 6682bb120c matugen: tweak qt6ct colors
related #2951
2026-07-30 17:08:59 -04:00
bbedward 5c02ec4789 lock: avoid lock at startup firing when using dms-greeter
fixes #2952
2026-07-30 17:08:59 -04:00
bbedward de1e1757c3 keybinds: preserve raw lua actions in parser
fixes #2953
2026-07-30 17:08:59 -04:00
bbedward 43d331d6cf desktop plugins: ensure transparent frame is shown when desktop widgets
disabled

related #2955
2026-07-30 17:08:59 -04:00
01832856d4 fix(network): authenticate Fortinet VPNs natively (#2959)
* fix(network): authenticate Fortinet VPNs natively

* cleanup and add test

---------

Co-authored-by: Grant Mosha <[email protected]>
Co-authored-by: bbedward <[email protected]>
2026-07-30 16:45:53 -04:00
Huỳnh Thiện Lộcandbbedward 0033e3f0e0 feat: add QR Generator launcher (#2941)
* feat: add QR Generator modal with auto-generate on input debounce

- QRGeneratorModal with dual-buffer sequential fade animation
- Auto-generate QR code on typing stop (200ms debounce)
- Clear text button on input field
- Escape key closes modal
- Register modal in PopoutService and AppSearchService
- Go backend for text QR code generation

* fix: add QR Generator toggle to Launcher DMS settings

* qr: also add qrg built in launcher plugin

---------

Co-authored-by: bbedward <[email protected]>
2026-07-30 16:33:36 -04:00
bbedward 2d3706321a frame: fix popout IPCs in some situations
related #2956
2026-07-30 15:30:44 -04:00
bbedward 5bb884db57 keybinds: fix labeling of spawn keybinds
fixes #2957
2026-07-30 15:30:44 -04:00
dms-ci[bot] b42763ccbf nix: update vendorHash for go.mod changes 2026-07-30 19:13:28 +00:00
bbedward f2a6d62d65 matugen: add description for vscode template
fixes #2958
2026-07-30 15:10:54 -04:00
bbedward f66693df6b core: bump dankgo 2026-07-30 14:52:14 -04:00
bbedward a710d6d7cc launcher: fix regex escaping launch args
fixes #2961
port 1.5
2026-07-30 14:33:58 -04:00
bbedward 64461c534f qs/socket: improve resilience of socket detection and connection
related #2369
port 1.5
2026-07-27 13:40:34 -04:00
bbedward 73da4879f6 frame: consolidate and simplify inset logic 2026-07-27 13:10:47 -04:00
euletheia 8594a414ec fix(BatteryService): fix typo in notification check (#2944) 2026-07-27 11:33:04 -04:00
euletheia df396bfa43 fix(BatteryService): fix unresolved icons for notifications and dismiss alerts when plugged in (#2943)
* feat(BatteryService): dismiss sticky critical battery notifications

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

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

Since the previously specified icons are not resolved, instead reuse
icons from DMS first-party plugin for critical battery alerts.
2026-07-27 11:13:47 -04:00
euletheia 33677150b1 feat(ipc): add 'dismiss' function to notifications ipc (#2942)
Allow to dismiss the last visible notification only instead of resorting
directly to dismissAllPopups.
2026-07-27 11:13:07 -04:00
bbedward c67b185076 qs: large sweep of dead code removals 2026-07-26 23:22:22 -04:00
bbedward bab2078dfd cups: fix sub/unsub race
port 1.5
2026-07-26 19:43:17 -04:00
bbedward e9bc0169f6 cc: migrate some settings to cache
port 1.5
2026-07-26 17:45:50 -04:00
bbedward 59c83d19e4 battery: auto-apply power profile at startup when DMS managed
fixes #2935
port 1.5
2026-07-26 15:27:07 -04:00
bbedward 9b7d3c64fe dbar: fix click-through mask of center section
fixes #2938
2026-07-26 15:16:48 -04:00
bbedward c367153bac sysupdate: fix gnome-terminal title 2026-07-26 15:00:40 -04:00
bbedward 1f94c3cbd4 dbar/notifications: fix right click context menu position and add middle
click to DnD
2026-07-26 14:51:51 -04:00
bbedward 564583951c theme: stop overwriting user gtk.css and dconf gtk-theme (#2933) 2026-07-26 14:48:23 -04:00
bbedward eed3617a0d ipc/dash: add openAt/toggleAt for opening popouts at specific locations
fixes #2932
2026-07-26 14:37:17 -04:00
bbedward 24cb3d19a0 display config: preserve config when turning off on niri
fixes #2939
port 1.5
2026-07-26 14:26:00 -04:00
feng-yifan 3182c70857 fix(settings): reset AppBrowserPopup model on close to avoid crash (#2925)
Reopening the autostart "Browse" picker crashed in
QQmlIncubatorPrivate::incubate. Drop the ListView model on hide()
and rebind it on show() so each open starts with a fresh
QQmlDelegateModel instead of reusing a stale incubation queue.
2026-07-26 14:24:21 -04:00
dms-ci[bot] 8bb7396362 nix: sync flake.lock to dank-qml-common submodule 2026-07-24 23:36:44 +00:00
purian23 62648ed75a Bump dank-qml-common: revert CachingImage cache change 2026-07-24 19:36:08 -04:00
bbedward fa629fbf99 dock: use required properties in dock item delegate, fixes context injection on Qt 6.8 (#2926) 2026-07-24 15:45:57 -04:00
ArijanJ 7222e072af osd: click to hide some osds (#2927) 2026-07-24 15:19:47 -04:00
bbedward f57f254855 notification: clip region on animation exit
related #2917
port 1.5
2026-07-24 14:35:14 -04:00
bbedward fa094db127 lock: add completed guard in fade to dpms window
related #2885
2026-07-24 14:12:26 -04:00
bbedward 77a357109a bluetooth: add soft rfkill unblock, wait for adapter to become available
instead of forfeiting subscription

fixes #2922
fixes #1537
2026-07-24 11:28:24 -04:00
dms-ci[bot] 295657492d nix: update vendorHash for go.mod changes 2026-07-24 14:52:12 +00:00
bbedward 2bafc21229 gamma: replace login1 scheduler re-calculation with suspend-aware
boottimer
2026-07-24 10:49:37 -04:00
bbedward ea03fb2788 cups: fix initialization race
port 1.5
2026-07-24 10:42:27 -04:00
bbedward 0815e48465 dock: limit input mask to body, disable hover headroom unless enabled
fixes #2923
2026-07-24 10:31:51 -04:00
bbedward 42060366cb network: fix networkPreference reverting from eth to wifi
fixes #2919
port 1.5
2026-07-24 10:10:59 -04:00
bbedward 8af71036cc frame: merge bars with frame window and get things on Top layer by
default
2026-07-23 14:10:17 -04:00
bbedward 5854327c28 settings: fix close IPC
fixes #2912
port 1.5
2026-07-23 14:09:59 -04:00
euletheia 99f452c999 feat(Themes): add support for GTK3 dynamic theming (#2523)
* feat(Themes): add support for GTK3 dynamic theming

Instead of overriding named colors of all GTK3 themes via the config
directory file, only override named colors for each variant of the
adw-gtk3 theme (light and dark), and let GTK3 apps change themes
depending on the value of org.gnome.desktop.interface color-scheme.

* fix(Themes): apply requested changes

- remove gtk3 override if it's a symlink
- remove call to gtk.sh with old arg 'assets-only'
- append key to dconf path to respect dconf write signature

* refactor(Themes): separate 'include'/'apply' logic for GTK3 dynamic theming

* revert(Themes): reset timer between color-scheme refresh

- rename DMS_ENABLE_GTK4_REFRESH to DMS_ENABLE_GTK_REFRESH
- revert a3fba8

* feat(Themes): add 'remove' mode to gtk.sh

Allow to remove all inclusions and overrides for GTK.

* fix(Themes): backup adw-gtk3 original css in 'apply' (not 'include')

* style(Themes): rename argument 'include' to 'patch' for clarity
2026-07-23 13:31:39 -04:00
bbedward d818067bee calendar/dcal: fix flatpak socket detection
port 1.5
2026-07-23 10:14:26 -04:00
Huỳnh Thiện Lộc 5538913320 feat(settings): add option to suppress config reload toast (#2730) (#2915)
* feat(settings): add option to suppress config reload toast (#2730)

Add a toggle in Settings → Compositor Layout to disable the 'config reloaded' toast for Niri and MangoWC compositors. Session setting showConfigReloadToast defaults to true.

* chore: update settings search index for config reload toast

* refactor(settings): rename 'Compositor Notifications' to 'Compositor Toasts'

* fix(settings): reuse existing I18n.tr('Notifications') translation
2026-07-23 10:12:39 -04:00
Thomas Kroll 8188ced955 Add option to ignore app-requested notification timeout (#2909)
* Add option to ignore app-requested notification timeout

Notifications may include an expireTimeout hint that currently takes
precedence over the configured Low/Normal/Critical durations, so
popups can disappear earlier (or later) than expected with no
interaction involved. Add notificationIgnoreAppTimeout (default off,
matching the notification spec) to let users force their own
durations regardless of what the app requests.

* Update settings search index for new timeout override toggle
2026-07-23 08:32:14 -04:00
Huỳnh Thiện Lộc 75eac83b47 feat(settings): add Mouse & Touchpad settings tab for Niri (#2905)
* feat(settings): add Mouse & Trackpad settings tab

* style(settings): clarify touchpad natural scroll description

* chore(settings): update settings search index

* refactor(settings): unify Mouse & Trackpad terminology to Mouse & Touchpad

* feat(setup): deploy default dms/input.kdl on setup

* style(niri): restore include section comment in base config template

* refactor(niri): remove redundant input block from base config template

* refactor(niri): restore keyboard and non-mouse input config to base template

* feat(settings): add touchpad disable-on-external-mouse toggle

* feat(settings): add left-handed, scroll-factor and dwt options

* feat(settings): add middle-emulation, scroll-method, tap-and-drag and drag-lock options

* refactor(settings): re-order mouse and touchpad UI options logically
2026-07-22 13:28:40 -04:00
bbedward ce548cb29c update CONTRIBUTING for submodule recurse option recommendation 2026-07-22 12:32:59 -04:00
bbedward 61510c2186 theme: support for QT_PLATFORMTHEME=kde 2026-07-22 12:20:27 -04:00
Thomas KrollandClaude Opus 4.8 237fb57d5e fix(lock): hold sleep inhibitor until compositor confirms lock (#2907)
lock-before-suspend released the logind sleep delay inhibitor as soon as Qt
reported the lock rendered (afterAnimating/afterRendering). That signal fires
before the compositor has committed/presented the ext-session-lock surface, so
the machine could freeze ~1 frame later with the desktop still the last
presented frame. On resume the desktop was briefly visible before the lock
appeared (both suspend and hibernate).

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

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

Signed-off-by: Thomas Kroll <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
2026-07-22 09:24:25 -04:00
bbedward fe561f2b5d location: prevent calls when weather auto-locate is disabled
port 1.5
2026-07-21 23:35:33 -04:00
purian23 1c01254e9b fix(void): update working templates on dms-git
Port 1.5
2026-07-21 19:28:26 -04:00
bbedward 958672badd lock/pam: attempt to fix fprintd retry spam after resume 2026-07-21 17:53:58 -04:00
bbedward 93c3030a1e popout: fix lifecycle of blur with various popouts
fixes #2901
port 1.5
2026-07-21 17:46:43 -04:00
dms-ci[bot] 53c729f685 nix: update vendorHash for go.mod changes 2026-07-21 16:51:00 +00:00
bbedward e0480bf383 core: bump dankgo 2026-07-21 12:48:32 -04:00
bbedward e63d210358 wallpaper: use filter chips for random transitions 2026-07-21 11:27:49 -04:00
bbedward 30d9a34011 lock: avoid lock surface churn
related #2885
port 1.5
2026-07-21 10:50:39 -04:00
bbedward f0bdaa7f72 compositor/toplevels: filter on screen for generic compositors, without
workspace distinction

fixes #2890
port 1.5
2026-07-21 10:26:58 -04:00
bbedward fa6c36ad57 themes: more detailed WCAG breakdowns in browser 2026-07-20 20:21:00 -04:00
bbedward 5de0339506 i18n: add Ukrainian 2026-07-20 19:50:47 -04:00
bbedward 814871d982 window-rules/niri: make floating a tri-state 2026-07-20 18:00:56 -04:00
bbedward 68e60f036d polkit: fix DMS_DISABLE_POLKIT regression from qs 0.2 deprecation commit
fixes #2897
port 1.5
2026-07-20 16:07:41 -04:00
bbedward d185135dd7 theme: add WCAG data 2026-07-20 15:59:31 -04:00
bbedward 70ceee139b audio: revert wpctl workaround
related #2899
port 1.5
2026-07-20 15:19:07 -04:00
bbedward e58b41498c matugen/vesktop: more contrast fixes 2026-07-20 14:31:27 -04:00
bbedward 116cef0b3a lock: fix dpms fade surface getting stuck after unlock
port 1.5
2026-07-20 10:39:44 -04:00
Ron Harel dc75f1f01d feat(network): initial WiFi hotspot support (#2825)
* Add hotspot contract layer with capability-gated dispatch.

Establish the interface and manager plumbing for hotspot support so backends can opt in without expanding the core Backend contract.
Only backends that implement HotspotBackend get hotspot state propagated; others are forced to unsupported regardless of what they self-report.

* Add IPC handlers and API docs for hotspot actions.

Wire up configure/ start/ stop hotspot through the request router so the QML service layer can drive hotspot operations.
Bump API version to 27 and document the capability-gating contract clients should follow.

* Implement NetworkManager hotspot backend.

Implement HotspotBackend on NetworkManagerBackend with DMS-owned profile management, AP capability detection, and band validation.
Device resolution is deferred to StartHotspot when no device is specified, so profiles survive hardware changes.

* Isolate client Wi-Fi state from AP-mode connections.

Filter AP-mode profiles and access points out of all client Wi-Fi paths so the DMS hotspot (and user-created APs) never appear as saved networks, visible networks, or connected state.
This protects existing client behavior before hotspot controls are exposed in the UI.

* Prefer idle radios for automatic hotspot device selection.

* Add hotspot properties and methods to QML service layer.

Expose hotspot state and actions through DMSNetworkService, NetworkService, and LegacyNetworkService.
Capability is gated on API version and backend-reported support, not backend name checks, and stays stable when Wi-Fi radio is disabled.

* Add hotspot controls to Settings and Control Center.

Settings shows a hotspot setup card as a sibling in the Wi-Fi tab with SSID, password, device, band, save, and start/ stop controls.
Control Center shows a compact row that toggles a configured hotspot or routes to Settings for initial setup.
Both stay visible when Wi-Fi is disabled, explaining the requirement instead of hiding.

* Add translator context to hotspot strings.
2026-07-20 09:29:54 -04:00
Legend-017 367ad5f69a Fix(spotlight): improve file search query restoration and mode switching (#2887)
* fix(spotlight): fix empty results on restoring last file search query

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

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

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

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

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

* fix(spotlight): code review fix
2026-07-20 09:27:58 -04:00
bbedward 8577c6b815 matugen/vesktop/vencord/equibop: more contrast fixes 2026-07-19 19:22:57 -04:00
bbedward 3d9374d2b2 matugen/vesktop/vencord/equibop: minor contrast improvements 2026-07-19 19:08:41 -04:00
bbedward e300b14dde core: some fixes for embedded distribution, VERSION file, matugen
scripts, and embed scripts
2026-07-19 19:03:10 -04:00
bbedward dc4916b9e0 greeter: more repairs for standalone package, scrap LegacyNetworkService 2026-07-19 17:05:38 -04:00
bbedward fb65a23fac core: remove unused functions after migration 2026-07-19 16:52:54 -04:00
bbedward a5b5ed7e51 gitmodules: set branch=master in .gitmodules 2026-07-19 16:48:44 -04:00
bbedward dfbbfe1793 greeter: replace commands with dms-greeter 2026-07-19 16:43:43 -04:00
bbedward a69acf171a greeter: migrated dms-greeter to github/AvengeMedia/dank-greeter 2026-07-19 16:37:54 -04:00
bbedward 1402d2312a systray: fix menu positioning on vbars
fixes #2818
port 1.5
2026-07-19 16:37:23 -04:00
bbedward d003683689 matugen/vesktop: tweak some colors of unread indicators to follow
primary theme
2026-07-19 14:26:30 -04:00
bbedward 57fd39d84d matugen: add config dirs to detection of presence
related #2894
port 1.5
2026-07-19 12:43:04 -04:00
bbedward 6239e5a8e4 mango: suppress excessive reload messages
fixes #2848
port 1.5
2026-07-19 11:47:39 -04:00
bbedward 4c59d61f89 matugen/vesktop/vencord: replace midnight template with a new more
modest template
2026-07-19 11:46:45 -04:00
bbedward 2a8b88c1f0 matugen: remove padding 0 from gtk light
fixes #2884
port 1.5
2026-07-19 11:37:42 -04:00
bbedward dbaf2e922c theme: put full theme catalog in neovim dropdown
fixes #2892
port 1.5
2026-07-19 11:20:45 -04:00
bbedward eb330f62e1 dash: fix color tokens for placeholder in calendar, and weather failed
fixes #2891
port 1.5
2026-07-19 11:17:47 -04:00
bbedward 90291bd627 freebsd: add initial support for running on FreeBSD, excluding Bluez,
ppd, and some things. Add a simple wpa_supplicant backend
2026-07-19 10:31:48 -04:00
bbedward 069df80b22 core: exclude qmlls.ini from sync-shell 2026-07-18 15:42:21 -04:00
bbedward 2846363f55 powermenu: don't require key release for actions other than suspend and
hibernate

port 1.5
2026-07-18 15:30:31 -04:00
bbedward 0cdb065739 core: migrate to dankgo shared go modules, embed quickshell in DMS
binary to mount at runtime, -c or DMS_SHELL_DIR overrides required to
explicitly override embedded configuration
2026-07-18 14:51:52 -04:00
purian23 2114ece0df feat(bluetooth): built in audio codec support - another dep bites the dust
- Updated the codec logic to utilize WirePlumber, bye pactl
- Improved UI to display media and call codec sections
2026-07-18 14:16:49 -04:00
bbedward 4a90bd3fb1 matugen/neovim: respect terminalsAlwaysDark
port 1.5
2026-07-18 11:52:21 -04:00
bbedward 698b66cf91 lock: add resilience for when the lock surface is rejected
related #2778
related #2841

port 1.5
2026-07-18 11:50:31 -04:00
bbedward 8fc38de0a1 packaging: update fedora dms-git spec 2026-07-18 11:11:18 -04:00
bbedward 235f0668b8 Squashed commit of the following:
commit 990d86d481
Author: bbedward <[email protected]>
Date:   Sat Jul 18 10:43:22 2026 -0400

    flake: update-common

commit 526cb157fd
Author: bbedward <[email protected]>
Date:   Thu Jul 16 17:56:40 2026 -0400

    i18n: update sync scrirpt for dank-qml-common

commit 92ba96d9f9
Author: bbedward <[email protected]>
Date:   Thu Jul 16 09:24:37 2026 -0400

    qs: integrate with dank-qml-common
2026-07-18 10:44:31 -04:00
bbedward 5dfd875b9e widgets: fix hidden widgets disrupting index when arranging widgets
fixes #2844
port 1.5
2026-07-17 21:09:58 -04:00
purian23 00b22df802 fix(frame): restore frame edge inset states regression in framemode
Port 1.5
2026-07-17 20:24:48 -04:00
bbedward b22b1bef9b i18n: sync 2026-07-17 18:42:17 -04:00
bbedward 5cae48a85d i18n: replace terms for 1.6 2026-07-17 18:39:22 -04:00
bbedward 4200270b55 i18n: preserve terms for 1.5 point releases
port 1.5
2026-07-17 18:38:18 -04:00
purian23 c2c180d304 feat(nix): opt-in dankshell-u2f PAM service for lock screen security keys
- Updated instructional pushed:
https://danklinux.com/docs

Related: #2874
Port 1.5
2026-07-17 18:07:06 -04:00
purian23 3938e60ce4 refactor(auth): relay on distro system auth over DMS managed sessions
- Gate greeter external-auth status fprint availability only on confirmed setups
- Reload dankshell-U2F/U2F-Key watchers live

Related: #2874
Port 1.5
2026-07-17 17:27:45 -04:00
bbedward 2986e354e8 media: further optimizations for bar widget animations
related #2863
port 1.5
2026-07-17 15:48:45 -04:00
bbedward bf12665adb lock: add lockAndOutputsOff IPC function 2026-07-17 15:24:35 -04:00
bbedward f8fe16738e screenshot: initial support for scroll capture mode
fixes #2871
port 1.5
2026-07-17 13:31:56 -04:00
purian23 3c688cfbd3 feat(notifications): introduce notification foreground layers settings override
port 1.5
2026-07-17 13:17:12 -04:00
purian23 92ba2bb57d feat(settings): add explicit border toggle and updated layouts
- Defaults On with blur enabled to clean up the border edges
- Darken Modal moves to Launcher settings

Closes #2446
port 1.5
2026-07-17 12:55:02 -04:00
bbedward 504c132f78 wallpaper: optimize blurred wallpaper layer
port 1.5
2026-07-17 11:40:57 -04:00
bbedward bdaa6bcc9d fix(network): cancel in-flight connection when setting autoconnect
fixes #2882
port 1.5
2026-07-17 11:00:37 -04:00
bbedward a50bb2cab3 sysupdate: tweak styling of ignored banner
port 1.5
2026-07-17 10:32:24 -04:00
bbedward b17a90a412 sysupdate: disallow partial updates on arch, add ignored to popout
port 1.5
2026-07-17 10:18:51 -04:00
bbedward 0ffec5011b sysupdate: pass custom command to daemon instead of fire and forget from
QML

related #2873

port 1.5
2026-07-17 09:46:32 -04:00
Artem Timofeevandbbedward adbbd9b112 feat(power): lower refresh rate on battery (#2428)
* feat(power): lower refresh rate on battery (#1203)

Add a Power & Sleep setting that lowers eligible displays to 60 Hz on battery and restores their previous mode on AC power.

Skip displays without an available 60 Hz mode, displays with a single refresh-rate option, current modes at or below 60 Hz, and VRR-enabled outputs.
Apply niri changes at runtime through output commands and use wlr-output-management as fallback.

* fix(quickshell): debounce display refresh sync

* chore: regenerate settings_search_index.json for pre-commit CI fix

* fix: translator context is missing for lowerDisplayRefreshRateOnBattery

* fix: activeDisplayProfileModes is missing from SettingsSpec

* feat: added guard to syncRefreshRates function

* refactor(display): move refresh sync listeners to display service

* fix(display): publish active profile modes after config write

* feat(display): persist previous refresh modes

* fix(display): route refresh mode writes through compositor backends

* fix(display): detect preferred niri refresh modes

* some small cleanups

---------

Co-authored-by: bbedward <[email protected]>
2026-07-17 09:41:08 -04:00
bbedward 95e1d6b72e bar: fix bar order-creation regression caused by async loading
port 1.5
2026-07-17 09:17:22 -04:00
purian23 4cb09dcc1b fix(DankSlideout): update textureSize for improved rendering
- Fixes #2880
Port 1.5
2026-07-17 08:24:37 -04:00
Hec ed44d02202 systemlogo: Add Zirconium (#2881)
Signed-off-by: Hec <[email protected]>
2026-07-17 00:26:15 -04:00
bbedward 4352f49ae3 core: remove useless tests 2026-07-16 12:33:53 -04:00
Aaron Tulino 9ddea82063 bar(IdleInhibitor): Color icon when activated (#2395) 2026-07-16 12:18:19 -04:00
bbedward 3c0f2cbc48 i18n: term overhaul
- Delete ~160-ish useless terms
- Add context to more terms
- Add a mechanism to duplicate the same terms with different contexts
- sync
2026-07-16 12:15:20 -04:00
bbedward 6c45413d7a launcher: fix sticky header appearance without blur
port 1.5
2026-07-16 10:50:39 -04:00
bbedward 149e1e64b1 launcher: transparent sticky header when blur is enabled
port 1.5
2026-07-16 10:47:57 -04:00
bbedward 5589cfb783 dash/weather: fix X wheel scroll
port 1.5
2026-07-16 10:41:16 -04:00
Aldi Prasetyo ff71502083 fix(minimal-install): add hyprland packages to minimal install preference (#2872)
(I guess we'll find out if this breaks any minimal script to desktop runs)
2026-07-16 10:37:14 -04:00
bbedward 1ef5371c3b i18n: cut down new pam terms 2026-07-16 10:36:35 -04:00
bbedward 3951cd6b18 popout: guard against bad state and reset input method on close
port 1.5
2026-07-16 10:19:44 -04:00
bbedward 9686f29246 core: fix log file deadlock
port 1.5
2026-07-16 10:07:47 -04:00
bbedward 6d206c9521 launcher: fix usage history loading by adding guards
port 1.5
2026-07-16 09:30:18 -04:00
purian23 48e36ae146 feat(auth): expand shared PAM support for custom policies
- Add support for DMS and System managed factor policies
- Add validated dedicated security-key PAM / U2F Key sources
- Restore U2F OR/AND flows

Related: #2874
Port 1.5
2026-07-16 09:24:12 -04:00
purian23 7c24e85c19 fix(qml): resolve lint regressions from async shell loading
port 1.5
2026-07-16 06:10:43 -04:00
purian23 fe1a783ec2 fix(media-player): update Mpris stable metadata handling
- Restores Brave browsers
port 1.5
2026-07-15 20:57:05 -04:00
purian23 358496134c refactor: update workfows & issue notes
port 1.5
2026-07-15 16:18:36 -04:00
purian23 dc924618fb refactor: unify media control calls to MprisController sync 2026-07-15 14:35:19 -04:00
bbedward fb2dbced08 system tray: fix overflow of menus
port 1.5
2026-07-15 14:19:49 -04:00
bbedward cdaedad969 media: throttle frame rate of media-related animations
port 1.5

related #2869
2026-07-15 12:57:25 -04:00
bbedward bf408f8d00 i18n: cut down terms and sync
port 1.5
2026-07-15 11:05:07 -04:00
purian23 b169fe0d77 feat: new PAM auth management & settings in greeter/lockscreen
- Introduce external management of greetd PAM
- New functionality to validate and apply custom PAM service paths in lockscreen

Port 1.5
2026-07-15 10:38:06 -04:00
bbedward 9b67cedaa1 battery: dont scale values and skew the actual battery values
fixes #2867

port 1.5
2026-07-15 10:20:59 -04:00
bbedward 2edf70a144 tailscale: fix inconsistency in styling of control center widget
port 1.5
2026-07-15 10:15:32 -04:00
bbedward 72a71cacba hyprland: scale overview displays relative to their actual scale
port 1.5
2026-07-15 10:10:33 -04:00
bbedward 0440e40a82 compositor: improve compositor detection
port 1.5
2026-07-15 09:45:25 -04:00
bbedward ec27d4643c qs: improve initial load of wallpaper, dock, and desktop plugins 2026-07-15 09:38:19 -04:00
bbedward f18d36f6c2 screenshot: add --json flag to capture metadata
fixes #2852

port 1.5
2026-07-15 09:17:55 -04:00
purian23 3254cc6a1e update release & changelog docs formatting
port 1.5
2026-07-15 00:57:26 -04:00
bbedward 4af3225f21 network: avoid dropping to legacy service when loading 2026-07-14 22:56:28 -04:00
bbedward 3c5245914f qs: use asynchronous loaders to load shell core 2026-07-14 22:35:58 -04:00
bbedward 89814a2c65 i18n: sync 2026-07-14 22:28:58 -04:00
bbedward 5d2093e54a gamma: fix scheduler losing track during suspend
port 1.5
2026-07-14 22:26:44 -04:00
purian23 3cd52ca327 greeter: update success message 2026-07-14 20:01:06 -04:00
purian23 5b41d699fa fix(greeter): reimplement regression to hide debug logs during login
- Provide more status feedback
- Allow 2 fprint tries within 10 seconds for DMS-managed PAM
- Updated dms greeter docs

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

Fixes #2853
Port 1.5
2026-07-14 19:41:36 -04:00
bbedward 544599bf1c media: fix track art flash from chrome changing media art sizes on track
change

port 1.5
2026-07-14 18:44:42 -04:00
bbedward 296b3a3d7e mango: add separate dispatch socket
port 1.5
2026-07-14 17:09:34 -04:00
bbedward 729a990fa7 dbar: missing show on overview from search index
port 1.5
2026-07-14 16:35:26 -04:00
bbedward c3fa7b2e1d cava: more optimizations for visualizer
related #2863
2026-07-14 14:29:18 -04:00
bbedward 2c5a1a2804 cava: optimize CPU burn
related #631
2026-07-14 14:16:05 -04:00
bbedward 1973526c4e plugins: prevent churn of daemon plugins by not re-creating the entire map
fixes #2860

port 1.5
2026-07-14 11:46:40 -04:00
Callum Wong bba5502960 Smooth battery time remaining estimate (#2854)
* feat(battery): smooth time remaining estimate with moving average

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

port 1.5
2026-07-14 11:32:48 -04:00
Alexis Corporal 52740290b2 chore: fix indentation in niri media key bindings. (#2858) 2026-07-14 11:26:11 -04:00
bbedward 27703575bc plugins: use grid-style plugin browser
port 1.5
2026-07-14 11:25:10 -04:00
bbedward e6504add7b core/cli: expose QR code CLI
port 1.5
2026-07-14 10:22:17 -04:00
Huỳnh Thiện Lộc f1e9121295 fix(Modal): respect targetScreen property instead of always using focused screen (#2861)
The open() method in both DankModalStandalone and DankModalConnected unconditionally set contentWindow.screen to the focused screen, ignoring the targetScreen property.

Fix: use root.targetScreen ?? CompositorService.getFocusedScreen() so the modal appears on the configured screen when set, and falls back to focused screen otherwise.
2026-07-14 08:42:08 -04:00
bbedward 9ff751b82a dock: fix dock s howing with no apps
port 1.5
2026-07-13 22:47:22 -04:00
Kilian Mio 21eaaef056 Feature/split move size hyprland windowrules (#2824)
* windowrules: add split move/size fields for Lua table syntax

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

* fix: use resolved dms binary path in Proc.runCommand
2026-07-13 18:30:26 -04:00
bbedward bb0be2b215 clipboard: store text alongside image when both are offered
related #2849
port 1.5
2026-07-13 18:23:00 -04:00
bbedward 25847c3f03 config/hyprland: set empty kb layout string by default
fixes #2851
port 1.5
2026-07-13 17:43:09 -04:00
bbedward 7b5c25c50f launcher/spotlight: improve height-change animation
port 1.5
2026-07-13 16:38:40 -04:00
bbedward 7535b70fa6 dash: allow hiding all tabs
port 1.5
2026-07-13 16:11:15 -04:00
bbedward e3034e4e94 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2026-07-13 16:03:56 -04:00
bbedward 3da19e5c15 launcher: add option to choose spotlight style on niri overview 2026-07-13 16:01:08 -04:00
Rafiandbbedward ca89e12963 core: fix security and concurrency issues found in a backend audit (#2805)
* core: fix security and concurrency issues found in backend audit

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

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

* apply code review comments

---------

Co-authored-by: bbedward <[email protected]>
2026-07-13 15:44:42 -04:00
arfan 4fb6995796 workspace switcher: fix apps icon won't re focus (#2830)
* fix(workspace): update delegate data on window focus change

* fix(workspace): update delegate data handling specific for hyprland event
2026-07-13 14:59:53 -04:00
Huỳnh Thiện Lộc 3f5a54aa88 fix(dash): resolve pointer cursor hover issue in connected mode (#2845)
* fix(dash): cursor not changing to pointer in Connected Mode (#2831)

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

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

* fix(dash): resolve pointer cursor hover issue in connected mode
2026-07-13 14:58:02 -04:00
Arsenijs Kitajevs d379d251b9 Fixed bluetooth UI bug mentioned in #2627 (#2850) 2026-07-13 14:57:41 -04:00
purian23 7ab0e01573 refactor(dms-updater): remove hidden updates display in popout view 2026-07-13 12:37:04 -04:00
bbedward 2cb48aaf6b listview: workaround delegates overlapping and make spotlight launcher
have a stable anchor

port 1.5
2026-07-13 12:30:49 -04:00
purian23 e4657aa5f9 feat(dms updater): add support for ignoring specific packages during system updates
- Added UI component & popout settings to manage ignored packages
- Added CLI support available in danklinux docs

Fixes: #2827
Closes: #2344, #1741
Port 1.5
2026-07-13 12:02:13 -04:00
bbedward 197d17ac4e launcher: add IPC feature-parity to spotlight-bar and improve list view
transitions

port 1.5
2026-07-13 11:25:01 -04:00
bbedward 31ea83584b i18n: add arabic 2026-07-13 10:46:38 -04:00
bbedward ea66b136ba launcher: don't select pre-filled queries, exclude from memory
port 1.5
2026-07-13 10:31:17 -04:00
bbedward 06c0ea2afb i18n: sync 2026-07-13 09:47:21 -04:00
bbedward f4f47c0bc5 time: add follow-locale option and squash separate greeter time options
port 1.5
2026-07-13 08:53:37 -04:00
purian23 8a0ed8a50f fix(port-audit): update git fetch command to include prune opt
port 1.5
2026-07-13 01:14:33 -04:00
purian23 63eea01243 refactor(debian): prefer native Debian Quickshell stable package support in dankinstall
Port 1.5
2026-07-13 00:39:06 -04:00
purian23 f590a2965a fix(dash-tabs): update visibility logic for dash tab elements
Fixes #2822

Port 1.5
2026-07-12 23:57:29 -04:00
purian23 4ab03deded refactor(settings): allow search settings to remain on sidebar until dismissed
Closes #2780

Port 1.5
2026-07-12 23:30:40 -04:00
sweenuandClaude Fable 5 846d07d86a feat(ipc): add settings dump to print full live config as JSON (#2817)
Adds `dms ipc call settings dump`, returning the complete live
configuration via SettingsData.getCurrentSettingsJson() — the same
serialization used by the read-only banner's copy button.

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

Co-authored-by: Claude Fable 5 <[email protected]>

Port 1.5
2026-07-12 22:57:51 -04:00
AiraNadih 55d88d8dfb fix(process-list): defer IPC actions until modal is loaded (#2829)
Port 1.5
2026-07-12 22:57:24 -04:00
Mati7235andMatias a803f96f41 dms doctor: detect missing XDG_MENU_PREFIX when KDE apps are installed (#2840)
* dms doctor: warn when XDG_MENU_PREFIX is missing with KDE apps

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

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

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

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

Port 1.5

---------

Co-authored-by: Matias <[email protected]>
2026-07-12 22:54:28 -04:00
A042 da7cc5648b fix: use sleep_monitor/wakeup_monitor for DPMS to preserve monitor layout (#2842)
Since Mango v0.15.0, disable_monitor sets only_sleep=0 which removes the
output from the layout entirely. On NVIDIA (proprietary driver), the DRM
handshake fails on re-enable, leaving a black screen recoverable only via
TTY switch.

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

These commands are available since Mango v0.15.0 (commit b0326d7).
2026-07-12 18:28:56 -04:00
bbedward 75443758f9 fix(printers): fix add by address with manually entered host
port 1.5
2026-07-12 18:27:51 -04:00
purian23 eaecff93a4 add(workflows): update notify_issues input to release flows
Port 1.5
2026-07-12 16:27:32 -04:00
purian23 6e7c20c59c feat(release): add issue notification script for retesting on new releases
Port 1.5
2026-07-12 15:58:17 -04:00
purian23 406dcfe110 refactor(workflows): update release refs to stable in CI configurations
Port 1.5
2026-07-12 15:23:04 -04:00
Huỳnh Thiện Lộc e2b3a2e3ca feat(media): add toggle for album art accent colors (#2831) (#2832)
* fix(media): use system colours in player instead of album cover accent

Closes #2831

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

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

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

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

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

Port 1.5
2026-07-11 22:29:35 -04:00
purian23 71ab752e1b fix(void): updated dms-greeter stability support
- Updates will land in DMS v1.5.1 and users are advised to hold off on v1.5.0

Related #2788

Port 1.5
2026-07-11 01:07:00 -04:00
purian23 204ecd0461 feat(void): switch package repositories to R2 2026-07-10 18:14:13 -04:00
purian23 0fdfac366e ci(void): publish XBPS repository to R2 2026-07-10 17:57:28 -04:00
bbedward 52123458c7 widgets: fix dropdown/icon picker referencing a dead window
port 1.5
2026-07-10 17:21:10 -04:00
Artem Timofeev 26b2955cf3 fix(notifications): dismiss popups when senders close notifications (#2815)
When a sender calls CloseNotification (per freedesktop spec) for a
persistent notification (expire-timeout=0), the onDropped handler
removed the wrapper from internal arrays but did not dismiss the
visible popup. The popup remained on screen forever for non-critical
notifications like YubiKey touch prompts.

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

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

Closes #2814
2026-07-10 17:08:03 -04:00
bbedward d82d86df5c theme: prevent failed portal writes from reverting theme mode
related: #2786

port 1.5
2026-07-10 15:44:36 -04:00
Kangheng Liu c445597f83 fix(player): combine trackid with text identity (#2808)
fixes #2807
2026-07-10 13:37:16 -04:00
bbedward 6a58adfb29 osd/media playback: delay showing until album art is ready and avoid
re-showing when duplicate metadata comes from mpris
related #2787
port 1.5
2026-07-10 13:35:51 -04:00
bbedward 05feb211ba audio: only show mic volume OSD through DMS IPCs, not external source
changes
fixes #2790
port 1.5
2026-07-10 12:43:40 -04:00
purian23 a3b2167e58 workflow: enhance porting logic to support inline verbiage & audits 2026-07-10 11:57:36 -04:00
bbedward 0b69feaa1a settings: restore lost geometric centering option
port 1.5
2026-07-10 11:12:08 -04:00
purian23 494144a7c7 workflow: updated target extraction regex options 2026-07-10 11:09:05 -04:00
bbedward 2a2c1ca9e6 calendar: add action button to events for opening links
fixes #2799
port 1.5
2026-07-10 11:02:25 -04:00
bbedward 56b7ecb008 qs/common: fix path decoding
fixes #2802
port 1.5
2026-07-10 10:49:50 -04:00
bbedward 867102b82c network/iwd: improve bad credential handling
related #2804

port: 1.5
2026-07-10 10:36:22 -04:00
Scott McKendry 4bdb7d17b2 fix(settings): battery tab items not in search (#2794) 2026-07-10 10:17:51 -04:00
bbedward c44ffae751 fix(media): resolve monochrome album art accents
port: 1.5
2026-07-09 19:40:21 -04:00
14Do ce1595d62d fix(ListViewTransitions): null transitions when duration truncates to 0ms (#2791)
A zero-duration ViewTransition still engages ListView's transition
machinery but resolves within the same frame, so removed delegates are
never released and displaced items are never repositioned. On a filtered
ScriptModel that re-filters on every keystroke this leaves stale rows and
empty gaps behind, visible in any DankListView-backed list.

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

Gate the four transitions to null whenever that shortest sub-duration
would truncate to 0, so the view takes the correct instant path instead
of a broken 0ms transition. Base >= 3 is unchanged.
2026-07-09 18:47:26 -04:00
Evan Maddock 45f6232e32 build: Add support for DESTDIR (#2783)
This makes it easier for distros to use the Makefile when creating
packages. It enables us to specify a base destination directory to
install the project files to. E.g., on Solus, when creating eopkgs, files
must be installed to a special directory path, which becomes the package
files. I believe Fedora packages, and others, are the same.

Signed-off-by: Evan Maddock <[email protected]>
2026-07-09 18:39:06 -04:00
bbedward 9cf2ca7196 auth: add some more intelligent pam config resolution for lock screen
and greeter
related #2789

port: 1.5
2026-07-09 15:07:22 -04:00
purian23 c0eeed4e89 workflows: update deps
port/1.5
2026-07-09 12:53:57 -04:00
bbedward d0a4c1c56e default apps: add configuration for geo: URIs
port: 1.5
2026-07-09 12:23:44 -04:00
purian23 cb0dc9c68d workflow: update porting logic 2026-07-09 12:02:14 -04:00
bbedward 0439d017b9 fix(notifications): handle sound-name hint 2026-07-09 12:00:36 -04:00
purian23 8008238ca0 Add GitHub workflows for release management 2026-07-09 11:26:30 -04:00
bbedward a095d0ed90 changelog: disable 2026-07-08 16:42:30 -04:00
purian23 a48cce59d4 fix(workflow): void stable packages 2026-07-08 13:52:12 -04:00
bbedward 4c806f83f1 bump to 1.6-beta 2026-07-08 13:10:39 -04:00
bbedward bdfd565b72 fix words 2026-07-08 12:53:54 -04:00
bbedward f3b698590c bump VERSION to 1.5.0 2026-07-08 12:13:41 -04:00
bbedward 7826d42ce4 i18n: sync terms and remove term freeze 2026-07-08 12:13:14 -04:00
bbedward 6cc30c289b dankinstall/arch: add dms-greeter-bin package 2026-07-08 12:12:20 -04:00
bbedward 43cc8e69d1 window rules: fix gating of window-rules to supported compositors 2026-07-08 12:10:05 -04:00
bbedward a62ae336e0 media: improve album art handling 2026-07-08 12:01:33 -04:00
bbedward e94af2a7aa dankinstall: add danksearch and dankcalendar as optional deps 2026-07-08 10:01:56 -04:00
bbedward bb9e69e0c1 lock: add support for readline/emacs style modifiers
fixes #2777
2026-07-08 09:16:35 -04:00
purian23 1cc9218ff6 fix(frame): update hairline seam in notification joined surface 2026-07-07 21:54:37 -04:00
Lucas 56712f46fa Add optional=true to niri configs (#2776)
* nix: update flake.lock

* nix/niri: use optional=true in includes

* nix: fix tests

* niri: update embedded config to add optional=true
2026-07-07 21:40:43 -04:00
bbedward 93168ee073 qs: launcher optimizations and weather tab optimizations 2026-07-07 21:16:09 -04:00
purian23 1724aedd49 fix(AudioService): update the default audio sink device handling
- Fixes #2764
2026-07-07 20:17:40 -04:00
bbedward d799175c07 media: tweak seek bar accent color 2026-07-07 18:36:38 -04:00
bbedward ee6f7b4798 qs/media: replace FBOs with ClippingRectangle and rewrite seekbar as a
shader
2026-07-07 18:16:26 -04:00
bbedward 0511cd19df qs: numerous performance optimizations
- ClippingRectangle usages
- Asynchronous loader usages
- Replace cava visualizers with shaders
2026-07-07 16:11:11 -04:00
bbedward 71b1901ab0 notepad: add inline tab renaming and bump tab size 2026-07-07 11:05:14 -04:00
purian23 32e2d96e55 fix(popout): reintroduce texture sizing during animation to prevent content shift 2026-07-07 10:01:05 -04:00
bbedward 647766b9fa dankinstall: xray false for niri in default config 2026-07-07 09:38:45 -04:00
bbedward 0cd8974110 i18n: add Korean 2026-07-07 09:32:15 -04:00
bbedward 0c4c5fc146 changelog: enable for 1.5 2026-07-07 09:16:54 -04:00
bbedward b447e16374 launcher: dont cache clipboard results and fix image previews 2026-07-06 23:57:27 -04:00
purian23 0bb8353a33 refactor(blurSync): reimplement blur sync in popouts & modals 2026-07-06 23:52:57 -04:00
purian23 19a7dcf17d feat: (void-linux): add dankinstall support for auto installs 2026-07-06 23:01:16 -04:00
bbedward 8a1acb63c9 launcher: improve clipboard preview performance
related #2769
2026-07-06 18:15:49 -04:00
bbedward 6f298d3f52 workspace: fix hiding index numbers on sway
fixes #2768
2026-07-06 16:20:31 -04:00
bbedward 3e481a566b wallpaper: more resilience to updatesEnabled missing expose/update
events
2026-07-06 15:57:19 -04:00
bbedward 405ea708b3 wallpaper: optimize caching and performance of wallpaper tab 2026-07-06 13:52:31 -04:00
bbedward cc0bec2682 dash: fix clock index hardcoded at tab 0 2026-07-06 13:06:30 -04:00
bbedward 240adfa28f calendar: support tasks/VTODO from dank calendar 2026-07-06 12:40:41 -04:00
bbedward c175f4823a hyprland: workaround for focus re-grabbing a closing surface
related #2577
2026-07-06 11:45:38 -04:00
bbedward 35613ebba8 worksapces: attempt to fix hyprland workspace stability in ScriptModel
related #2754
2026-07-06 11:27:49 -04:00
purian23 583e46f2f0 fix(imageCache): improve cache management & file handling 2026-07-06 00:35:00 -04:00
purian23 590555dfc6 fix(dankdash): update tabIndex reordering for media & weather widgets
- Fixes #2762
2026-07-06 00:33:42 -04:00
bbedward c11169eb4c matugen: only write accent-color if changed 2026-07-05 23:13:49 -04:00
bbedward bddcdb3f99 core/completions: handle QS timeouts instead of hanging
related #1404
2026-07-05 23:12:30 -04:00
purian23 6ec67cefd0 add(settings): enable focused window icon display by default 2026-07-05 22:45:35 -04:00
David Mireles 8d9d7ff0ef fix(notifications): dismiss popup reliably on user-initiated close paths (#2761)
The notification popup's close button (X), action button clicks, and the
cardClick body's else branch all set notificationData.popup = false
directly. This relies on wrapperConn.onPopupChanged firing startExit(),
which can be interrupted by four races:

1. enterDelay (160 ms Timer) starts notificationData.timer after the
   user clicks, on an already-orphan wrapper.
2. The dismiss Timer keeps running post-click and flips popup again.
3. wrapperConn.target is set imperatively in onNotificationDataChanged
   (line 293) and may be stale after NotificationPopupManager._sync()
   reorders wrappers.
4. exiting or _isDestroying stuck true gates wrapperConn.enabled.

Introduce a single helper dismissPopupReliably() that stops the timer,
sets popup = false, and kicks off startExit() via Qt.callLater as a
belt-and-suspenders fallback. Apply to the three vulnerable handlers.

This matches the existing upstream pattern used in the hover, contextMenu,
and Component.onDestruction paths (which all stop the timer before
mutating popup state).

Closes #2760
2026-07-05 22:01:31 -04:00
Scott McKendry acc39ceb16 fix(settings): missing vpn confirm delete modal (#2759) 2026-07-05 22:00:57 -04:00
bbedward 43863a86fb workspaces: use stable references for placeholders in ScriptModel to fix
animation jitter
related #2754
2026-07-05 21:59:52 -04:00
bbedward 36ad34a555 keybinds: fix lua parsing on Hyprland for non-static configuration
fixes #2753
2026-07-05 17:16:55 -04:00
bbedward 78e823e23a workspaces: skip placeholders in findClosestWorkspace
related #2754
2026-07-05 11:26:38 -04:00
bbedward b6a27dc713 wallpaper: rewrite CachingImage and make it work with file browser
fixes #2756
2026-07-05 11:17:21 -04:00
bbedward 79dfd34ca2 control center: fix height expansion animation being out of sync 2026-07-05 10:42:15 -04:00
bbedward eedba0e8c9 vpn: fix tls openvpn connection types
related #1344
2026-07-05 01:35:01 -04:00
bbedward 2b12292895 Revert "dankbar: keep blur enabled at opacity-0"
This reverts commit 8a5e7c78fb.

fixes #2750
2026-07-05 00:20:31 -04:00
bbedward dd7f4f3ac5 audio/bluez: update device aliases at runtime when renamed
fixes #1628
2026-07-04 23:00:22 -04:00
bbedward 3df801e3b2 keyboard layout: add support for sway
fixes #1681
2026-07-04 22:55:50 -04:00
purian23 74d220767d fix(greeter): update user cursor theme support
- Fixes #2054
2026-07-04 22:52:16 -04:00
bbedward a3fba8fcb6 matugen: make GTK4 live-refresh opt-in, off by default. Bump the settle
timeout
fixes #2552
2026-07-04 22:38:49 -04:00
purian23 33384e4d5d fix(niri): update custom keyboard shortcut parsing properties
Fixes #2187
2026-07-04 22:25:53 -04:00
purian23 78f2ea4642 fix(Pywalfox): update distro detection handling
- Fixes #2220
2026-07-04 22:17:55 -04:00
purian23 7dffe85e94 fix(iwd): update rfkill to allow backend management
- Fixes #2221
2026-07-04 21:57:27 -04:00
purian23 c5c3105469 fix(Bluetooth): update codec selection and error handling
Fixes #2240
2026-07-04 21:48:34 -04:00
bbedward 8f100960cf theme: fix gtk3 checkboxes in light mode
fixes #1555
2026-07-04 21:28:08 -04:00
bbedward 8a5e7c78fb dankbar: keep blur enabled at opacity-0 2026-07-04 20:57:07 -04:00
bbedward 8a85ab06a1 portal: prevent feedback loop with the gtk4 hack to make it react to
color scheme changes
2026-07-04 20:43:32 -04:00
bbedward 30ceda97a0 brightness: avoid targeting inactive panels
fixes #1127
2026-07-04 20:43:32 -04:00
purian23 fefc0afa6f fix(SystemTrayBar): improve menu position boundaries
- Fixes #2252
2026-07-04 20:24:59 -04:00
bbedward b01042d530 brightness/ddc: avoid re-probing already identified devices
related #2049
2026-07-04 20:12:27 -04:00
bbedward 7355cc54b2 display config: persist applied layout to auto profile and skip no-op re-apply on startup
related #2526
2026-07-04 20:03:11 -04:00
purian23 187b7ba8ff feat(Settings): add option to show icon for focused windows
- Closes #2122
2026-07-04 18:42:40 -04:00
purian23 c0e7c1b2b4 feat(Battery): add horizontal battery pill style option in widget settings 2026-07-04 18:41:52 -04:00
bbedward ea0c235d6e notepad: make dentry NoDisplay 2026-07-04 17:00:43 -04:00
bbedward 57a04cf409 i18n: add mechanism for blocking new terms 2026-07-04 16:51:28 -04:00
bbedward fd99558ce5 clipboard: implement virtual-keyboard-unstable-v1 to replace wtype for
pasting entries
2026-07-04 16:24:14 -04:00
purian23 81a4d3b4e0 feat(Notepad): add notepad desktop entry & IPC commands file handling 2026-07-04 16:10:56 -04:00
Scott McKendry bfda5f2d66 fix(lockscreen): clock off-center with single hour digit (#2745) 2026-07-04 15:18:34 -04:00
bbedward 65c4167ab2 display config: fix mixed ID/model identifiers
fixes #2736
2026-07-04 13:24:53 -04:00
bbedward e7bd8e4002 greeter: fix weather location sync
fixes #2303
2026-07-04 13:06:49 -04:00
bbedward 25a1b125b7 keyboard layout: add consistent helper to map layout codes to display
fixes #1849
2026-07-04 11:57:17 -04:00
bbedward d766c0773c theme: fix setting color mode when no wallpaper is set in auto mode
fixes #2239
2026-07-04 11:40:27 -04:00
Huỳnh Thiện Lộc 5d12962fd6 fix(clipboard): fix action buttons hover cursor shape (#2744) 2026-07-04 11:31:39 -04:00
bbedward 9911698237 lock: synchronize password inputs between displays and fix video
screensave/password focus
fixes #1933
2026-07-04 11:29:41 -04:00
Huỳnh Thiện Lộcandbbedward 7e095415a7 refactor(battery): align status layout and add individual notification types (#2743)
* refactor(battery): restore status list layout and add individual notification types

* settings/battery: fix RTL display of top tablular-like view

---------

Co-authored-by: bbedward <[email protected]>
2026-07-04 11:20:44 -04:00
bbedward 68d5d7c5ed wallpaper: persist cycling state to disk
fixes #1010
2026-07-04 11:17:13 -04:00
Scott McKendry 7b1c542585 feat: lockscreen screen filter (#2740)
this makes use of the existing getFilteredScreens function to allow
users to specify which displays the lockscreen should appear on. prior
to this change, it was "all" or just one.
2026-07-04 11:04:59 -04:00
Kangheng Liu 52ed719489 fix: add missing vertical anchor for cava/music icon (#2742) 2026-07-04 10:58:55 -04:00
bbedward 6e508d416d core/wayland: fix fatal error handling when the socket becomes
unavailable
2026-07-04 10:52:52 -04:00
bbedward 518cc93276 weather: fix location search with han characters
fixes #2274
2026-07-04 10:48:08 -04:00
bbedward 8cb4e21716 animations: fix regression eliding button group text caused by recently
RTL fixes
fixes #2741
2026-07-04 10:44:02 -04:00
bbedward 19b44b1782 settings: make display config identify only show when interacting 2026-07-04 10:30:24 -04:00
purian23 0509694d78 fix(Notifications): update flexible image caching in DankCircularImage
- Fixes #2480
2026-07-04 01:26:56 -04:00
purian23 da44e07cf7 fix(International keyboards): update key mapping w/symbolic & shifted fallback support
- Fixes #2497
2026-07-04 01:13:49 -04:00
purian23 b34941e3b8 fix(keybinds): respect niri mod-key when capturing binds
Hardens #2224
2026-07-04 00:50:58 -04:00
purian23 c554d973ef fix(Hover): refactor & update hover tracking w/context menus
Fixes #2737
2026-07-04 00:08:58 -04:00
bbedward 2861cc89c6 idle: fix screensaver inhibit interfering with manually triggered
inhibitor
2026-07-03 23:27:19 -04:00
bbedward 1c5965f1e4 matugen: fix vscode text legibility on auto-complete menu
fixes #2198
2026-07-03 23:18:32 -04:00
bbedward 66d552b3d5 niri: re-work layer-rule to exclude bar for xray instead of using 2
rules
2026-07-03 23:13:25 -04:00
bbedward ebad8e2dda mango: add specific sorting for widgets using data from mango IPC
fixes #649
2026-07-03 23:11:34 -04:00
purian23 8dadf73d52 refactor: (modals/popouts): sync functions & improve blur calculations across modals 2026-07-03 22:20:10 -04:00
bbedward 9f77b3127c control center: fix brightness icon not showing up if no pinned devices
fixes #2488
2026-07-03 21:59:01 -04:00
bbedward 46f8457777 niri: make xdg pictures dir for screenshot, in case it doesnt exist
fixes #1577
2026-07-03 21:56:10 -04:00
Huỳnh Thiện Lộc 8465ed4311 feat(plugins): add update all CLI flag and settings updates dialog (#2682)
* feat(plugins): add update all CLI flag and settings update dialog

* feat(plugins): add comparison diff URL support and update button styling

* feat(plugins): skip system plugins in bulk CLI update

* fix(plugins): remove check shorthand to resolve conflict with config flag

* feat(plugins): inline update dialog, show version tags, restructure buttons
2026-07-03 21:51:39 -04:00
bbedward 6e3e178721 running apps: fix alignment of fallback icons in non-compact mode
fixes #1963
fixes #2024
2026-07-03 17:04:45 -04:00
bbedward 27230227ef tmux: fix new session hint
fixes #2556
2026-07-03 16:56:32 -04:00
purian23 7178563c2c fix(sway): correctly handle Sway named workspaces
- Fixes #2531
2026-07-03 16:33:44 -04:00
bbedward dfe309a543 dock: fix launcher button color override
fixes #2530
2026-07-03 16:29:06 -04:00
bbedward 04962ffed1 desktop widgets: fix rounding of system monitor graphs
fixes #2356
2026-07-03 15:50:06 -04:00
bbedward 5008406be8 display config: fix monitor names overflowing, add identity overlay to
ease configuration
fixes #1398
2026-07-03 15:45:58 -04:00
bbedward 930a36bcfc animations: add DankColorAnimation helper to properly animate between
colors with different alpha values
fixes #2720
2026-07-03 15:36:45 -04:00
bbedward 3253adb6dd workspace: fix display of named workspace + icon set on vertical bars
fixes #2067
2026-07-03 15:21:13 -04:00
purian23 b8fd52e00f fix(Notifications): update hasNotificationImage logic to support file paths
- Fixes #2533
2026-07-03 13:38:21 -04:00
bbedward 8e7126472e workspaces: fix hardcoded font weight to use configured value
fixes #2076
2026-07-03 12:10:45 -04:00
bbedward 61108b7668 i18n: numerous RTL layout fixes across DankDash and Settings 2026-07-03 11:57:09 -04:00
bbedward 09ca37c9fc workspace: fix occupied color in niri
fixes #2538
2026-07-03 11:21:50 -04:00
purian23 a2513e939a fix(NiriService): enhance column movement functions to focus on specified monitor
- Fixes #2498
2026-07-03 00:14:14 -04:00
purian23 a73380b310 fix(PowerMenu): defer keyboard hold actions to key release
- Fixes #2489
2026-07-03 00:02:35 -04:00
purian23 69d7c1fb13 fix(WorkspaceSwitcher): color handling w/animation transition
- Fixes #2720
2026-07-02 23:50:47 -04:00
bbedward d1eb655e46 theme: fix accent colors in light mode
fixes #2711
2026-07-02 23:38:14 -04:00
purian23 a3ecef7627 fix(PluginBrowser): simplify model binding for filtered plugins
- Fixes #2721
2026-07-02 23:01:23 -04:00
bbedward 186621ab81 lock: set LockedHint when locking with loginctl lock integration
- was previously read-only
fixes #2732
2026-07-02 22:57:09 -04:00
bbedward 31efe9b385 hyprland: fix named workspaces hiding from widget
fixes #2047
2026-07-02 22:49:09 -04:00
purian23 6cdb891551 feat(matugen): add color preview pallette & outline border option theme settings
- Closes #1738
2026-07-02 22:45:55 -04:00
bbedward 3bb3da228c dock: fix context menu overflow
fixes #2287
2026-07-02 22:12:02 -04:00
purian23 0c99e4b8d1 refactor(compositor): reduce unnecessary UI updates
- Defer updates when window state hasn't actually changed
- Cache frame-blocked state per screen instead of recalculating constantly
- keep popouts cache so reopening them is instant & clean up otherwise
2026-07-02 21:42:20 -04:00
bbedward 8b79d1dad3 notifications: fix blur on popout
fixes #2288
2026-07-02 21:32:10 -04:00
bbedward 85fa3326c1 blur: fix blur for goth corners in dankbar
fixes #2666
2026-07-02 20:27:42 -04:00
purian23 2fade1a617 fix(notepad): use surfaceVariantText color for line numbers 2026-07-02 17:14:32 -04:00
purian23 597ba597e5 feat(settings): implement greeter & lockscreen customization options
- New option to customize your font & wallpaper on the LockScreen
- New floating sync button if missed to explicitly sync on greeter changes
2026-07-02 17:13:18 -04:00
purian23 957020c9ea fix(nix): support declarative greeter auto-login (#2734) 2026-07-02 17:33:10 -03:00
bbedward ae520620ac idle: fix idle monitors firing when bar isnt visible + keep awake is
active
fixes #2698
2026-07-02 15:51:22 -04:00
bbedward 4c43bed2f8 lock: fix custom lock commands with lock before suspend and hibernate
fixes #2326
2026-07-02 15:41:05 -04:00
bbedward 2663bcd96c lock: add blinking cursor for text input
fixes #2316
2026-07-02 15:20:36 -04:00
purian23 e423e17807 fix(greeter): update NixOS declarative configuration
- Report NixOS greeter state from `var/lib/dms-greeter`
- Allows `dms greeter status` use
- Prevent imperative greeter changes on module-managed NixOS systems
- Preserve immutable-distro sync policy
- Closes #2728
2026-07-02 15:18:53 -04:00
bbedward 70fa0ec903 dankbar: fix widget registration during visibility changes
fixes #1956
2026-07-02 15:07:43 -04:00
bbedward db3615805c process list: use onClosed signal to call hide()
fixes #2560
2026-07-02 14:55:09 -04:00
bbedward 8162e60475 clock/calendar: toggle clock enabled/disabled on resume from suspend
fixes #1298
fixes #2705
2026-07-02 14:53:23 -04:00
purian23 5594ec27ff fix(xray): fix dbar autohide w/xray blur
Fixes #2733
2026-07-02 14:08:52 -04:00
bbedward ca050ace9c battery: clamp percentage to 100
fixes #1834
2026-07-02 13:35:26 -04:00
bbedward 093acdbff7 theme: sweep everything that isnt using a spacing token from singleton,
add Theme.XXS
2026-07-02 12:00:34 -04:00
Legend-017 a98417d66b fix(spotlight): select last query text on open; last-query persistence in connected frame mode (#2726)
* fix(spotlight): Fixes 'remember last query' being ignored in connected frame mode."

* fix(spotlight): select remembered query text instead of placing cursor at end
2026-07-02 11:39:35 -04:00
Youseffo13 83b9cdcb27 fix(ui): resolve ui inconsistencies and missing i18n strings (#2719)
* Update WorkspacesTab.qml

* Update SettingsButtonGroupRow.qml

* Update WallpaperTab.qml

* Update WidgetsTabSection.qml

* add missing tooltips

* Update WallpaperTab.qml

* Update WorkspacesTab.qml
2026-07-02 11:27:15 -04:00
purian23 4ae8ef927c refactor(launcher): invert framemode emerge from top position 2026-07-02 11:18:33 -04:00
bbedward 2128e6f14e compositor/xray: re-add manual bar xray toggle 2026-07-02 10:59:34 -04:00
bbedward 9ff085c46a niri/xray: make bar xray implicit, add conflict detection for
configuration
2026-07-02 10:05:23 -04:00
purian23 57d08f6b3b refactor(Xray): Update Xray & standalone to frame transitions
- Fix dbar autohide with Xray options that could have blocked bar area content
- Fixes #2729
2026-07-01 21:39:47 -04:00
purian23 7964b66827 feat(Xray): Automatic Xray blur effects for Niri & Hyprland layouts
- Utilizes include layout within niri configs & hyprland's layout layer rules for lua configs
2026-07-01 16:15:02 -04:00
bbedward 38a03a4485 hyprland: general focus fixes related to #2561 2026-06-30 23:27:25 -04:00
purian23 a1727d18fd refactor(transparency): stop overwriting dms settings buttons/sliders by default
- Fixes #2692
2026-06-30 17:01:49 -04:00
purian23 aabcef0c9c refactor(theme): decouple foreground layers dep on blur on/off 2026-06-30 16:57:49 -04:00
purian23 19406e99b9 refactor(include): Add missing layout include & normalize config banners
- The Display config process ID now includes the output name in NiriService
2026-06-30 14:48:09 -04:00
bbedward 5432d264f1 ci: add guards to workflows 2026-06-30 13:15:01 -04:00
bbedward d704a0ba3d wallpaper: migrate auto-cycling scheduling to backend 2026-06-30 13:11:29 -04:00
bbedward 161118122e launcher: fix hyprland focus on context menu
fixes #2714
2026-06-30 10:04:44 -04:00
bbedward 9d550d7c11 desktop widgets: ensure creation order based on configured order,
improve drag and drop behavior in settings
fixes #2715
2026-06-30 09:51:00 -04:00
bbedward e40faaa442 calendar: use surfaceVariantText for adjacent month day numbers 2026-06-29 16:14:41 -04:00
bbedward 894efa58a3 theme: large consolidation of colors into Theme.qml, add
surfaceContainerLow/Lowest
2026-06-29 15:50:42 -04:00
bbedward 50570023ff ui: cleanup some anti patterns 2026-06-29 12:39:56 -04:00
bbedward f293def3a8 welcome page: fix to not shell our to external dms IPC 2026-06-29 12:12:17 -04:00
Huỳnh Thiện Lộc de3e8cffa0 feat(mpris): allow excluding specific media players by identity (#2712)
* feat(mpris): allow excluding specific media players by identity

* chore(translation): update settings search index and clean trailing whitespace
2026-06-29 12:01:19 -04:00
bbedward a874f76d72 popout: fix texture sizing during animation 2026-06-29 08:21:21 -04:00
bbedward 424d4ed701 run pre-commit fixes 2026-06-29 08:01:38 -04:00
purian23 8f2a50c00e workflow(void): update the cron schedules 2026-06-28 21:23:07 -04:00
purian23 46cddb965f distro(Void): Require dbus by default 2026-06-28 21:17:11 -04:00
purian23 1ca64abad9 void: remove dbl dms-git transform 2026-06-28 20:44:17 -04:00
purian23 32d68a034f void: automate XBPS key registration 2026-06-28 20:36:15 -04:00
purian23 1a176920d4 distros(Void): refactor to read danklinux repos for local builds 2026-06-28 20:10:49 -04:00
purian23 afa6fd9e2e distros(Void): Prep DMS to host Void packages 2026-06-28 19:54:24 -04:00
purian23 6bee1b2c86 feat(HoverMode): implement hover popout & launcher functionality in all modes
- New Hover toggle found in DankBar Settings
- New Hover to Reveal Launcher in FrameTab Settings
2026-06-27 22:47:38 -04:00
Maddison Cohodas 48f6a0c632 feat(bar): separate workspace appearance for unfocused displays (#2687)
* feat(bar): separate workspace appearance for unfocused displays

Add a toggle in the Workspace Appearance card to style workspace
indicators independently on displays that aren't currently focused.

The card is split into Focused Display and Unfocused Display(s) tabs.
When the new master toggle is off (default), unfocused displays inherit
the focused settings, preserving existing behavior. When on, unfocused
displays use their own colors (focused/occupied/unfocused/urgent) and
focused border (enable, color, thickness).

WorkspaceSwitcher resolves the focused monitor per compositor and routes
color/border resolution through isFocusedMonitor/useUnfocusedAppearance.

* refactor(bar): address review feedback for unfocused workspace appearance

- Consolidate focused-monitor lookup into BarWidgetService.getFocusedScreenName(),
  adding Mango support, and drop the duplicate compositor switches in
  WorkspaceSwitcher (effectiveScreenName + isFocusedMonitor now use the helper).
- Extract the shared color and border options into reusable
  WorkspaceAppearanceColorOptions and WorkspaceAppearanceBorderFields components
  so the focused and unfocused tabs no longer duplicate the layout.
- Gate the unfocused-display options on BarWidgetService.focusedScreenDetectionSupported
  and show an explanatory note on compositors without focus detection (e.g. labwc),
  instead of silently no-opping.
2026-06-26 23:34:24 -04:00
bbedward 03d86f78f4 core: improve how DMS handles multiple-sessions under the same user 2026-06-26 10:09:00 -04:00
purian23 0bc89429bc fix(qml): use explicit mouse params in modal MouseArea handlers 2026-06-25 21:32:47 -04:00
purian23 dd3eb71485 fix(wallpaper): Hyprland import for scrolling workspace sync 2026-06-25 21:32:27 -04:00
bbedward 016527fda9 plugins: fix plugin browser parentWindow and add similar support 2026-06-25 14:27:28 -04:00
bbedward d1f833fbbb greeter: fix single user artifact 2026-06-25 13:45:31 -04:00
purian23 fd65a8cea9 fix(BatteryService): charging state binding loop 2026-06-25 13:05:20 -04:00
purian23 bcd6006e25 refactor(WidgetsTab): port updated drag-and-drop functionality & cross section target ordering 2026-06-25 12:57:34 -04:00
purian23 cde0d83620 fix(WindowBlur): ensure blurRegion is cleared if BackgroundEffect exists 2026-06-25 12:56:21 -04:00
Bruno Rocha 92994061da skill(dms-plugin-dev): update for composite plugins, startupCheck, IPC discovery (#2699)
- Add composite plugin type with multi-surface components field
- Add startupCheck manifest field and dependency gating docs
- Add IPC runtime plugin discovery commands (plugin-scan target)
- Add getPluginPath() to data persistence reference
- Document bar reveal visibility optimization for widget plugins
- Sync plugin-schema.json with source, add dependencies field
- Bump skill version to 1.1
2026-06-25 11:53:07 -04:00
purian23 1e7ee25e13 dms(settings): few misc tweaks 2026-06-25 00:22:19 -04:00
purian23 838d21aae8 feat(DankDash): configurable drag-n-drop tab arrangement & hide options in dms settings 2026-06-24 23:33:35 -04:00
bbedward 2fc3b8ee4a compositor/workspace: fix DMS binding to ext-workspace-v1 even when we
don't use it
2026-06-24 14:22:05 -04:00
bbedward 52834c6a35 packaging: add com.danklinux.dms.desktop entry, remove custom icon
resolution
2026-06-24 10:01:45 -04:00
hecate cantusandbbedward 1a39b7f66c shader-based scrolling wallpaper mode (#1802)
* feat: parallax-scroll wallpaper

Add a `Scrolling` wallpaper fill mode that translates the wallpaper crop
with the active workspace — like Android home-screen parallax, but along
niri's vertical workspace axis.

The image is scaled to cover the screen on its non-scroll axis, and the
active workspace index drives a fractional offset into the cropped
overflow along the scroll axis. Scroll position is spring-animated
CPU-side and handed to a minimal single-texture shader as a UV offset.
Per-monitor scroll position is published into SessionData so the lock
screen renders the same crop as the active workspace, keeping visual
continuity across lock/unlock.

Two implementation details worth calling out for review:

- QSG_USE_SIMPLE_ANIMATION_DRIVER=1 is exported to the spawned quickshell
  process. The default animation driver advances in fixed ~16ms steps,
  capping the scroll at 60Hz and desyncing it from compositor motion on
  high-refresh displays; the simple driver advances by real elapsed time,
  restoring native-refresh pacing. Removing it visibly regresses to 60Hz.

- The wallpaper survives wl_output rebind cycles (e.g. OLED image-cleaning
  on DPMS soft-off), which otherwise leave a stuck or void background.
  Recovery re-anchors the scroll target on output-lifecycle signals,
  rebuilds the ShaderEffect against the current render context, and
  re-attaches the wallpaper-layer surface on unlock for parallax-active
  monitors — guarded against lock state so the shader gets reliable frame
  hints.

* simplify bindings and gate lock screen shader in a loader

---------

Co-authored-by: bbedward <[email protected]>
2026-06-24 00:14:42 -04:00
purian23 aea5189abb fix: gate mango matugen and Cosmic config sync on active session 2026-06-23 23:22:42 -04:00
Kangheng Liu 774796ada2 launcher: add ctrl+H/L navigation in grid mode (#2688) 2026-06-23 23:06:38 -04:00
bbedward 14f21b6c6d qs: replace all which commands with command -v 2026-06-23 22:49:34 -04:00
bbedward 21ddc3cc3f popout: fix focus regression on close 2026-06-23 22:33:27 -04:00
bbedward 8b94b149b2 plugins: add startupCheck function 2026-06-23 22:12:13 -04:00
purian23 99cc3b8449 feat(wallpaper): preload thumbnails & switch/sort options
- add sorting and clickable page jump
- wrap-around page navigation
2026-06-23 21:39:06 -04:00
purian23 b2e728315b feat(pluginBrowser): Add inline image previews while browsing 2026-06-23 20:00:55 -04:00
bbedward bed11feaa4 plugins: enhance browser and CLI with new votes and labels 2026-06-23 14:48:43 -04:00
bbedward 28f40afccf fix(network): fix excessive network prompting on failures, add VPN
connection statuses
2026-06-23 13:58:58 -04:00
bbedward 0e7901ebbe keybinds: fix mouse wheel keysims on Hyprland and Mango
fixes #2683
2026-06-23 11:21:38 -04:00
bbedward 73f3a72d00 sysupdate: fix check scheduling
- deadline-based, fixes the issue before that would reset the interval
  without accounting for prior time waited
2026-06-23 09:55:39 -04:00
bbedward 63feabf517 i18n: add esperonoto and sync 2026-06-23 09:45:19 -04:00
Youseffo13andbbedward 84f87dd7f3 fix: UI polishes, uptime formatting, and missing i18n strings (#2686)
* Update CompositorLayoutTab.qml

* add missing i18n strings

* fix context menu overflow and strip uptime prefix

* fix settings index

---------

Co-authored-by: bbedward <[email protected]>
2026-06-23 09:33:29 -04:00
Ralph Zhou 39c247d9cb fix(lock): add complete bundled PAM services (#2685) 2026-06-23 09:26:51 -04:00
NihilDigit a98c4f6c54 fix(network): show WiFi connection progress (#2684) 2026-06-23 09:19:37 -04:00
bbedward bed6622530 settings: fix index/pre-commit 2026-06-22 23:29:47 -04:00
bbedward c5e08a6f2c core: migrate to mockery v3 2026-06-22 23:27:29 -04:00
purian23 ad32af6eeb settings(dbar): add bar inset padding UI opts in framemode/stanalone
Fixes #2597
2026-06-22 21:10:36 -04:00
bbedward 02ced4b87b notifications: expireTimeout already comes in miliseconds 2026-06-22 15:03:49 -04:00
bbedward 5b28a63f75 portal: add bidirectional syncing with freedesktop color-scheme, read in
addition to the existing write
2026-06-22 14:52:10 -04:00
bbedward b5e2e68a22 greeter: redesign the switch-user/auto-login flow
- Fix visual glitches
- Make auto-login and switch user texts not always visible
- Make from-zero memory state not confusing
2026-06-22 13:50:50 -04:00
bbedward 556c0819c4 core/network: avoid endless probing when no network backend is present 2026-06-22 13:09:04 -04:00
purian23 b0edccb80e fix(greeter): restore legacy remembered sessions by desktop id
- Derive a desktop id from legacy remembered session paths
- Match remembered sessions by desktop id before falling back to path
2026-06-22 12:56:45 -04:00
bbedward 4e1c921ffb notifications: respect expireTimeout set by notifiers 2026-06-22 12:33:51 -04:00
purian23 fdbbcafb6c fix(greeter): avoid pinning auto-login session commands
- Store a desktop session identity instead of relying on raw Exec commands
- Resolve the current session desktop file when auto-login launches
- Preserve legacy memory compatibility while ignoring stale lastSessionExec
- Add regression coverage for stale /nix/store session paths
- Autologin users should rerun the process
2026-06-22 11:34:27 -04:00
Huỳnh Thiện Lộc a043d477fb feat(battery): add options to toggle percentage and remaining time (#2672)
* feat(battery): add options to toggle percentage and remaining time

* fix(battery): add SettingsSpec entries and combine verticalDisplayText

* fix(battery): format vertical battery remaining time as HH\nMM and center alignment

* fix(battery): remove percent sign from vertical layout when time is enabled
2026-06-21 22:12:48 -04:00
Huỳnh Thiện Lộc 4cbe766cbd feat(wallpaper): support configurable background color (#2671)
* feat(wallpaper): support configurable background color

* chore(translations): update settings search index

* revert(wallpaper): keep Pad naming instead of Center
2026-06-21 03:57:08 -04:00
Huỳnh Thiện Lộc 8610b915ec feat: add option to hide package source badges in launcher (#2676) 2026-06-21 03:51:44 -04:00
jbwfuandpurian23 59fd6db83e Enhance/clipboard history interactions (#2668)
* feat(clipboard): add entry context menu

* feat(clipboard): add click to paste option

* feat(clipboard): add optional copy and paste action buttons

* fix(clipboard): default selection and esc behavior

* fix(clipboard): prevent clear and filter overlap

* Update to remove dead kb nav & add escape action on context menu

---------

Co-authored-by: purian23 <[email protected]>
2026-06-21 03:50:51 -04:00
purian23 465cf7355b distros(void linux): initial packaging support 2026-06-21 01:21:39 -04:00
bbedward de91b78943 theme/icons: add separate light/dark path, hot-reload, detect external
DMS maangedand reset
fixes #608
closes #2674
2026-06-20 15:23:19 -04:00
bbedward 4203148cab ci: add settings index check to pre-commit 2026-06-18 18:42:33 -04:00
bbedward 5adc0c464a toast: fix blur 2026-06-18 15:49:53 -04:00
Rocho 097290f7da fix(lock): show the faillock lockout reason instead of "incorrect password" (#2669)
When pam_faillock locks the account (DMS authenticates through the system PAM
stack: /etc/pam.d/login -> system-auth), the lock screen kept showing the
generic "Incorrect password - try again" even though the real cause is a
lockout, so a correct password looks rejected and only a reboot (which clears
the tmpfs /run/faillock tally) appears to help. See #2647.

The previous onMessageChanged only matched the *English* faillock strings
("The account is locked ...") and then wiped that text again on the trailing
pam_unix "Password:" prompt. On a non-English system (e.g. German) the strings
never matched, so the lockout was never surfaced at all.

Detect the notice by position rather than by text: pam emits its informational
messages within an attempt before the password prompt. Collect every non-prompt
info message and, once the prompt arrives, surface the collected lines (minus
the prompt itself) as lockMessage. If the stack short-circuits without ever
prompting (e.g. pam_faillock preauth configured as requisite), the notice is
surfaced on completion instead. This is locale-independent. A per-attempt flag
keeps the message stable across repeated locked attempts and retires it when an
attempt completes without a lockout (faillock reset / unlock_time elapsed).

Fixes #2647
2026-06-18 09:54:02 -04:00
Ira Limitanei 475ef5d1ca feat(launcher): make dms color picker entry toggleable (#2667) 2026-06-18 08:46:04 -04:00
Ralph Zhou 2f37019782 fix(popouts): keep Hyprland focus during close (#2655) 2026-06-18 01:41:25 -04:00
Rocho 9f4123cc3c fix(bar): clear blur region when the bar is hidden (auto-hide) (#2658)
When a bar with background transparency + blur uses auto-hide, the
ext-background-effect-v1 blur region was only slid off-surface via the
reveal Translate, but the region object stayed non-empty.

Hyprland gates layer-surface blur on `!m_blurRegion.empty()`, so the
non-empty region keeps blur enabled; the renderer then intersects the
off-surface region with the surface box, the clip degenerates to empty,
and an empty clip is treated as "unclipped" — so the whole bar surface
box gets blurred, leaving a blurred strip where the hidden bar would be.
(niri clips correctly, so it never showed there.)

Gate the published blur region on `barRevealed`: tear it down (null)
whenever the bar is not currently shown, so the region is genuinely
empty and the compositor disables the effect. Fixes #2656.
2026-06-18 00:58:19 -04:00
Yechiel 482a87a80d feat(bar): add visual indication for connected bluetooth (#2662) 2026-06-18 00:57:27 -04:00
jbwfu b925010cb3 fix(clipboard): paste selected history entry (#2660) 2026-06-18 00:57:07 -04:00
Huỳnh Thiện Lộc 085ce01da6 feat: port critical battery alert from DankBatteryAlerts plugin (#2664)
* feat: port critical battery alert from DankBatteryAlerts plugin

* ui: shorten notification type labels

* refactor: remove duplicate battery charge limit from Power & Sleep
2026-06-17 22:14:57 -04:00
Jonas Blochandbbedward 7af530de8f fix(ui): use primaryText instead of primary for text and icons displayed on primaryContainer background in the launcher (#2586)
* fix(ui): use primaryText instead of primary for text and icons displayed on primaryContainer background in the launcher

* fix(ui): use onPrimaryContainer instead of primaryText on primaryContainer background

* launcher pills: switch to buttonBg buttonText

---------

Co-authored-by: bbedward <[email protected]>
2026-06-17 21:58:11 -04:00
bbedward 9a4cff4e49 settings: fix index conflict on battery tab 2026-06-17 21:30:52 -04:00
Huỳnh Thiện Lộc 480ffa4ac2 feat: add battery settings tab and update battery widget interactions (#2634)
* feat: add battery settings tab and update battery widget interactions

* fix: import Quickshell.Io in BatteryTab.qml to fix Process type compilation error

* feat: move battery tab under media player and add notify when charge limit reached option

* chore: change default notification settings to false

* feat: move battery tab under Power & Security section in settings

* feat: add notification type button selection for battery alerts
2026-06-17 20:29:23 -04:00
dionjoshualoboandpurian23 d5ac0c9aa0 feat(clipboard): add type filters to clipboard history (#2640)
* feat(clipboard): add active filter state

* feat(clipboard): add clipboard filtering logic

* feat(clipboard): wire clipboard filter state to UI

* feat(clipboard): add filter dropdown

* feat(clipboard): move filter dropdown beside search

* refactor(clipboard): update filter defaults

---------

Co-authored-by: purian23 <[email protected]>
2026-06-17 17:27:45 -04:00
bbedward 29f19b07a9 widgets: fix tooltip position mapping 2026-06-17 12:46:24 -04:00
purian23 39301c534c add: DankColorSwatch component & settings sync 2026-06-17 11:38:25 -04:00
Rocho 58b9e4bda7 fix(dropdown): respect Animation Duration setting for DankDropdown popups (#2661)
DankDropdown popups opened and closed at a fixed speed regardless of the
configured Animation Duration. The Popup inherited Qt Material's default
enter/exit transitions, whose durations are hardcoded and never reference
Theme.shortDuration.

Override enter/exit with theme-driven transitions that keep the Material
grow/fade look (scale + opacity) but read their duration from
Theme.shortDuration, so every DankDropdown instance follows the
animation-speed setting.

Fixes #2659
2026-06-17 09:46:09 -04:00
purian23 820a9ce983 refactor(settings): colors & cleanup 2026-06-17 00:32:58 -04:00
purian23 68410e882d feat(dbar): add workspace & widget color customization options 2026-06-17 00:02:51 -04:00
bbedward f26c0af39a keybinds: add toggle to switch to FloatingWindow and back 2026-06-16 23:03:19 -04:00
Rocho 0ca451483f fix(shell): don't treat DPMS off/on as a screen-reconnect recovery storm (#2654)
A DPMS off/on cycle removes an output from Quickshell.screens and re-adds
it, which DMSShell's onScreensChanged cannot distinguish from a hotplug. It
fired triggerSurfaceRecovery() on every such event; on hardware where
recreating layer-shell surfaces re-wakes the just-powered-down output, this
drives an endless recovery storm that visibly power-cycles the monitor.

Route the screen-reconnect path through a 450 ms debounce (collapsing the
output-remove + re-add pair into a single pass) followed by a 4 s cooldown,
so repeated flaps trigger at most one recovery per window. Recovery still
runs once per resume, so the partial-DPMS-resume recovery added for #2579 is
preserved. The session-resume path runs its own recovery directly and now
clears any queued screen-reconnect recovery to avoid a redundant follow-up.

Fixes #2642
2026-06-16 22:58:21 -04:00
purian23 2849dd0ba2 feat(distros): Add Ubuntu 26.10 Stingray Support 2026-06-16 22:50:58 -04:00
bbedward df41ae4acb running apps: fix blurred tooltips 2026-06-16 18:26:29 -04:00
lingdianshirenandlingdiansr 2692777707 fix(weather): robust location resolution with parallel fetch and multi-tier fallback (#2638)
Decouple weather data fetching from reverse geocoding so that
weather loads as soon as coordinates are available, even when
Nominatim is unreachable (e.g. mainland China).

- Fetch Open-Meteo weather immediately once lat/lon are known.
- Resolve city name in parallel via Nominatim -> Photon -> BigDataCloud.
- If all reverse geocoding fails, keep displaying weather with a
  placeholder city name.
- Skip reverse geocoding entirely when the user has configured a
  city name.
- Fall back to ip-api.com when GeoClue2 is unavailable or returns
  zero coordinates.
- Add request-generation tracking to discard stale geocoding
  responses after location changes.
- Hold explicit WeatherService refs in bar widget and dashboard tab.

Co-authored-by: lingdiansr <[email protected]>
2026-06-16 14:42:01 -04:00
Rocho ca1a45ccf8 fix(lock): dismiss fade-to-lock overlay when using a custom lock command (#2653)
When a custom lock command is configured, Lock.lock() runs the command and
returns early without engaging WlSessionLock, so IdleService.isShellLocked
never transitions true->false. That transition is the only trigger that
dismisses a completed FadeToLockWindow, so the fully-faded black overlay stays
on screen and the desktop is unusable after re-login (regression from b8f4c35,
which added the _completed guard and tied dismissal solely to isShellLocked).

Add a dedicated dismissFadeToLock signal that the custom-lock branch emits
after launching the external locker, mirroring the existing fade signal wiring,
so the overlay is handed off and torn down. The built-in WlSessionLock path is
unchanged and still dismisses on unlock.

Fixes #2595
2026-06-16 13:45:28 -04:00
jbwfu 2f39f248fc fix(network): keep Wi-Fi when password prompt is canceled (#2651) 2026-06-16 12:52:25 -04:00
Rocho 90f8ce5035 feat(sounds): make muting sounds during media playback configurable (#2652)
Commit e3dbaed started skipping all system sounds (notifications, volume,
power, login) whenever an MPRIS player is playing, with no way to opt out.
Users who rely on notification sounds while listening to music lost them
entirely.

Add a 'Mute During Playback' toggle (SettingsData.muteSoundsWhenMediaPlaying,
default true) so the current behaviour is preserved, but users can disable
it to restore audible sounds while media is playing. The media check is
wrapped in shouldMuteForMedia(), which gates on the new setting before
querying the active player.

Closes #2616
2026-06-16 12:49:51 -04:00
Adwait Adhikari cb29125580 add lexical-binding (#2649) 2026-06-16 09:19:38 -04:00
Rocho 988b54515e feat(tailscale): add connect/disconnect, exit-node and LAN-access controls (#2644)
* feat(tailscale): add connect/disconnect/exit-node/LAN-access backend

The Tailscale backend previously exposed only read-only status
(tailscale.getStatus, tailscale.refresh). This adds write actions through the
existing tailscale.com/client/local integration:

- tailscale.connect / tailscale.disconnect (EditPrefs WantRunning)
- tailscale.setExitNode (EditPrefs ExitNodeID; empty id clears it and any
  legacy ExitNodeIP, mirroring `tailscale set --exit-node`)
- tailscale.setAllowLanAccess (EditPrefs ExitNodeAllowLANAccess)

The manager's client interface gains GetPrefs/EditPrefs; fetchState merges
ExitNodeAllowLANAccess from prefs, and Peer exposes ExitNodeOption so the UI
can list exit-node-capable peers.

* feat(tailscale): expose the new actions in TailscaleService

Adds connectTailscale/disconnectTailscale, setExitNode/clearExitNode and
setAllowLanAccess wrappers, plus derived exitNodeOptions/currentExitNode and the
exitNodeAllowLanAccess state. Write-action errors surface via ToastService.

* feat(tailscale): add connection, exit-node and LAN-access controls to the widget

The control-center widget toggle was a no-op. It now connects/disconnects, and
the detail panel gains a connection status row with a connect/disconnect button,
an exit-node picker and a LAN-access toggle.
2026-06-16 09:08:22 -04:00
Rocho 2fd9de5062 fix(keybinds): record numpad keys as KP_* keysyms (#2645)
* fix(keybinds): record numpad keys as KP_* keysyms

The shortcut recorder passed only the Qt key code to xkbKeyFromQtKey and
dropped Qt.KeypadModifier. Since Qt reuses the same Qt::Key_* values for the
numpad and the main row / nav cluster, numpad presses collapsed onto their
twins: numpad-7 became "7" (NumLock on) or "Home" (NumLock off) instead of
"KP_7"/"KP_Home", numpad-+ became "Equal", numpad-* became "8", numpad Enter
became "Return". numpad-5 with NumLock off (Qt.Key_Clear) was missing from the
map entirely, so the capture was silently dropped.

niri and the other providers bind against the xkb KP_* keysym names, so these
tokens never matched the physical key.

Pass the keypad flag through to xkbKeyFromQtKey and map keypad presses to the
KP_* keysyms: KP_0..KP_9 for the NumLock-on digit codes, the navigation names
(KP_Home, KP_End, KP_Up, ...) for the NumLock-off codes, plus the operators
and KP_Enter. Main-row keys are unaffected because they never carry the keypad
modifier.

* fix(keybinds): ignore lock keys while capturing a shortcut

NumLock/CapsLock/ScrollLock are toggles, not useful bind targets. Pressing
NumLock to switch the numpad between its digit and navigation keysyms
(KP_7 vs KP_Home) was captured as the bind itself (e.g. "Super+Num_Lock").
Skip them in the recorder like the other pure modifier keys already are.
2026-06-16 09:07:48 -04:00
Rocho fd5aabcb17 fix(keybinds): parse niri configs with leading-underscore identifiers (#2646)
DMS reads the niri config with kdl-go, which rejects '_' as the first
character of a bare identifier ("unexpected character _") even though niri's
own parser and the KDL spec accept it. The common trigger is the
`_JAVA_AWT_WM_NONREPARENTING "1"` environment node (the standard Java /
tiling-WM fix). When the parse aborts, `dms keybinds show` returns nothing and
the Keyboard Shortcuts UI shows no binds at all.

Extend the existing preprocessor approach (the brace fix from #2230) with
quoteLeadingUnderscoreIdents, which double-quotes bare identifiers that begin
with '_' before the text reaches kdl-go. The scan is string/comment aware and
only touches a leading '_' at a token boundary, so mid-identifier underscores
(XDG_CURRENT_DESKTOP) and underscores inside strings/comments are left alone.
Token boundaries include the ends of block comments and KDL slashdash (/-), so
a node abutting a comment with no whitespace is handled too. This is safe
because the niri parser only dispatches on fixed node/section names that never
start with '_', so re-quoting such a name cannot change what DMS reads.

Refs #2230
2026-06-16 09:06:55 -04:00
jbwfu 85b63219b9 feat(network): add saved WiFi state to settings (#2648) 2026-06-16 09:06:29 -04:00
bbedward ddf943846f i18n: add Vietnamese 2026-06-15 23:47:12 -04:00
purian23 e7221ec623 fix(powermodal): use overlay layer in standalone mode 2026-06-15 21:41:00 -04:00
bbedward 78daaf0cb4 calendar: remove launch button from settings 2026-06-15 19:02:22 -04:00
jbwfu a6ab3bab4c fix(settings): dedupe search index tab entries (#2643) 2026-06-15 16:50:41 -04:00
bbedward 53cea7023f calendar: rename dcal binary 2026-06-15 15:26:06 -04:00
jbwfu a098088f03 refactor(settings): split network settings into tabs (#2633) 2026-06-15 15:21:02 -04:00
bbedward 59998e9fd2 calendar(dank): Add support for DankCalendar backend
- Add keyboard navigation to overview
- Add edit events to overview
- Add create events to overview
- Add setting for auto/khal/dankcalendar backend selection
2026-06-15 14:02:35 -04:00
purian23 1df7e478df fix(FileBrowser): Improve save-to-file handling w/safety override diags
Fixes #2641
2026-06-15 01:11:32 -04:00
Artrix 1fc4890857 tray: add automatic overflow popup (#2629) 2026-06-15 00:10:02 -04:00
purian23 f5d52f1506 update(ipc): docs & dms ipc list 2026-06-14 23:35:22 -04:00
purian23 2026ba5bd2 fix(Notepad): clean up edge cases & updated popout handling 2026-06-14 22:15:34 -04:00
purian23 db56c8d74d feat(Notepad): Complete refactor - New popout mode & settings
- Add a full popout Notepad experience w/new layout settings
- New ability to choose the left or right side of the screen
- Add more safegaurds to preserve user data throughout
- New banner to show file reload/conflict handling
- New extensionless file support
- Polish settings with gap controls, compact buttons, and shortcuts help
2026-06-14 20:20:36 -04:00
Huỳnh Thiện Lộc 9d1a81c93c feat(media-control): support scroll and right-click on audio output devices in media popout (#2615)
* feat(media-control): support scroll and right-click on audio output devices in media popout

* feat(media-control): make device list volume scrolling optional
2026-06-13 17:51:03 -04:00
bbedward 3701b3d7a3 wallpaper: re-introduce updatesEnabled 2026-06-12 20:00:53 -04:00
purian23 bae98daa5c fix(wallpaper): simplify wallpaper rendering logic & reliability
- Keep wallpaper surfaces persistent and remove `updatesEnabled` throttling that could leave wallpapers grey or frozen after DPMS, suspend, fullscreen, or output changes

Fixes #2612
Fixes #2299
Fixes #2272
Fixes #2028
2026-06-12 17:30:54 -04:00
jbwfu b34a04f723 fix(clipboard): hide pin action while keeping saved indicator (#2626) 2026-06-12 15:39:23 -04:00
purian23 1c0245f2db fix(translations): add newline at end of JSON file and output file 2026-06-12 15:06:36 -04:00
purian23 7777e87dc8 refactor(settings): reorg to break out sections and verbiage 2026-06-12 14:57:25 -04:00
jbwfu 820fa07846 feat(settings): add clipboard entry action visibility controls (#2621)
* feat(settings): add clipboard entry action visibility controls

* fix(clipboard): show pinned indicator for saved entries when pin action is hidden
2026-06-12 14:08:05 -04:00
purian23 66794582c9 fix(fullscreen): retain user dbar standalone configs while in framemode fullscreen 2026-06-12 12:39:38 -04:00
jbwfu 73eb471ae3 fix(clipboard): keep first item selected when navigating upward (#2622) 2026-06-12 11:35:07 -04:00
jbwfu 0f2f4b96c4 Fix/clipboard confirmation keyboard safety (#2623)
* fix(clipboard): improve confirmation dialog keyboard focus

* fix(clipboard): require confirmation for clear-all shortcut
2026-06-12 11:34:16 -04:00
purian23 d53809cf2b refactor(framemode): unify connected surface chrome via SDF pipeline
- Shadow system rewrite with SDF quads
- Replace ConnectedShape/layer FBOs w/frame & chrome SDF shaders
- Improve frame blur performance
- Plugin performance gate
2026-06-12 11:03:39 -04:00
Klesh WongandKlesh Wong 08fd6e26d8 feat(notifications): user-configurable font size for notification summary and body (#2461)
* feat(notifications): add user-configurable font size for summary and body in notification popups

* feat: add Unset for falling back to previous default values

* fix: prek hook errors

---------

Co-authored-by: Klesh Wong <[email protected]>
2026-06-11 15:40:33 -04:00
Youseffo13 29e8470f2e fix(settings): fix text truncation in some section of settings and update icons (#2618)
* fixed spacing issues

* added one missing icon and replaced two
2026-06-11 15:35:51 -04:00
573785d4ce feat(notifications): add opt-in timeout progress bar on popups (#2587)
Adds a thin bar pinned to the bottom of the notification card that drains
full->empty over the auto-dismiss timer, as a visual countdown to
dismissal. Opt-in via notificationShowTimeoutBar (default off), with a
toggle in Settings > Notifications. Shown for any timed notification
(timer.interval > 0, including timed criticals); inset by the corner
radius, and frozen while hovered or during the exit animation. Plain
Rectangle - no offscreen textures or shader passes. A Connections on the
timer resets the bar on every (re)start, including the in-place restart
on a deduped notification.

Co-authored-by: bogdan-velicu <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-11 15:22:22 -04:00
jbwfu 5483303714 Fix/clipboard pinned recents dedupe (#2605)
* fix(clipboard): unpin pinned duplicates from history entries

* fix(clipboard): dedupe recents when using pinned entries
2026-06-11 15:05:28 -04:00
David MirelesandClaude Opus 4.8 5a5cc4f4e9 feat(plugins): expose scan/rescan/reload IPC handlers for runtime plugin discovery (#2611)
* feat(plugins): expose IPC handlers for runtime plugin discovery

Follow-up to #1659. That issue landed hot-reload for settings.json via
FileView.watchChanges + a 1ms Timer to skirt the JSON parse race. It does
not cover plugin discovery in runtime: adding a new plugin directory to
~/.config/DankMaterialShell/plugins/ while the shell is running is not
consistently picked up by the existing FolderListModel watcher in
PluginService.qml, and there is no IPC handle for forcing a rescan from
outside the shell.

Adds an IpcHandler on PluginService with five small functions:

- scan(): wraps existing scanPlugins(), returns count snapshot
- rescan(pluginId): wraps existing forceRescanPlugin(id), validates id
- reload(pluginId): wraps existing reloadPlugin(id), validates id
- list(): newline-joined id\tloaded\ttype\tname for every known plugin
- status(pluginId): loaded\ttype\terror for one plugin

Scope intentionally small: no file-watcher changes, no new daemons, no
schema additions. Target string "plugins" does not collide with any
existing target in DMSShellIPC.qml.

Validation:
- qs ipc --pid <PID> call plugins list returns one row per known plugin
- qs ipc --pid <PID> call plugins scan returns SCAN_TRIGGERED with count
- qs ipc --pid <PID> call plugins rescan <id> returns RESCAN_TRIGGERED
- Empty-arg paths return ERROR strings instead of throwing
- git merge-tree against origin/master is clean

* hardening(plugins): fix 7 review findings in scan-ipc IPC handlers

Follow-up to commit 43603f56 which ported PR #2601 (AvengeMedia scan-ipc)
to the fork. The original port was functionally correct but had seven
review issues that would block upstream adoption. This patch addresses
each one with a minimal, focused change.

* B1 IPC target collision: renamed `target: "plugins"` to
  `target: "plugin-scan"`. The original name collided with the
  existing IpcHandler in DMSShellIPC.qml:1180 which already registers
  enable/disable/toggle/list/status under "plugins". The split keeps
  both APIs discoverable without one shadowing the other.

* H1 Fire-and-forget scan: documented that scan() returns the
  pre-debounce count and that callers must poll list/status (or wait
  ~200ms) to observe the post-debounce state. A proper requestId +
  await mechanism was considered and rejected for scope reasons.

* H2 TOCTOU in rescan(): the handler now reads availablePlugins[id]
  inside forceRescanPlugin via the id string only — no captured
  object reference. A parallel resyncDebounce tick can otherwise
  mutate the entry between the read and the use.

* M1 list() cap: added a 256-entry cap and a leading header line
  (`# count=N returned=M`) so callers can detect truncation. A
  hostile / buggy plugin mass-creating entries could otherwise
  allocate 80 KB+ per IPC call.

* M2 status() prefix: "unknown\t\t" became
  `ERROR: unknown pluginId '...'` to match the rest of the
  handlers' prefix convention. Empty trailing field means no error.

* M3 id sanitization: every handler that takes pluginId now
  validates against `/^[a-zA-Z0-9_\-:]{1,64}$` before use. This
  rejects shell-injection payloads ("foo\tmalicious") and prototype
  pollution attempts ("__proto__", "constructor"). The list() and
  status() handlers also sanitize \t/\n in name and error fields
  so callers can rely on the TSV structure.

Verification: brace count balanced (252/252). Manual read of all
five handlers confirms no logic regression. QML runtime tests are
not part of the DMS test suite, so end-to-end validation requires
rebuilding the shell — deferred to the user.

Not pushed. Stage-local-first rule.

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

* refactor(plugins): strip inline comments per review feedback

Purian23 in PR #2611 review: 'let's address the amount of line
comments in the code, there's not a need for all of them to exist.'

Removed 48 comment lines. The substantive justification (why the
regex, why fire-and-forget, why re-read inside forceRescanPlugin,
why the 256 cap, why the target rename) now lives in the PR body
under 'Review-driven fixes in this iteration' and 'What changed'
where the reviewer already reads it.

No code logic changed. Brace count 252/252. Diff is -48/+0 on
quickshell/Services/PluginService.qml.

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

---------
2026-06-11 14:44:41 -04:00
bbedward cd672c341f settings: add DankSpinner, re-org some settings 2026-06-10 18:53:43 -04:00
bbedward 12438d63c2 mango: remove legacy dwl service 2026-06-10 17:01:03 -04:00
Ralph Zhou 35255e4053 fix(lock): bypass IME for password input (#2609) 2026-06-10 16:29:05 -04:00
goatnath 8856d45887 add local to-do planner / tasks to calendar overvie… (#2583)
* feat(quickshell): add local to-do planner / tasks to calendar overview card

* feat(quickshell): add auto-focus and task reordering support in calendar planner

* feat(quickshell): implement smooth drag-and-drop task reordering and inline editing

* fix(quickshell): resolve overlap and jitter in task drag-and-drop

* fix(quickshell): fix boundary swaps and prevent task list scrambling on reload

* fix(quickshell): resolve race, fix qml error, simplify dragging, and remove python dependency

* fix(quickshell): use Log service instead of console.warn in CalendarService

* style: format QML files w/qmlformat-qt6

---------
2026-06-09 00:43:41 -04:00
jbwfu 38af56c6fd fix(clipboard): exit saved filter when pinned entries are empty (#2604) 2026-06-08 23:54:06 -04:00
jbwfu 9111e4809d fix(powermenu): close control center on lock and power actions (#2598) 2026-06-08 23:53:43 -04:00
purian23 d08c7c5e55 refactor(frame): improve connected mode surface recovery
- share modal and launcher ownership handling
- recover missing background and blur layers
2026-06-07 17:47:24 -04:00
purian23 69f3dee25a feat(settings): add compositor section & restructured settings
- add dedicated Compositor pages for comp specifc features
- add Dank Bar Appearance subsection
- improve lazy loading, caching, search routing, & IPC navigation
- standardized responsive Setting categories from global animations
2026-06-07 03:52:00 -04:00
purian23 8155970ba2 fix(fonts): auto-rebuild font cache when configured fonts are missing
- Add Fonts category to dms doctor for manual diagnostics
- Fix a default font setting warning
2026-06-06 19:24:52 -04:00
pathmann d356957dad fix: ignore keyboard shortcuts of disabled powermenu actions (#2580)
* fix: ignore keyboard shortcuts of disabled powermenu actions

* fix typo when checking for lock shortcut

* ignore shortcuts for hidden powermenu actions in grid navigation

* ignore keyboard shortcuts of disabled actions in lock power menu

* ignore keyboard shortcuts of disabled actions in lock power menu (list navigation)
2026-06-06 18:28:38 -04:00
purian23 e7ccb702a3 refactor: update KeybindsModal dynamic sizing 2026-06-05 23:17:14 -04:00
Connor Welsh bf3ce6deb2 fix(osd): size from PanelWindow.screen (#2582) 2026-06-05 23:14:51 -04:00
purian23 f5295fb35d fix(greeter): remove auto-login state resolution from first install configuration
- Update auto-login command to:
`dms greeter sync --autologin`
2026-06-05 23:05:56 -04:00
purian23 6c5836722a fix(authModals): enable overlay layer for for auth popups 2026-06-05 21:27:37 -04:00
Youseffo13 5716249bd9 (Control Center): revamp of 25% pill option (#2568)
* revamp of control center

* update comment of SmallCompoundButton.qml
2026-06-05 19:46:01 -04:00
purian23 4d0aab773b fix(wallpaper): external management toggle & partial monitor DPMS recovery
Fixes #2579, #2581
2026-06-05 19:36:23 -04:00
purian23 e50ac208e3 feat(mangowm): add live config reloads & misc QOL updates
- Hide workspace tags during Mango overview
- Add HJKL focus/move defaults
- Add Mango natural touchpad scrolling &  cursor configs
- Fix Mango startup
2026-06-05 10:53:26 -04:00
bbedward bcb5617194 plugins: add support for composite plugins
- single plugin can register multiple types - e.g. daemon, bar widget,
  desktop widget
2026-06-05 10:33:34 -04:00
bbedward d3c23ba737 settings: add missing tabs to index and tweak search scoring 2026-06-05 09:49:49 -04:00
purian23 e0ab0a6b90 feat(keybinds): new mango overview workspace pill & keybind updates 2026-06-04 23:23:03 -04:00
purian23 713ce5f430 fix(display): unify compositor output profiles 2026-06-04 22:25:56 -04:00
purian23 8eb23bcc29 feat(mango): first-class MangoWM support across DMS, dankinstaller & UI tools
- Bring up Mango to parity with niri/hyprland via a native JSON-IPC w/Native MangoServic., replaces the legacy dwl/`mmsg` path and recent breaking changes
- Dankinstall: mango supported installer, config/binds templates, and packaging (Arch AUR, Fedora Terra auto-enable, Gentoo GURU)
- Window rules: Go provider + CLI + Settings GUI editor
- Keybinds + config reload on edit (mmsg dispatch reload_config)
- Misc new supported options in DMS settings
2026-06-04 18:45:04 -04:00
CoastlineSec 4181343ef3 feat(keybinds): add default app IPC actions (#2546) 2026-06-04 10:16:02 -04:00
bbedward d16566aa8d gh: add PR template 2026-06-04 10:15:44 -04:00
Nachum BarcohenandClaude Opus 4.8 45eb101f40 fix(lock): wake monitors on input when powered off by lock (#2572)
The "power off monitors on lock" path relies on wake handlers
(MouseArea/Keys) at Lock.qml's root Scope, which sit outside the
WlSessionLock surface and never receive input while locked. The feature
only appeared to work on compositors that auto-restore output power on
input; compositors that fully tear down the output (e.g. MangoWC
disable_monitor) leave the screen off until blind-unlock.

Add a seat-level IdleMonitor (wlr idle-notify, surface-independent) in
IdleService that restores monitor power on any keyboard/pointer
activity. Gated on isShellLocked + monitorsOff + the setting, so it is
inert unless that path actually powered the monitors off.

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-04 10:05:59 -04:00
purian23 59431869dc fix(keybinds): guard missing fields in cheatsheet search filter
Fixes #2116
2026-06-03 20:06:14 -04:00
purian23 6e7aca8b15 feat(window-rules): add niri default-floating-position rule
- Closes #2018
2026-06-03 19:43:23 -04:00
purian23 6f387b0481 fix(windowrules): honor $XDG_CONFIG_HOME for config paths
- Window rule read/write/includes detection and setup hardcoded
`$HOME/.config` so rules weren't applied when `XDG_CONFIG_HOME` pointed
elsewhere.
- Fixes #2289
2026-06-03 19:01:25 -04:00
purian23 82d4364032 feat(hyprland): add Resize on Border layout option 2026-06-03 18:45:33 -04:00
purian23 e3de54c941 feat(workspace): optionally group app icons on the active workspace
Closes #1208
2026-06-03 18:32:53 -04:00
purian23 6991b45fbe fix(settings): surface Windows Rules Tab for Hyprland
- small hyprland parser update
2026-06-03 17:39:44 -04:00
bbedward e5fff91ae6 notification: fix modal escape key handling in connected mode
fixes #2566
2026-06-03 10:49:51 -04:00
bbedward 2f2d4c9d9b i18n: remove redundant terms and sync 2026-06-03 10:32:51 -04:00
bbedward bfca1b46a6 greeter: support lua hyprland configs
fixes #2565
2026-06-03 09:52:45 -04:00
bbedward b117c80e47 settings: fix blur error message 2026-06-03 09:46:15 -04:00
purian23 d20aa3b80a feat(window-rules): view & convert external rules to DMS
- Read and convert external compositor rules into editable DMS rules
- Preserve niri multi-match rules and add match editor
- niri background-effect (blur/xray/noise/saturation) support
2026-06-03 08:59:51 -04:00
purian23 a34fda984d fix(Clipboard): stale image entry handling
- Resolved random DMS API errors & QML Warnings
2026-06-03 03:17:02 -04:00
purian23 510269dda9 fix(keybinds): show percentage amount in titles & performance improvements 2026-06-03 00:35:19 -04:00
purian23 d51b34797c refactor(Hyprland): updated the default close/kill keybinds for lua 2026-06-02 23:16:05 -04:00
arfan d2905072c0 feat(settings): Added Settings Tab Autostart App (XDG Autostart) (#2535)
* feat(Autostart): add Autostart tab and application selection popup

* fix(AutoStartTab): update systemdUserDir property to use XDG_CONFIG_HOME

* fix(AutoStartTab): update autostartDir and systemdUserDir to use StandardPaths for config home

* refactor(AutoStartTab): use FileView & FolderListModel

* refactor(AutoStartTab): implement systemd override generation for autostart applications using FileView

* feat(AutoStartTab): add systemd check to determine environment and update tray icon visibility

* feat(SettingsSidebar, AutoStartTab, DesktopService): add autostart functionality and systemd checks

* feat(AutoStartTab): add hidden property support for desktop entries and toggle functionality

* feat(AutoStartTab): add initialize autostart directory and add toast if writer failed

* add(AutoStartTab): logging for scoped log tracking

---------
2026-06-02 22:52:58 -04:00
purian23 1ee42506b6 refactor(control-center): consolidate detail section height rules 2026-06-02 15:48:47 -04:00
Guilherme Pagano 84fe2d751f fix(control-center): honor plugin ccDetailHeight instead of fixed 250px slot (#2559) 2026-06-02 15:31:55 -04:00
euletheia 5d0fc48706 Add DoNotDisturb & IdleInhibitor Indicators for the ControlCenterButton widget + Add optional icon to KeyboardLayoutName widget (#2513)
* feat(ControlCenterButton): add IdleInhibitor icon

* feat(ControlCenterButton): add DoNotDisturb icon

* fix(WidgetTabSection): adjust text width for the SystemTray Widget popup

* feat(KeyboardLayoutName): add optional icon

* refactor(KeyboardLayoutName): simplify icon visibility logic by using root.showIcon

---------
2026-06-02 14:25:35 -04:00
purian23 335c5b4ac5 feat(Greeter): add auto-login feature for startup settings
- Introduced a new cli flag:
`dms greeter sync --autologin-only`
and updated UI toggle in Greeter settings
2026-06-02 02:03:02 -04:00
bbedward 8c20f448ed control center: improve drag handling
misc: fix layer shell enum usage
2026-06-01 13:16:47 -04:00
jbwfu 0a668df138 fix(clipboard): prefer image MIME types over text fallbacks (#2551) 2026-06-01 11:44:10 -04:00
Guilherme Pagano 3e4d2b4d46 feat(control-center): add DiskUsage widget config overlay with showMountPath toggle and standardized tile sizing (#2507)
* feat(control-center): add widget config overlay with showMountPath toggle for DiskUsage

Introduces WidgetConfigOverlay and DiskUsageWidgetConfigMenu components, allowing
users to toggle mount path visibility per DiskUsage widget in edit mode

* refactor(control-center): use Theme.iconSizeLarge and Theme.fontSizeLarge for small tiles

Standardize SmallDiskUsageButton and SmallBatteryButton sizing with Theme.iconSizeLarge
and Theme.fontSizeLarge, and unify font weight to Font.Bold on both tile widgets.

* fix(control-center): adjust SmallDiskUsageButton font size based on showMountPath

* refactor(control-center): simplify DiskUsage config menu i18n strings

Remove the redundant "Disk Usage Widget" title and the toggle description
to reduce translatable strings
2026-06-01 11:35:14 -04:00
jbwfu 12e43d120e fix(clipboard): raise CLI IPC scanner limit for large entry payloads (#2550) 2026-06-01 11:01:34 -04:00
jbwfu a9845bf3cd fix(wallpaper): redraw wallpaper layers when fill mode changes (#2542) 2026-06-01 10:36:50 -04:00
Graeme Foster e51ceed175 fix(network): exclude virtual ether links and prune stale ones from networkd (#2505)
The networkd backend treated any link reporting Type=ether as a wired uplink.
Podman bridges and veth pairs report Type=ether, so they were classified as
ethernet: isWired() short-circuited on Type and never consulted looksVirtual(),
which also lacked a podman prefix.

The link map was also never pruned. Links discovered at enumeration or via
signals were kept forever, so torn-down container interfaces lingered as
routable and could win the wired-uplink slot over the real NIC -- leaving the
indicator showing WiFi while a wired connection was active and default-routed.

- isWired()/isWireless() exclude virtual interfaces before consulting Type, and
  looksVirtual() now recognises podman.
- enumerateLinks() reconciles the cached map against ListLinks via syncLinks(),
  pruning links that no longer appear so dead interfaces don't accumulate.
2026-06-01 09:45:49 -04:00
Karel "Angerion" Čeledaandbbedward 304baf6f60 fix(CavaService): prevent 100% CPU EOF spin loop by using temp file (#2471)
* fix(CavaService): prevent 100% CPU EOF spin loop by using temp config file

* cava: make tmp file non-deterministic

---------

Co-authored-by: bbedward <[email protected]>
2026-06-01 09:45:16 -04:00
purian23 6b141a9b06 refactor(Hyprland): updates to Lua syntax/dispatchers 2026-06-01 09:31:19 -04:00
purian23 0c3659a612 feat(PluginBrowser): add sorting and filtering options for plugins
- Introduced sorting and filtering by installed, default, category, name, and author
2026-05-31 23:58:13 -04:00
purian23 a44bef5796 fix(Notifications): restore long message content from overflowing
- Addtional security escape patch
- Tweak Notification Center width
2026-05-31 22:38:50 -04:00
Youseffo13 b1ac6b0ef9 Add context to font weight keys, wrap render settings, fix uptime prefix (#2529)
* fix: i18n inconsistencies

* Update UserInfoCard.qml
2026-05-31 18:10:43 -04:00
purian23 98844a3b85 fix(Clipboard): prevent security risk HTML in window titles from fetching remote URLs
- Default StyledText to PlainText; keep RichText only on notification content
2026-05-31 17:41:55 -04:00
purian23 a32b8911c7 feat(Hyprland): add touchpad gesture support via DMS default configs 2026-05-31 15:45:17 -04:00
purian23 3118e7b9c3 fix(Hyprland): correct Lua keybind writes
- Write titles as Lua description metadata
- Use hl.dispatch for custom dispatcher actions
- Preserve legacy trailing comment titles on rewrite
- Update option edits before saving keybinds
2026-05-31 15:35:13 -04:00
Miguel Saliba 2ca2bc5fb8 fix: make matugen switch pywalfox dark/light mode (#2537)
Currently, when switching dark/light theme in DMS, pywalfox's mode does not get updated. This commit adds `pywalfox light/dark` to the matugen post_hook to fix that.
2026-05-31 00:35:06 -04:00
purian23 4bfb08f6ef fix(Hyprland): Lua config for display setup writes
- Check display include status on startup from legacy to lua
2026-05-31 00:11:30 -04:00
purian23 0689339780 feat(Hyprland): add fractional scaling display presets
- Show Hyprland scale presets that fit the active mode
- Preserve current dms setup values
2026-05-31 00:10:22 -04:00
purian23 a265625851 refactor(Hyprland): Update Lua migration and keybind writes
- emit native hl.dsp.* dispatchers for generated Lua keybinds
- keep legacy hyprland.conf installs read-only but preserved until dms setup migration
2026-05-30 23:07:06 -04:00
Bodyandpurian23 389fffaf64 feat(Clipboard-Bar-Hist): Add search/filter to saved clipboard entries & animation states (#2464)
* Fix gaps and overlaps when filtering clipboard history

* feat(Clipboard-Bar-Hist): Add search/filter to saved clipboard entries as well. Change title on toggle between recent/saved.

* keep Pinned/Saved icon highlighted when selected

* add back filter animations

* Implement snap state for list views based on animation settings

---------

Co-authored-by: purian23 <[email protected]>
2026-05-30 15:10:58 -04:00
purian23 b7daf3f64a feat(ipc): add powerprofile status & shared profile helpers
- Follow-up to PR #2515
2026-05-30 14:57:01 -04:00
Huỳnh Thiện Lộc 461da22b08 feat(ipc): add native powerprofile target for power profiles management (#2515)
* feat: add native powerprofile IPC target for power profiles management

* feat: show centered PowerProfileModal with 3 square buttons for powerprofile IPC toggle

* style: enhance PowerProfileModal size, icons, description, and keyboard hints

* feat: add Space key binding to select highlighted power profile
2026-05-30 14:51:19 -04:00
jbwfu 2b661e241d fix(settings): support localized settings search (#2521) 2026-05-30 01:55:46 -04:00
Kyunghyun Park d7df3800c2 keybinds: add missing XF86 key mappings (#2500) 2026-05-30 01:31:58 -04:00
Bogdanandpurian23 f2961f9b6a feat(DiskUsage): updated dynamic width options for DiskUsage widget (#2517)
* feat(WidgetsTabSection.qml): added dynamic width and static padding for DiskUsage

* feat(DiskUsage.qml):added functionality for dynamic width and static padding, also changed spacing to work like in cpu and ram monitor. Now they look complete and same

* fix(DiskUsage): restore display modes & formatting

---------

Co-authored-by: purian23 <[email protected]>
2026-05-30 01:02:15 -04:00
purian23 f2d5ee4692 fix(animation): adjust the Popout/Control Center motion 2026-05-29 22:04:46 -04:00
purian23 7c2d5ce15e fix(Screenshot): allow region capture over shell overlays 2026-05-29 17:03:19 -04:00
Lucas 5ceb908b8b greeter: remove keep-max-bpc-unchanged option (#2528) 2026-05-29 15:14:41 -04:00
Paul d819865853 fix: Display Configurator in Hyprland (#2506)
* fix: display configurator

* fix: replace
2026-05-28 17:26:14 -04:00
jbwfu 38176ab543 fix(settings): make desktop widget group delete button clickable (#2512) 2026-05-28 12:19:02 -04:00
purian23 53936d7034 Revert "fix(IconTheme): apply stored icon theme at startup (#2511)"
This reverts commit aafc2ea4d7.
2026-05-28 11:42:07 -04:00
aafc2ea4d7 fix(IconTheme): apply stored icon theme at startup (#2511)
Add applyStoredIconTheme() calls alongside existing applyStoredTheme()
calls in loadSettings(), onLoaded, and onLoadFailed, ensuring the stored
icon theme is synced to GTK/Qt/Cosmic configs on every startup and reload.

The applyStoredIconTheme() function and its _hooks entry already existed
in the codebase but were never invoked during initial settings loading.

Co-authored-by: lingdiansr <[email protected]>
Co-authored-by: Claude Opus 4.7 <[email protected]>
2026-05-28 11:18:44 -04:00
Connor Welsh 8a4be4936a Hide MPRIS players that have nothing to play (#2509)
* fix(Mpris): exclude idle players from active-player selection

Add MprisController.isIdle() (player Stopped with empty title and
artist). _resolveActivePlayer excludes idle players from every
selection path, and re-resolves when the active player itself goes
idle. Existing triggers (availablePlayers change, isPlaying becoming
true) do not fire for a player merely stopping.

The fallback now gates on !isIdle(p) instead of p.canPlay. canPlay
describes whether Play() would succeed; !isIdle describes whether
there is anything to surface. canControl unchanged.

When no eligible player remains, activePlayer becomes null and
consumers that gate on it unload (the bar media widget via
WidgetHost; the dash via the companion fix).

* fix(DankDash): show no-player state when active player resolves to null

showNoPlayerNow gated on _noneAvailable (player count === 0) or
activePlayer being idle. Neither covers activePlayer being null
while players remain registered, which is now possible (an
always-on player sitting stopped with empty metadata).

Key showNoPlayerNow on !activePlayer; drop the unreachable
_trulyIdle.

* add(MprisController): add track artist change handling to active player resolution

---------
2026-05-28 00:19:34 -04:00
purian23 af097d0f33 feat(Greeter): Enhance login experience & manual username fallback support 2026-05-27 22:31:54 -04:00
Ethan Todd 44867e7b43 fix: awk in new greeter (#2508) 2026-05-27 22:02:37 -04:00
purian23 a366bf3ca0 fix(ClipboardEditor): Support legacy QT 6.xx decoding & large clipboard data 2026-05-26 16:38:32 -04:00
Huỳnh Thiện Lộc 89f86be00a feat: unify media controls dropdown interactions, hover behavior and cycle controls (#2470)
* feat: unify media controls dropdown interactions, hover behavior and cycle controls

- Implement hover-to-show and hover-to-hide for all media control dropdowns.
- Make clicking the Output Devices and Media Players buttons cycle through items when expanded.
- Always display the 'speaker' icon for Output Devices to maintain visual consistency.
- Bind dropdown player properties dynamically to fix list stale rendering states.

* fix(DankDash): use trackArtist property for artist label in MediaPlayerTab

* fix(DankDash): simplify active player label for consistency with output devices

* feat(DankDash): display volume levels for audio output devices in dropdown

* fix(DankDash): display Unknown Artist when artist is empty in player list

* feat(DankDash): add keyboard shortcuts for seeking, track cycling and playback control in Media popout

* feat(DankDash): change Up/Down arrow keys to adjust volume in Media popout

* feat(DankDash): auto-open volume dropdown overlay when using Up/Down shortcuts

* feat(DankDash): add Key M shortcut to toggle mute in Media popout

* fix(mpris): clamp minimum seek position to 0.1s to prevent browser player reset

* fix(mpris): cache stable length to prevent browser transient reset issues

* fix(mpris): persist activePlayerStableLength in MprisController singleton

* fix(mpris): resolve browser player album art with raw metadata and YouTube url fallbacks

* fix(mpris): resolve browser player album art with local caching and 16:9 youtube fallbacks

* style(mpris): trim trailing whitespace in TrackArtService

* fix(mpris): address code review feedback on remote caching, stale artwork, and hover state

* fix: secure curl commands and prevent premature dropdown overlays closing on button re-hover
2026-05-26 13:44:51 -04:00
bbedward 12a744e985 clipboard: fix editing in popout 2026-05-26 11:49:14 -04:00
Guilherme Pagano 54f272ba1e fix(toast): align dimensions to whole pixels to avoid blurry rendering (#2494)
The toast Rectangle uses `layer.enabled: true`, which renders to a
texture before compositing. With fractional implicit/content sizes
(derived from text and icon metrics), the cached texture was being
sampled with sub-pixel interpolation and the toast looked blurry
under fractional-scale-aware compositors (e.g., niri).

Wrap toastWidth/toastHeight and implicitWidth/implicitHeight with
Theme.px(value, dpr), matching the alignment NotificationPopup.qml
already applies to its surface.
2026-05-26 11:23:10 -04:00
Cong Luan Tran 60b64f22c6 fix(BatteryService): Make bluetoothBattery detection actually work (#2486) 2026-05-26 11:22:39 -04:00
Niltempus 97666dc73d Wait for location capability before requesting state (#2476) 2026-05-26 11:16:42 -04:00
bbedward 6c6756936b i18n: sync 2026-05-26 11:09:06 -04:00
purian23 91f8ca4efe ci: upgrade prek-action to v2 2026-05-26 09:06:26 -04:00
purian23andNachum Barcohen 045ac59a44 feat(Clipboard): Clipboard Editor PR Revived (#2492)
* feat(clipboard): Add editing capability to clipboard entries
* Add split save menu for clipboard editor
* Add clipboard editor shortcuts and hints
* Show full clipboard text in editor
* feat(Clipboard): Revive ClipboardEditor PR

- Original PR #1916 by @nabaco
* fix(clipboard): restore Save button targets in editor

---------

Co-authored-by: Nachum Barcohen <[email protected]>
2026-05-25 23:25:57 -04:00
purian23 078180fe42 feat(Greeter): improved multi-user UI and per-user theme sync
- Introduce multi-account greeter login with per-user theme previews
- Add `dms greeter sync --profile` for secondary users with or without sudo
- Add Manage greeter group membership from Settings UI → Users Tab
2026-05-25 22:41:23 -04:00
purian23 d9525908f1 refactor(Notifications): further support for duplicate notification logic
- New setting to stack or suppress identical alerts (on by default)
Closes #2334
2026-05-24 22:22:34 -04:00
Lucas 6093c37b41 settings: add descriptions for DankBar menu (#2490) 2026-05-24 18:56:42 -04:00
purian23 bb05cbb6c5 feat(sessions): implement local user session switching functionality
- Core user is logged in tty1 while user two is in tty3, you can now seamlessly switch bewteen them
New IPC options:
- `dms ipc call sessions list`
- `dms switch-user [target]`
- New Powermenu switch users option
2026-05-24 18:33:38 -04:00
purian23 4d4af8f549 feat(Users): add user management UI in DMS Settings 2026-05-24 18:15:41 -04:00
Feng Yu 0b55fbcb15 fix(DankBar): Resolve tray freeze and wallpaper loss after DPMS resume (#2457)
Fixes #2354

Root cause (tray freeze): In clickThrough mode, the PanelWindow mask uses
sectionRect() with mapToItem() to compute input regions. After DPMS resume,
the PanelWindow is recreated with width=0, and mapToItem() returns wrong
positions. The right section's implicitWidth doesn't change after creation
(fixed-size tray icons), so the mask binding is never re-evaluated when the
compositor sets the actual screen width. Adding barWindow.width as a binding
dependency ensures the mask recalculates on resize.

Root cause (wallpaper loss): Wallpaper PanelWindows are recreated by Variants
during screen reconnection before the compositor finishes output initialization.
The wallpaper Image renders at 0x0 dimensions, resulting in a black screen.

Changes:
- DankBarWindow: add barWindow.width dependency to clickThrough mask bindings
- DMSShell: add surface recovery mechanism (screen reconnect + session resume)
  with progressive 2-pass timer (800ms + 2800ms) that recreates bar, Frame,
  wallpaper, and dock surfaces after the compositor is ready
- WlrOutputService: re-request output state on session resume
2026-05-22 09:05:41 -04:00
Domen KožarandClaude Opus 4.7 7476a220b5 feat: Blink WiFi/Bluetooth icons while connecting (#2448)
Pulses the WiFi and Bluetooth status icons while a connection is in
progress (lock screen, DankBar control center button, control center
compound pill). The pulse is implemented as a reusable Widgets/DankBlink
component, and the wifi-connecting condition is centralized as
NetworkService.isWifiConnecting.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-22 09:03:25 -04:00
Huỳnh Thiện Lộc aaff1ab61e feat: implement interactive microphone volume OSD and IPC controls (#2406)
* feat: implement interactive microphone volume OSD and persistence

Addresses #2388

* refactor: reduce scope to interactive microphone OSD and IPC controls only
2026-05-22 09:00:12 -04:00
Cloud 39622eb62a fix(lock): avoid U2F PAM polling in OR mode (#2459) 2026-05-22 08:59:11 -04:00
Lucas eea039f575 feat(Launcher/Spotlight): improve context keyboard navigation and mode persistence (#2467)
* feat(Spotlight): fix submenu keyboard navigation

* feat(Launcher/Spotlight): disable persisting last mode when changed by triggers

* feat(Launcher/Spotlight): add option to disable last mode being persisted

* fix(Launcher/Spotlight): fix context menu keys navigation

* fix(NiriOverviewOverlay): fix context menu keys navigation and position
2026-05-22 08:53:45 -04:00
purian23 ef5de19f6b distros(dms-greeter): Add sysusers.d immutable distro support
- Closes #1975
2026-05-21 23:21:44 -04:00
bbedward f0c31bd7b3 launcher: add /d /f file search prefixes. Fix prefix not always
triggering
2026-05-21 21:10:30 -04:00
Domen KožarandClaude Opus 4.7 7ddd0ca90d fix(Network): Bucket WiFi signal for stable list order (#2449)
Sort networks by signal bucket of 25 with SSID as tiebreaker so minor
RSSI fluctuations between scans no longer reshuffle the list while the
user is selecting a network.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-21 13:04:31 -04:00
bbedward b84e5abc4a core: remove niri parse test 2026-05-21 10:33:12 -04:00
purian23 fb9ec8e721 feat: (Launcher/Spotlight): Updated w/New Settings & QOL features
- New Spotlight toggle to show/hide chips, off by default
- Updated blur effects on all launcher inputs and footers
- Fixed previous queries resurfacing
- Upated Spotlight keyboard navigation
- Added functionality to show and shortcut to keybinds from the Launcher tab
2026-05-21 01:05:56 -04:00
purian23 078c9b4890 refactor(niri): Normalize key bindings to be case insensitive within DMS 2026-05-21 00:38:31 -04:00
purian23 37c98220a9 refactor(Spotlight): Use Spotlight alongside OG Launcher
- Update to add DMS Action keys in Keyboard Shortcuts
- Defaulted in niri/hyprland includes file as `Alt+Space`
- New (IPC): `dms ipc call spotlight-bar toggle`
- Slight UI update to follow user radius
2026-05-20 17:21:03 -04:00
Klesh WongandKlesh Wong fc07611b3b fix(osd): ensure OSD appears on all monitors after resume (#2453)
Some monitors, especially cheaper models, are slow to power on after sleep and may not be present in Quickshell.screens
when onScreensChanged is triggered. This change adds a 3-second delay before updating currentOSDsByScreen to ensure all
screens are detected, mitigating the issue of OSDs not appearing on certain monitors after resume.

Co-authored-by: Klesh Wong <[email protected]>
2026-05-20 11:58:13 -04:00
Graeme Foster a923308c09 networkd: classify links by Type instead of name prefix (#2447)
* networkd: classify links by Type instead of name prefix

The systemd-networkd backend decided wifi-vs-ethernet by checking
whether the interface name started with "wlan" or "wlp". Anything
else (that was not on a small virtual-prefix denylist) was treated
as wired ethernet. That misclassified two common cases:

* Nebula tunnels (kernel name like "nebula.homelab", Type=none,
  Kind=tun) showed up as a wired ethernet device — DMS rendered an
  "Ethernet connected" indicator whenever the overlay was up, even
  with no physical NIC plugged in.
* Renamed wifi interfaces (e.g. systemd link files that rename
  wlan0 to a friendlier name like "wifi") were also miscategorised
  as ethernet, because they no longer matched wlan*/wlp*.

networkd already publishes the real link kind in the JSON returned
by the per-link Describe method ("ether", "wlan", "loopback",
"none"). Fetch it during enumerateLinks, cache it on linkInfo, and
classify against that. The old prefix logic is kept as a fallback
for the case where Describe ever fails to populate Type.

The package-level looksVirtual() helper replaces the unexported
isVirtualInterface method so the new classification helpers and
their tests can use it without needing a live backend.

Tests cover both the Type-based and fallback paths, including the
Nebula-shaped Type=none/tun case that motivated this change.

* networkd: cache linkType across signal ticks and unit-test Describe fallback

enumerateLinks runs on every PropertiesChanged signal under
/org/freedesktop/network1, which fires on carrier flap, DHCP renew, and
each address change. The previous version rebuilt every linkInfo from
scratch on each tick, including a synchronous Describe D-Bus round-trip
per link, despite the link Type being fixed at netlink creation. Preserve
existing entries when the D-Bus path matches, refreshing only ifindex,
and only call fetchLinkType on a genuinely new entry. A link torn down
and re-created at a different path still triggers a refetch.

Extract parseDescribeType from fetchLinkType so the JSON failure path —
malformed payload, missing Type field, wrong type for Type — can be
exercised without a live D-Bus connection. The classifier's fallback to
name-prefix heuristics already had coverage; this locks in that the seam
between Describe and the classifier surfaces an empty string on every
failure mode rather than misclassifying a link.
2026-05-20 11:43:50 -04:00
Lichie 0990b43a43 feat(FocusedWindow): Improve content width calculation and add size options (#2444)
* use RowLayout in focusedapp widget for better width calculation

* Add context menu with additional size options for focused app widget
2026-05-20 11:43:14 -04:00
purian23 548c2305fb refactor(Settings): Rename fullscreen properties to overlay layer for consistency
- Updated property names from `showOverFullscreen` to `useOverlayLayer` across various components.
- Fixed Darken Modal Overlay in Standalone mode
2026-05-19 22:03:33 -04:00
purian23 4634763840 refactor(fullscreen): Refine fullscreen layering and frame overlay behavior
- Replaced fullscreen hide/reveal toggles with Show Over Fullscreen layer toggles
- Added Launcher opt to Show Over Fullscreen setting
- Kept fullscreen stacking compositor-owned via top/overlay layer choices
- Fixed Hyrland Special Workspaces
- Updated DMS Advanced Configuration docs
2026-05-19 18:42:45 -04:00
bbedward cdc1102092 popout: fix opening popouts across monitors
cc/brightness: fix delegate bindings and pinning
2026-05-19 11:23:03 -04:00
purian23 4845299cc2 fix(Spotlight): Update the new clipboard/settings merge w/cache & debouced refresh 2026-05-19 01:39:16 -04:00
purian23 81a1bb1cd7 fix(Hyprland): Respect legacy conf configs before migrating to lua 2026-05-18 16:51:25 -04:00
sima 4528552610 Fix Hyprland Lua dispatch helpers (#2443) 2026-05-18 13:34:49 -04:00
purian23 0b55bf5dac feat(Hyprland): Introduce Lua support for Hyprland configurations
- Note: We do not convert your existing conf configs to lua. This update only reflects DMS defaults state
- Updated README.md to reflect changes
- Updated Keyboard shortcut support
2026-05-18 13:06:58 -04:00
bbedward 8dd891f93a i18n: term sync 2026-05-18 09:43:25 -04:00
Josh Symonds 9bd68d44a1 notifications: honor freedesktop suppress-sound hint (#2440)
Senders that play their own audio for a notification can set the
standard org.freedesktop.Notifications "suppress-sound" boolean hint
to ask the server not to double up. NotificationService skipped its
sound only as a side effect of the dedup early-return (when an
identically-keyed popup was still visible), so transient notifications
double-sounded while lingering ones didn't — nondeterministic. Read
notif.hints["suppress-sound"] and gate the AudioService call on it.
2026-05-18 09:40:50 -04:00
Sheershak sharma 90ea136379 fixed batterylevel shown zero in case of unknown state (#2436) 2026-05-18 09:40:20 -04:00
Huỳnh Thiện Lộc 2f4a39f9eb fix(settings): ensure plugin browser install button resizes correctly (#2431)
Fixed a visual bug where the install button would overflow its container after a plugin was installed. This was caused by the width not re-evaluating correctly and StyledText's default elide/wrap properties interfering with implicit width calculations.
2026-05-18 09:40:00 -04:00
sima 5e558660c3 Fix Hyprland scrolling overview geometry (#2442) 2026-05-18 08:21:39 -04:00
purian23 c923c43322 feat(Clipboard): Implement clipboard/settings search functionality in DMS Launcher 2026-05-16 17:43:52 -04:00
purian23 9f2ae6241e feat(Spotlight): Add a New Lightweight Spotlight style launcher option 2026-05-16 17:34:56 -04:00
purian23 05c7a77c8b fix(Dock): Update Separate mode render to remove connected layer 2026-05-16 15:13:05 -04:00
purian23 a8ab0b55f3 (lint-qml): update output message 2026-05-16 15:11:54 -04:00
arfan fbcc28785a fix(HyprlandService): Correct window address format for focus dispatch (#2426) 2026-05-15 09:48:03 -04:00
bbedward 1c1ab1c7d5 text: change default rendering back to Qt 2026-05-15 09:45:21 -04:00
purian23 0a892a4a9e refactor(SysUpdate): Explicit Run on Startup option in settings
- Relocated DMS System Updater to System Setting section
- Removed forced auto refresh upon update & widget loading
2026-05-14 20:32:42 -04:00
bbedward e5cd9caba1 system update: fix App ID of terminal mode 2026-05-14 13:33:02 -04:00
bbedward 9c4aa06664 system update: fix local pacman DB mtime 2026-05-14 13:25:29 -04:00
Boris 66e38c5efe Support Hyprland lua dispatching (#2419) 2026-05-14 13:08:07 -04:00
bbedward 018795125e app picker: extend App Picker to integrate with mime overrides
- Adds "DMS Opener" as an option (dms-open.desktop)
- Add mime type GO utils
- Add rememberance to App Picker modal
2026-05-14 13:06:22 -04:00
Iris be4ea71756 feat(settings): Added Default Apps page to settings (#2416)
* feat(settings): Added Default Apps page to settings

Added a new page to settings. This page relies on xdg-mime and gio, so their existence is checked to show the page.
This logic and the mime type stuff was added to DesktopService.qml.
Slightly reordered the settings sidebar to include an Applications category

* fix(settings): read xdg-terminals.list directly

read the file directly instead of using xdg-terminal-exec which might not be installed
2026-05-14 10:45:18 -04:00
bbedward 39b90bb140 workspaces: cleanup dead connection 2026-05-14 10:38:47 -04:00
bbedward fb5198fd0b workspace/ext-ws: drop custom ext-workspace in favor of quickshell
WindowManager implementation
2026-05-14 10:34:35 -04:00
bbedward 71438530a8 dankinstall: fix gentoo parsing and use guru overlays for DMS/dgop 2026-05-14 10:13:37 -04:00
bbedward 79fe956058 workspaces/niri: fix scrolling to use ID instead of Index
fixes #2212
2026-05-14 10:03:11 -04:00
Divya Jain a33c7e0250 feat: add Display Profiles builtin control center widget (#2410)
* feat: add Display Profiles builtin control center widget

Adds a new built-in widget that lets users switch between their configured
display profiles directly from the Control Center, without opening Settings.

- Widget shows the active profile name and cycles profiles on pill click
- Detail panel shows all profiles as a button group for direct selection
- Integrates with existing DisplayConfigState/SettingsData singletons
- Follows the same loader/instance pattern as VPN, CUPS, and Tailscale widgets

* fix: refine display profiles widget behavior
2026-05-14 09:45:05 -04:00
Lucas 459ec47310 flake: use quickshell from nixpkgs-unstable pkgs (#2417) 2026-05-14 09:29:33 -04:00
Amaan Qureshi 4bb3dd8310 flake: expose mkDmsShell and buildDmsPkgs via lib output (#2409) 2026-05-14 02:40:55 -03:00
Domen KožarandClaude Opus 4.7 9a630fad92 fix(network): open Wi-Fi password modal upfront for enterprise networks (#2414)
NetworkManager rejects AddConnection for 802-1x without a non-empty
identity, so the new API v7 agent-prompt flow never reaches the
SecretAgent for fresh enterprise connections. Fall back to the existing
modal-upfront path (already wired to ask for username + password via
requiresEnterprise) when modelData.enterprise, mirroring the legacy
DMSService.apiVersion < 7 branch.

Fixes #2358

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-13 16:22:49 -04:00
purian23 5bde54fa89 refactor(dbar/dock): Update fullscreen hiding logic 2026-05-13 14:38:44 -04:00
Divya Jain 022f47a580 feat: add cycleProfile IPC command and default Mod+P keybind (#2411)
* feat: add cycleProfile IPC command for display outputs

Adds `dms ipc outputs cycleProfile` which cycles through configured display
profiles in order, wrapping around from the last back to the first.

Useful for binding to a compositor keybind (e.g. Super+P in niri/Hyprland)
without needing an external wrapper script.

* feat: add Mod+P default keybind for cycling display profiles

Binds Super+P to `dms ipc outputs cycleProfile` in both the niri and
Hyprland default keybind configs. Mod+Shift+P is already reserved for
powering off monitors on both compositors, so Mod+P is a natural fit.
2026-05-13 09:28:56 -04:00
Kilian Mio 0dfa95ffe4 feat: retrieve WiFi/VPN secrets from D-Bus Secret Service with unlock support (#2402)
* fix: preserve pre-existing connection profiles on cancelled WiFi activation

When a pre-existing WiFi connection activation is cancelled, do not
remove the connection profile — only remove profiles for newly created
connections.

* feat: retrieve WiFi/VPN secrets from D-Bus secret service with unlock

Wireless, 802.1x, VPN, and WireGuard secrets are now looked up from
org.freedesktop.Secret before prompting the user. If the keyring is
locked, the unlock prompt is triggered via Prompt.Prompt() and the
lookup retried after the vault is unlocked.
2026-05-13 09:27:44 -04:00
Youseffo13 e6da762870 Added Missing i18n Strings (#2398)
* Update hypr-colors.conf

* added i18n strings

* added missing i18n strings

* Update TypographyMotionTab.qml
2026-05-13 09:23:50 -04:00
purian23 8f958658dc (FrameMode): Update to load Connected Mode by default 2026-05-12 22:07:10 -04:00
Huỳnh Thiện Lộc 392eaea5fc refactor(ClipboardHistory): Sync toggle button between saved and recent tabs (#2407)
* fix(ClipboardHistory): toggle button now toggles between saved and recents tabs

- Change tooltip text to reflect current tab state ('Recent' when on saved tab, 'Saved' when on recents tab)
- Previously always switched to saved tab; now toggles between 'saved' and 'recents'

* refactor(ClipboardHistory): remove redundant History button, keep single toggle button

The Saved button now toggles between saved/recents tabs, so the separate History
button is redundant and has been removed for a cleaner UI.
2026-05-12 14:10:19 -04:00
bbedward 0aea542e8f i18n: term sync 2026-05-12 10:29:51 -04:00
bbedward 7a566e1088 tailscale: fix flaky test 2026-05-12 09:36:44 -04:00
Huỳnh Thiện Lộc 4bc62cc6ec feat: consolidate plugin directory management into a single dynamic button (#2400)
* feat: add Open Dir button to Plugins settings

* feat: consolidate plugin directory management into a single dynamic button
2026-05-12 09:29:17 -04:00
Bruno Rocha 9b68fc8213 add dms-plugin-dev agent skill for plugin development (#2394) 2026-05-12 09:26:38 -04:00
purian23 c878ffb7f9 fix(DankLauncher): Improve clipping logic for connected chrome
- Fixes #2397
2026-05-12 00:08:38 -04:00
purian23 3fc805ba53 distro(OBS): Bundled Go toolchain for dms-git Debian/openSUSE builds 2026-05-11 21:37:01 -04:00
euletheia 371a7d0cd1 fix(DankTextField): set TextInput 'fontFamily' (#2399)
This allows input fields created using the widget to inherit their font
family from Theme.
2026-05-11 18:25:10 -04:00
Bruno Rocha 189b7c84ce fix(ipc): dash and dankdash commands work without clock widget (#2392)
getPreferredBar("clockButtonRef") returns null when the clock widget
is removed from the bar. Fall back to getPreferredBar() (any bar) so
triggerDashTab can still open the popout with default positioning.
2026-05-11 14:18:07 -04:00
bbedward b8f4c350a8 quickshell: drop support for 0.2, require 0.3+
- Remove all compat code
- Rewire LegacyNetworkService to use Quickshell.Networking
- Add parentWindow to settings child windows
2026-05-11 13:05:24 -04:00
purian23 3989c7f801 fix(DisplayConfigState): Repair crash introduced in Automatic configs PR 2026-05-11 11:25:15 -04:00
bbedward 2690305724 fonts: native rendering + add settings to override renderType
fixes #2371
2026-05-11 10:49:29 -04:00
bbedward 676219bc09 screenshot: respect XDG_PICTURES_DIR
fixes #2383
2026-05-11 10:00:21 -04:00
bbedward b192b5f779 osd: fix positioning with multiple bars
fixes #2387
2026-05-11 09:51:18 -04:00
Kilian Mio a5352623fd Automatic Display Profiles Enhancement & Disabled Output Support (#2367)
* fix(niri): properly close KDL output block when disabled

* feat(display): parse off directive and sync disabled state from compositor configs

* feat(display): visual treatment and canvas filtering for disabled outputs

* feat(display): eager auto-profile generation with debounced auto-select

* refactor(display): pass target profile ID to confirmChanges and remove dead code

* fix(display): make profile dropdown reactive by binding to property directly

* i18n(display): add Disabled translation term for output state
2026-05-11 09:34:51 -04:00
Huỳnh Thiện Lộc 2b6ae58bff feat(settings): add expandable search bar to plugins tab (#2377) 2026-05-11 09:33:42 -04:00
Kostiantyn To b12511481d fix(RunningApps): add middle mouse click to close running applications in vertical orientation (#2386) 2026-05-11 09:21:45 -04:00
purian23 c7d44cfb12 fix(Settings): Update card registration logic to fix binded crash 2026-05-10 21:47:15 -04:00
purian23 4193cf51ff Refactor(DankBar): Restore previous autohide logic & add autoHideStrict setting 2026-05-10 21:14:09 -04:00
purian23 1ec0311086 feat(IPC): Add dbar toggleReveal logic for autohide modes 2026-05-09 23:49:25 -04:00
purian23 c6a1473d2f refactor(DankBar): Update dbar autohide mode reveal logic 2026-05-09 23:48:33 -04:00
purian23 ee16047e15 fix(Notepad): Blur handling on the header layer to remain in sync
- Fixes #2372
2026-05-09 21:59:08 -04:00
purian23 4968b80268 refactor(blur): Update blur layer sync handling w/Popout/Modals
- This results in better visuals so you will no longer get a brief flash of blur rending first upon load on certain displays
2026-05-09 21:21:16 -04:00
Josh SymondsandClaude Opus 4.7 e28b0c695e Theme: expose tertiary on the singleton (was undefined in dynamic mode) (#2375)
The dynamic-mode currentThemeData object at Theme.qml:447-468 omitted
tertiary entirely, even though matugen generates it and writes it to
the matugen colors JSON. As a result, Theme.tertiary returned undefined
in dynamic mode, and any QML binding through .r/.g/.b/.a defaulted to
white — visible as desaturated near-white highlights in the bar's
ChromeShader (which mixes Theme.tertiary at highlight peaks).

Two-line fix:
- Add tertiary to the dynamic-mode currentThemeData via getMatugenColor
- Add property color tertiary on the Theme singleton, falling back to
  secondary if tertiary is absent (covers the stock-theme path where
  tertiary is added separately via buildMatugenColorsFromTheme:1811)

Fallback hex #efb8c8 is the M3 baseline tertiary (light pink) for dark
mode, in case matugen output is missing.

Theme.tertiary was already referenced at LockScreenContent.qml:737 so
the property name is the right one.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-09 17:51:39 -04:00
purian23 7f6486b3e7 fix(Dock): Refactor & Fix Dock in FrameModes. Updates to Zone offset in Standalone & Connected modes
- Fixed hairline seam in some connected mode variants
- Moved dock-related properties to a new DockGeometry component for a future base
2026-05-09 17:37:42 -04:00
Josh SymondsandClaude Opus 4.7 faa30c4d48 bar: fix fullscreen-detection race during cross-monitor focus moves (#2373)
hasFullscreenToplevelOnScreen's fast-path used `NiriService.currentOutput
=== screenName` as a proxy for "the active toplevel is on screenName".
Those are two independent state channels (niri's workspace-activated
events drive currentOutput; wlr-foreign-toplevel-management drives
ToplevelManager.activeToplevel) and they don't update atomically.

When focus crosses from a fullscreen toplevel on monitor A to a
non-fullscreen toplevel on monitor B, currentOutput flips to B before
activeToplevel updates away from A's still-fullscreen-and-activated
window. For one tick, B's bar evaluates `active.fullscreen &&
active.activated && currentOutput === B` → true, hides itself, then
flips back when activeToplevel finally updates. Visible as a one-frame
bar flicker on focus changes between monitors when one has a fullscreen
window.

Replace the proxy with _toplevelOnScreen(active, screenName), the same
helper the X11 fallback path uses 25 lines below. The check now
inspects the toplevel's actual outputs instead of trusting a separate
state signal, so the race can't fire.

The per-workspace loop below was already correct; it would catch any
real fullscreen+activated toplevel on the bar's workspace regardless
of focused output. The fast-path was redundant when the assertion
held and wrong when it didn't.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-09 14:35:35 -04:00
purian23 cf641b4e08 fix(DockContextMenu): Restore jump windows 2026-05-09 01:54:23 -04:00
purian23 b75453c7d6 fix(IPC): Remove recent duplicate IPC bindings 2026-05-08 23:13:40 -04:00
bbedward 10872b5fc8 system tray: fix parent screen binding 2026-05-08 23:00:12 -04:00
purian23 80c853f16c fix(Settings): Restore Darken Modal Background ops in Standalone mode 2026-05-08 22:58:12 -04:00
purian23 6167f22837 feat(SystemUpdate): Implement system update IPC commands 2026-05-08 21:00:23 -04:00
bbedward d8835f2bc6 launcher: fix files tab actions
fixes #2357
2026-05-08 09:20:14 -04:00
purian23 1c01774fde fix(ConnectedMode): Update modal state management w/ownership Id 2026-05-07 23:03:31 -04:00
purian23 0d3eb774e2 (SystemUpdate): Ensure Polkit Prompts Top Level Auth layer 2026-05-07 16:17:12 -04:00
purian23 7fb4b6e0d9 refactor(SysUpdate): Flatpak & Cli command handling 2026-05-07 16:16:10 -04:00
bbedward 5df2b5fc33 launcher: also allow context switching with alt+1/2/3/4 2026-05-06 09:21:02 -04:00
purian23 d49c49cd99 refactor(sysupdate): Streamline DMS Updater command handling 2026-05-05 15:16:36 -04:00
purian23 b209827f38 distro:(Workflow) Save the resources 2026-05-04 23:10:12 -04:00
purian23 1b9d1c667c fix(Ubuntu): Workflow dput & SFTP 2026-05-04 22:25:45 -04:00
purian23 04d961ad0b distro(Ubuntu): Update Workflow & Dual Series Logic 2026-05-04 21:24:34 -04:00
Connor Welsh e60caf8028 feat(bar/dock): add 'Hide When Fullscreen' opt-out toggles (#2349)
Adds per-bar 'fullscreenDetection' and global 'dockHideOnFullscreen'
settings to gate CompositorService.hasFullscreenToplevelOnScreen()
2026-05-04 19:05:40 -04:00
dms-ci[bot] 1e67927f8a nix: update vendorHash for go.mod changes 2026-05-04 17:44:46 +00:00
bbedward e6e343dacb i18n: term sync 2026-05-04 13:42:27 -04:00
bbedward f87ad3d2ca core: update dependencies 2026-05-04 13:41:32 -04:00
bbedward a6ba4b1c79 dock: fix startup warning 2026-05-04 13:40:45 -04:00
dms-ci[bot] 7cf718cd50 nix: update vendorHash for go.mod changes 2026-05-04 17:39:40 +00:00
Giorgio De Traneandbbedward d223a74740 feat(tailscale): add Tailscale control center widget (#1875)
* feat(tailscale): add Tailscale control center widget

Full-stack Tailscale integration for DMS control center:

Backend (Go):
- Event-driven manager via WatchIPNBus (no polling)
- Reconnects with exponential backoff when tailscaled unavailable
- Typed conversion from ipnstate.Status to QML-friendly IPC types
- Testable via tailscaleClient interface with mock watcher
- Manager cleanup in cleanupManagers()
- 19 unit tests

Frontend (QML):
- TailscaleService with WebSocket subscription
- TailscaleWidget with peer list, filter chips, search
- Copy-to-clipboard for IPs and DNS names
- Daemon lifecycle handling (offline/stopped states)

Dependencies:
- Add tailscale.com v1.96.1 (official local API client)
- Bump Go to 1.26.1 (required by tailscale.com)

* cleanups

---------

Co-authored-by: bbedward <[email protected]>
2026-05-04 13:37:25 -04:00
purian23andCopilot 408beb202c refactor(sysupdate): improve cmd handling, formatted colors & progress indication
Co-authored-by: Copilot <[email protected]>
2026-05-04 13:13:44 -04:00
cfe6e6867e feat(display): Fix and implement display auto-switching with JSON profile storage (#2275)
* feat(display): fix monitor auto config and add output disable guard

* feat(display): fixed some race conditions and sole display getting disabled.

Co-authored-by: Copilot <[email protected]>

* feat(display): changes console log to use new log service

* feat(display): fix trailing spaces

* prek run

* add migration, fix missing hyprland HDR parameters, use FileView >
python

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: bbedward <[email protected]>
2026-05-04 13:07:24 -04:00
bbedward 7c991bc4e3 bluetooth: fix codec parsing names 2026-05-04 12:09:49 -04:00
bbedward 50f0cbb122 osd: fix inconsistent transparency
fixes #2330
2026-05-04 11:46:52 -04:00
Satvik Sharma 7ee0879103 Reverse session dir order so user configs override system configs as expected (#2341) 2026-05-04 11:19:01 -04:00
bbedward 56ef186ce8 notifications: add some de-duplication mechanism
related to #2334
2026-05-04 11:18:17 -04:00
purian23 5b507136e3 refactor(sysUpdate): Include the refresh flag in Fedora upgrade cmd 2026-05-04 11:06:26 -04:00
purian23 19c561da14 refactor: (Framemode) Added DeferredAction for dbar/dock state handling 2026-05-04 10:43:07 -04:00
bbedward cc47703d48 modals: fix sub-modals and power menu keyboard focus in connected mode
fixes #2346
2026-05-04 10:39:16 -04:00
purian23 31e60a3df5 fix(FrameMode): Update logic to hide the dbar/dock upon maximized windows/apps/games 2026-05-03 22:03:02 -04:00
purian23 082de6f1f0 refactor(FrameMode): Update modal background opts w/Connected Mode 2026-05-03 16:35:11 -04:00
purian23 fd24b4a36d feat(DMS FrameMode): A New Connected Unified Surface & Animation Overhaul
- Introduces Standalone & Connected Modes
- Updated Animations & Motion effects for both modes
- Numerous QOL tweaks and updates throughout the system
- Highly inspired to the OG Caelestia Shell / @Soramanew
2026-05-03 15:38:30 -04:00
ksp-apoc dd668469d7 fix: clean up orphaned idle inhibitor processes on startup (#2324)
When quickshell crashes or is force-killed, the child systemd-inhibit
process (used as fallback when native IdleInhibitor is unavailable) gets
reparented to PID 1 and continues to block idle indefinitely. This causes
hypridle to ignore all idle timeout rules even though the idle inhibitor
widget appears inactive after restart.

Add a cleanup step during initialization that kills any orphaned
systemd-inhibit processes from previous sessions.
2026-05-03 12:45:04 -04:00
bbedward 434490e100 audio: use typed PwNode list to assign audio devices
related to #2279
2026-05-03 10:04:31 -04:00
bbedward d2f6cb3ae4 process list: fix unloading
fixes #2284
2026-05-03 09:52:50 -04:00
bbedward c1cbd0994f settings: fix semvar signal moved to different service 2026-05-01 18:38:48 -04:00
bbedward c81645bacb add pre-commit hook for console.log 2026-04-30 16:59:26 -04:00
Archit Arora cdc4ca7e1f matugen: generate theme for Vencord (#2320) 2026-04-30 16:16:55 -04:00
gibbert 7d92842ff2 matugen: fix emacs template constant line number size (#2317)
Made it so line numbers don't stay a constant size when changing buffer
text scale.

See this thread:
<https://emacs.stackexchange.com/questions/74507/constant-font-size-in-display-line-numbers-mode-when-zooming-in-and-out>
2026-04-30 11:47:24 -04:00
Body d8bf3bdfe8 processes: fix list gaps and overlap when searching (#2315) 2026-04-30 11:45:46 -04:00
David Mirelesandlouzt 23ed795e85 Fix VPN UI for active transient entries (#2312)
Co-authored-by: louzt <[email protected]>
2026-04-30 11:41:41 -04:00
bbedward 2877c63c97 system update: make refresh synchronous 2026-04-30 11:41:07 -04:00
bbedward 86096db26b system update: general fixes to flatpak parsing 2026-04-29 16:14:19 -04:00
bbedward f76724f7cd logger: add a dedicated QML logging Singleton
- adds log.info/error/debug/warn/fatal
- adds ability to write logs to any file
- add CLI options in addition to env to set log levels
2026-04-29 15:42:30 -04:00
bbedward 3b96c6ab22 Revert "system updater: make all distros use terminal"
This reverts commit 1467f5dba9.
2026-04-29 14:56:54 -04:00
bbedward 1467f5dba9 system updater: make all distros use terminal 2026-04-29 14:41:24 -04:00
dms-ci[bot] baaa30c94e nix: update vendorHash for go.mod changes 2026-04-29 16:42:28 +00:00
bbedward 24a3cd5a3d core: update go dependencies 2026-04-29 12:40:24 -04:00
bbedward 65151dbfd7 i18n: term sync 2026-04-29 12:39:32 -04:00
bbedward 7bd9574868 system updater: complete overhaul
Move system update flow to GO, with a CLI (convenient AIO tool) and
server integration. All lifecycle, scheduling, execution occurs on
backend side.

Run some backends via pkexec, some via terminal like paru/yay.

Incorporate flatpak as an option to update.

Add terminal override setting in GUI, in addition to $TERMINAL env
variable.

fixes #2307
fixes #822
fixes #1102
fixes #1812
fixes #1087
fixes #1743
2026-04-29 12:33:57 -04:00
purian23 a4cfdf4a59 (dms): Add input group to dms setup
- Suppress fix/warnings
2026-04-28 14:03:37 -04:00
bbedward fd651dc943 niri overlay: fix state binding
fixes #2301
2026-04-28 13:19:34 -04:00
Kangheng Liu 919b09fc96 feat(desktop): expose screen var to desktop plugins (#2300) 2026-04-28 11:45:34 -04:00
bbedward aeb3fdd637 osd(media): workaround for firefox reporting youtube thumbnails as
players
fixes #2298
2026-04-28 11:27:16 -04:00
Amaan QureshiandLucas dc5636bed5 flake: let module callers supply pkgs so overlays reach the build (#2244)
The nixosModule/homeModule path previously called `buildDmsPkgs pkgs` but
internally referenced `self.packages.${system}.default`, which was
instantiated via `nixpkgs.legacyPackages`, an unoverlayed pkgs. That
meant downstream flakes couldn't reach through their own overlays to
the dms-shell build (e.g. to swap `kdePackages.sonnet` or trim perl
out of the aspell closure).

Extract the derivation as `mkDmsShell = pkgs: ...` at the top-level
`let`, and call it from both `packages.${system}.dms-shell` (for
direct consumers of the flake) and `buildDmsPkgs pkgs` (for module
consumers, which now pass in the system's overlayed pkgs).

Also re-checks overrideAttrs / .override still work: `mkDmsShell pkgs`
is the same `pkgs.lib.makeOverridable` wrapper as before, just
parameterized on the caller's pkgs instance.

Co-authored-by: Lucas <[email protected]>
2026-04-27 16:14:25 -03:00
bbedward 36a7692da7 dock: add trash CLI, refine implementation 2026-04-27 11:14:57 -04:00
Kangheng Liu c9b38023d5 feat(desktop): expose accept keyboard focus to desktop widgets (#2285)
Opt in by setting acceptsKeyboardFocus: true
2026-04-27 10:23:49 -04:00
Kangheng Liu 536e654b5e dock: add trash bin button (#2277)
* dock: add trash bin button

- icon reflects content- filled/empty
- multiple file manager support with nautilus as default, builtin as
fallback
- settingsspec at dock tab
- context menu

* fix: remove support for builtin filebrowser

needs specific adaptors at FB adhering the trash freedesktop spec

* fix: suppress auto-hide dock with trash context menu open

* feat: allow for custom file manager command

* feat: switch runner to proc.runcommand with toasts on command failures
2026-04-27 09:55:00 -04:00
Nic Ficca e805f6b5ac Fix: close notification center after clicking action buttons (#2276)
* Close notification center after clicking action buttons

When clicking action buttons (e.g., "View", "Activate") in the
notification center, the action fires but the popout stays open. Since
the center is a layer-shell surface, it blocks focus changes on Wayland
compositors like niri, making the action appear to do nothing.

The keyboard navigation path already closes the center after invoking
actions; this brings the mouse click path in line.

Also fix closeNotificationCenter() in PopoutService to set
notificationHistoryVisible = false (matching PopoutManager._closePopout)
instead of calling close() directly, which left the visibility property
stale and caused the bell toggle to require two presses to reopen.

Fixes #2178

* Sync notificationHistoryVisible with shouldBeVisible

NotificationCenterPopout has its own notificationHistoryVisible property
that drives open/close, but the PopoutService public API (open, close,
toggle) calls DankPopout methods directly, bypassing that property. This
leaves notificationHistoryVisible stale, causing the bell toggle to
require two presses to reopen after a programmatic close.

Sync the property from onShouldBeVisibleChanged so any caller going
through open()/close() gets the state corrected automatically.
2026-04-27 09:48:36 -04:00
Lucas 94f4b6d4a9 nix: add VM tests for flake modules (#2281)
* nix: add VM tests for flake modules

* ci: add NixOS tests
2026-04-27 09:37:28 -04:00
purian23 28f68ac702 refactor: update PPA upload script to handle series selection 2026-04-25 19:56:58 -04:00
purian23 441ec42ee0 (ubuntu): Update Workflow handling 2026-04-25 19:33:35 -04:00
Kangheng Liu 5415444e15 keybinds: add move workspace to monitor keybinds (#2268)
and distinguish with move columns
2026-04-25 12:07:18 -04:00
Archit Arora bd5276b40d feat(system-tray): add icon tinting (#2266) 2026-04-25 12:06:56 -04:00
Kangheng Liu dd3f17f51e clipboard: add keybind to switch tabs and toggle pinned (#2262)
* clipboard: add keybind to switch tabs

* clipboard: add bind to toggle pinned
2026-04-25 12:06:33 -04:00
purian23 a459b7d1b4 (dbar): Settings reorg 2026-04-25 00:40:33 -04:00
purian23 0f71c29776 dms(blur): Dank all the things 2026-04-24 22:52:14 -04:00
Lucas 4a32739d3f nix: update quickshell version (#2263)
Updated the quickshell revision to 783c95, matching the "stable" package in other DMS distributions.
2026-04-24 17:12:44 -04:00
bbedward 1abb221024 blur: revise general blur styling and refine it 2026-04-24 12:07:23 -04:00
Walid Salah b2668a2ffc Fix focused app when switching to empty workspace (#2259)
* Fix multiple screens on niri, when switching to an empty wokspace the other screen focused app widget would get confused

* Blank workspace fix
2026-04-24 10:48:24 -04:00
bbedward f4c11bc2ff clipboard: decode metadata only 2026-04-23 09:28:26 -04:00
bbedward 97fa86d8f0 loginctl: simplify event handling 2026-04-22 10:32:05 -04:00
Kristijan Ribarić b87c36d29e fix(quickshell): restore night mode and OSD surfaces after resume (#2254) 2026-04-22 10:08:50 -04:00
bbedward c6ed64b24e launcher: add elide helpers for RichText 2026-04-21 15:18:41 -04:00
bbedward cf382c0322 launcher: add indicators for flatpak/snap/appimage/nix
fixes #2251
2026-04-21 14:03:47 -04:00
bbedward 9139fd2fb1 doctor: add Miracle WM to checks 2026-04-20 09:27:59 -04:00
bbedward da3df9bb77 systray: fix missing import 2026-04-20 09:24:13 -04:00
Jos Dehaes e7834c981a Labwc service (#2248)
* services: add LabwcService with quit

labwc has a minimal IPC surface (no socket, no queries) but it does
expose `labwc --exit` as a clean shutdown path. Wrap that in a small
Singleton service following the same shape as DwlService/NiriService
so the compositor-specific dispatch in callers can stay uniform.

* session: dispatch labwc logout via LabwcService

CompositorService.isLabwc was detected but never dispatched in
_logout(); labwc sessions therefore fell through to the Hyprland
exit call, which silently no-ops under labwc. Users had to set
customPowerActionLogout to 'labwc --exit' as a workaround.

Add a labwc branch alongside the existing niri/dwl/sway branches
so the power menu logout works out of the box.
2026-04-20 09:22:20 -04:00
supposede 316428b14a Update color variables in zen-userchrome.css because it got broken again (#2246) 2026-04-20 09:16:04 -04:00
Walid Salah 6a9de8b423 Fix: Expand tilde from config paths (#2242)
* Expand tilde to the home directory for paths from config

* Remove extra line
2026-04-20 09:15:29 -04:00
Roni Laukkarinen f1e3452307 feat(system-tray): add optional monochrome icons setting (#2241)
Adds a 'Monochrome Icons' toggle to the system tray widget context menu.
When enabled, all system tray icons are desaturated using MultiEffect,
giving a cleaner monochrome bar aesthetic that matches minimal themes.

The setting is per-user (settings.json), defaults to false to preserve
existing behavior.
2026-04-20 09:15:02 -04:00
SunnyandLuckShiba 4c2c193766 added non-flake nix compatibility with flake-compat (#2009)
* added non-flake nix compatibility with flake-compat

* nix: move flake-compat files to distro/nix

---------

Co-authored-by: LuckShiba <[email protected]>
2026-04-17 22:42:19 -03:00
Lucas 112f2165f3 doctor: add blur support (#2236) 2026-04-17 18:57:13 -03:00
Lucas 40e3a22b99 nix: update flake.lock (#2237) 2026-04-17 18:57:02 -03:00
bbedward 7ced91ede1 notifications: add configurable durations for do not disturb
fixes #1481
2026-04-16 16:51:05 -04:00
bbedward c6e8067a22 core: add privesc package for privilege escalation
- Adds support for run0 and doas
fixes #998
2026-04-16 13:02:46 -04:00
bbedward d7fb75f7f9 keybinds(niri): add preprocessors to KDL parsing
fixes #2230
2026-04-16 10:36:55 -04:00
bbedward cf0fa7da6b fix(ddc): prevent negative WaitGroup counter on rapid brightness changes 2026-04-16 10:25:08 -04:00
purian23 787d213722 feat(Notepad): Add Expand/Collapse IPC handlers 2026-04-15 18:24:20 -04:00
purian23 2138fbf8b7 feat:(Notepad): Add blur & update animation track 2026-04-15 18:23:38 -04:00
bbedward 722b3fd1e8 audio: defensive checks on PwNode objects 2026-04-15 14:16:45 -04:00
dev 2728296cbd README.md - Update AUR badge to Arch (#2228)
The AUR dms-shell-bin package is replaced by dms-shell in the Arch Extra package repository. The AUR package has been removed.
2026-04-15 13:26:23 -04:00
Dimariqe fe1fd92953 fix: gate startup tray scan on prior suspend history (#2225)
The unconditional startup scan introduced duplicate tray icons on normal boot because apps were still registering their own SNI items when the scan ran.

Use CLOCK_BOOTTIME − CLOCK_MONOTONIC to detect whether the system has ever been suspended. The startup scan now only runs when the difference exceeds 5 s, meaning at least one suspend/resume cycle has occurred.
On a fresh boot the difference is ≈ 0 and the scan is skipped entirely.
2026-04-15 08:52:06 -04:00
bbedward 0ab9b1e4e9 idle/lock: add option to turn off monitors after lock explicitly
fixes #452
fixes #2156
2026-04-14 16:28:52 -04:00
bbedward 6d0953de68 i18n: sync terms 2026-04-14 11:51:39 -04:00
bbedward bc6bbdbe9d launcher: add ability to search files/folders in all tab
fixes #2032
2026-04-14 11:49:35 -04:00
DavutHaxorandbbedward eff728fdf5 Fix ddc brightness not applying because process exits before debounce timer runs (#2217)
* Fix ddc brightness not applying because process exits before debounce timer runs

* Added sync.WaitGroup to DDCBackend and use it instead of loop in wait logic, added timeout in case i2c hangs.

* go fmt

---------

Co-authored-by: bbedward <[email protected]>
2026-04-14 10:27:36 -04:00
bbedward 8d415e9568 settings: re-work auth detection bindings 2026-04-13 09:46:17 -04:00
bbedward e6ed6a1cc2 network: report negotiated link rate when connected
fixes #2214
2026-04-13 09:11:42 -04:00
bbedward ca18174da5 gamma: more comprehensive IPCs 2026-04-13 09:06:23 -04:00
Particle_G 976b231b93 Add headless mode support with command-line flags (#2182)
* Add support for headless mode. Allow dankinstall run with command-line flags.

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146219

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146253

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146271

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146296

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146348

* FIx https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056146328

* Update headless mode instructions

* Add log dir config. Use DANKINSTALL_LOG env var, fallback to /var/tmp

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056737552

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056737572

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3056737592

* Add explanations for headless validating rules and log file location

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058087146 and https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058087234

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058087271

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058310408

* Enhance configuration deployment logic to support missing files and add corresponding unit tests

* Fix https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058310495

* Reworked the log channel handling logic to simplify the code and added the `drainLogChan` function to prevent blocking (https://github.com/AvengeMedia/DankMaterialShell/pull/2182#discussion_r3058609491)

* Added dependency-checking functionality to ensure installation requirements are met, and optimized the pre-installation logic for AUR packages

* feat: output log messages to stdout during installation

* Revert dependency-checking functionality due to official fix

* Revert compositor provider workaround due to upstream fix
2026-04-13 09:03:12 -04:00
bbedward 3d75a51378 gamma: add ipc call night getTemperature and enrich status function
fixes #1778
2026-04-12 21:46:41 -04:00
bbedward dc4b1529e6 gamma: reset lastAppliedTemp on suspend instead of destroying/recreating
outputs

fixes #2199 , possibly regresses #1235 - but I think the original bug
was lastAppliedTemp being incorrectly set, this allows compositors to
cache last applied gamma

gamma: add a bunch of defensive mechanisms for output changes
related to #2197

gamma: ensure gamma is re-evaluate on resume
fixes #1036
2026-04-12 21:40:39 -04:00
bbedward f61438e11f doctor: fix quickshell regex
fixes #2204
2026-04-11 12:44:58 -04:00
bbedward 8f78163941 dankinstall: workarounds for arch/extra change 2026-04-11 12:24:08 -04:00
mihem f894d338fc feat(running-apps): stronger active app highlight + indicator bar (#2190)
The focused app background used 20% primary opacity which was barely
visible. Increase to 45% to make the active window unambiguous at a glance.
2026-04-11 12:01:21 -04:00
bbedward f2df53afcd colorpicker: re-use Wayland buffer pools 2026-04-09 12:08:43 -04:00
Thomas KrollandClaude Opus 4.6 4179fcee83 fix(privacy): detect screen casting on Niri via PipeWire (#2185)
Screen sharing was not detected by PrivacyService on Niri because:

1. Niri creates the screencast as a Stream/Output/Video node, but
   screensharingActive only checked PwNodeType.VideoSource nodes.

2. looksLikeScreencast() only inspected application.name and
   node.name, missing Niri's node which has an empty application.name
   but identifies itself via media.name (niri-screen-cast-src).

Add Stream/Output/Video to the checked media classes and include
media.name in the screencast heuristic. Also add a forward-compatible
check for NiriService.hasActiveCast for when Niri gains cast tracking
in its IPC.

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-09 11:50:39 -04:00
Andrey Yugai a0c9af1ee7 feature: persist last active player (#2184) 2026-04-09 11:30:04 -04:00
Thomas KrollandClaude Opus 4.6 049266271a fix(system-update): popout first-click and AUR package listing (#2183)
* fix(system-update): open popout on first click

The SystemUpdate widget required two clicks to open its popout.

On the first click, the LazyLoader was activated but popoutTarget
(bound to the loader's item) was still null in the MouseArea handler,
so setTriggerPosition was never called. The popout's open() then
returned early because screen was unset.

Restructure the onClicked handler to call setTriggerPosition directly
on the loaded item (matching the pattern used by Clock, Clipboard, and
other bar widgets) and use PopoutManager.requestPopout() instead of
toggle() for consistent popout management.

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

* fix(system-update): include AUR packages in update list

When paru or yay is the package manager, the update list only showed
official repo packages (via checkupdates or -Qu) while the upgrade
command (paru/yay -Syu) also processes AUR packages. This mismatch
meant AUR updates appeared as a surprise during the upgrade.

Combine the repo update listing with the AUR helper's -Qua flag so
both official and AUR packages are shown in the popout before the
user triggers the upgrade. The output format is identical for both
sources, so the existing parser works unchanged.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-09 10:49:15 -04:00
Ron Harel 0eabda3164 Improve seek and scrub indicator/ animations in the media controls widget. (#2181) 2026-04-09 10:35:56 -04:00
bbedward 32c063aab8 Revert "qml: cut down on inline components for performance"
This reverts commit f6e590a518.
2026-04-07 15:22:46 -04:00
Ron Harel 37f92677cf Make the system tray overflow popup optional and have the widget expand inline as an alternative. (#2171) 2026-04-07 11:13:32 -04:00
Ron Harel 13e8130858 Add adaptive media width setting. (#2165) 2026-04-07 11:07:36 -04:00
bbedward f6e590a518 qml: cut down on inline components for performance 2026-04-07 10:57:11 -04:00
bbedward 3194fc3fbe core: allow RO commands to run as root 2026-04-06 18:19:17 -04:00
bbedward 3318864ece clipboard: make CLI keep CL item in-memory again 2026-04-06 16:09:10 -04:00
IrisandIris e224417593 feature: add login sound functionality and settings entry (#2155)
* added login sound functionality and settings entry

* Removed debug warning that was accidentally left in

* loginSound is off by default, and fixed toggle not working

* Prevent login sound from playing in the same session

---------

Co-authored-by: Iris <[email protected]>
2026-04-06 14:11:00 -04:00
Marcus Ramberg 3f7f6c5d2c core(doctor): show all detected terminals (#2163) 2026-04-06 14:09:15 -04:00
bbedward 0b88055742 clipboard: fix reliability of modal/popout 2026-04-06 10:30:39 -04:00
bbedward 2b0826e397 core: migrate to dms-shell arch package 2026-04-06 10:09:55 -04:00
bbedward 7db04c9660 i18n: use comments instead of context, sync 2026-04-06 09:41:45 -04:00
Al- Amin 14d1e1d985 fix: Add match rule for new version of Gnome Calculator app ID (#2157) 2026-04-06 09:11:26 -04:00
Dimariqe 903ab1e61d fix: add TrayRecoveryService with bidirectional SNI dedup (Go server) (#2137)
* fix: add TrayRecoveryService with bidirectional SNI dedup (Go server)

Add TrayRecoveryService manager that re-registers lost tray icons after
resume from suspend via native DBus scanning in the Go server.

The service resolves every registered SNI item (both well-known names and
:1.xxx connection IDs) to a canonical connection ID, building a unified
registeredConnIDs set before either scan section runs. This prevents
duplicates in both directions:

- If an app registered via well-known name, the connection-ID section
  skips its :1.xxx entry.
- If an app registered via connection ID, the well-known-name section
  skips its well-known name (checked through registeredConnIDs).
- After successfully registering via well-known name, registeredConnIDs
  is updated immediately so the connection-ID section won't probe the
  same app in the same run.

A startup scan (3 s delay) covers the common case where the DMS process
is killed during suspend and restarted by systemd (Type=dbus), so the
loginctl PrepareForSleep watcher alone is not sufficient. The startup
scan is harmless on a normal boot — it finds all items already registered
and exits early.

Go port of quickshell commit 1470aa3.

* fix: 'interface{}' can be replaced by 'any'

* TrayRecoveryService: Remove objPath parameter from registerSNI
2026-04-06 09:03:36 -04:00
Walid Salah 5982655539 Make focused app widget only show focused app on the current screen (#2152) 2026-04-05 10:53:18 -04:00
Aaron Tulino 1021a210cf Change power profile by scrolling battery (#2142)
Scrolling up "increases" the profile (Power Saver -> Balanced -> Performance). Supports touchpad.
2026-04-03 12:04:00 -04:00
bbedward e34edb15bb i18n: sync 2026-04-03 11:56:00 -04:00
dev 61ee5f4336 Update CpuMonitor.qml to reserve enough widget space for the widest number "100" instead of "88" (#2135)
My bar kept shifting around by a few pixels every time I hit 100% cpu usage
2026-04-02 15:26:56 -04:00
Phil JacksonandClaude Sonnet 4.6 ce2a92ec27 feat: rewind to track start on previous when past 8 seconds (#2136)
* feat: rewind to track start on previous when past 8 seconds

Adds MprisController.previousOrRewind() which rewinds the current track
to position 0 if more than 8 seconds in (with canSeek check), and falls
back to previous() otherwise — matching traditional media player behaviour.

All previous() call sites across Media.qml, MediaPlayerTab.qml,
MediaOverviewCard.qml, LockScreenContent.qml and DMSShellIPC.qml
are updated to use the new shared function.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: poll position in MprisController for previousOrRewind accuracy

Without a polling timer, activePlayer.position is never updated in
contexts that don't display a seekbar (e.g. the DankBar widget), causing
the position > 8 check in previousOrRewind() to always see 0 and fall
through to previous() instead of rewinding.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-04-02 15:26:36 -04:00
Al- Amin 66ce79b9bf fix:update resizeactive binding to include height to make it work (#2126) 2026-04-01 09:19:01 -04:00
Al- Amin 30dd640314 fix:add window rule for the new version of Gnome Calculator (#2125) 2026-04-01 09:18:56 -04:00
bbedward 28f9aabcd9 screenshot: fix scaling of global coordinate space when using all
screens
2026-03-31 15:13:10 -04:00
bbedward 3d9bd73336 launcher: some polishes for blur 2026-03-31 11:04:18 -04:00
bbedward 3497d5f523 blur: stylize control center for blur mode 2026-03-31 09:42:08 -04:00
bbedward 8ef1d95e65 popout: fix inconsistent transparency 2026-03-31 09:06:48 -04:00
bbedward e9aeb9ac60 blur: add probe to check compositor for ext-bg-effect 2026-03-30 15:18:44 -04:00
bbedward fb02f7294d workspace: fix mouse area to edges
fixes #2108
2026-03-30 15:16:17 -04:00
bbedward f15d49d80a blur: add blur support with ext-bg-effect 2026-03-30 11:52:35 -04:00
Sheershak sharma c471cff456 False error Fix (#2109)
* fix: use UnsetWorkspaceName for empty input in workspace rename
Previously, empty input would set workspace name to empty string,
causing issues with Niri's unique workspace name requirement.
Now uses UnsetWorkspaceName action when input is empty.

* prevent false failed to load config toast on niri validation
Move error toast logic from StdioCollector.onStreamFinished to Process.onExited
so it only displays when niri validate actually fails (non-zero exit code),
not when stderr outputs early progress messages during config processing.
2026-03-30 09:21:59 -04:00
Graeme Foster f83bb10e0c fix(osd): coerce optional chain to bool in VolumeOSD enabled bindings (#2101)
Fixes #2100
2026-03-30 09:16:58 -04:00
Triệu Kha 74ad58b1e1 feat(color-picker): add --raw flag (#2103)
* Add --raw flag

* Fix typo

* cleanup duped code
2026-03-30 09:12:48 -04:00
Triệu Kha 577863b969 feat(danklauncher): add launcher history (#2086)
* remember last search query

* add launcherLastQuery SessionSpec

* add rememberLastQuery option

* Add remember last query for appdrawer

* Add query history sessiondata

* Complete and cleanup

* Discard changes to quickshell/Modules/Settings/LauncherTab.qml

* Cleanup logic

* Add rememberLastQuery option

* Add rememberLastQuery option description

* Move setLauncherLastQuery above validation

* Fix logic bug with empty query
2026-03-30 09:09:39 -04:00
Tulip Blossom 03d2a3fd39 chore(niri): use satty screenshot tool as default (#2105)
Swappy seems to not be quite as well maintained as satty, latest commit (https://github.com/jtheoof/swappy/commit/ff7d641b8c0d461b8a90448a5893e4aa3a0533b1) is a translation, before that the latest commit was in august 2025. Latest release is also from Aug 27 2025.

Satty however has had commits quite recently and releases as well.
- https://github.com/Satty-org/Satty/commit/590253c8bba01f5c559808dd432cbb80bc4bd7c8 - This was from yesterday
- https://github.com/Satty-org/Satty/releases/tag/v0.20.1 - Febuary 6th

Signed-off-by: Tulip Blossom <[email protected]>
2026-03-29 21:32:15 -04:00
purian23 802b23ed60 auth: Add Nix store fallback detection to PAM configs 2026-03-29 18:37:08 -04:00
Sheershak sharma 2b9f3a9eef fix: use UnsetWorkspaceName for empty input in workspace rename (#2094)
Previously, empty input would set workspace name to empty string,
causing issues with Niri's unique workspace name requirement.
Now uses UnsetWorkspaceName action when input is empty.
2026-03-29 12:28:21 -04:00
bbedward 62c60900eb fix(clipboard): wait for forked child to register Wayland source before returning 2026-03-27 13:54:34 -04:00
Patrick Fischer b381e1e54c fix(nix): patch U2F PAM config with full Nix store path (#2071) 2026-03-27 13:39:12 -04:00
purian23 e7ee26ce74 feat(Auth): Unify shared PAM sync across greeter & lockscreen
- Add a neutral `dms auth sync` command and reuse the shared auth flow from:
- Settings auth toggle auto-apply
- `dms greeter sync`
- `dms greeter install`
- greeter auth cleanup paths

- Rework lockscreen PAM so DMS builds /etc/pam.d/dankshell from the system login stack, but removes fingerprint and U2F from that password path. Keep /etc/pam.d/dankshell-u2f separate.

- Preserve custom PAM files in place to avoid adding duplicate greeter auth when the distro already provides it, and keep NixOS on the non-writing path.
2026-03-27 12:52:31 -04:00
bbedward 521a3fa6e8 fix syncWallpaperForCurrentMode 2026-03-27 09:35:39 -04:00
Connor Welsh 5ee93a67fe fix(bar): exclude niri from fullscreen toplevel detection (#2091)
Niri already renders fullscreen windows above the top layer-shell layer
(see render_above_top_layer() in niri's scrolling.rs). Aside from
redundancy, this check also hides the bar for windowed-fullscreen
windows (aka fake fullscreen), since the Wayland protocol reports
identical state for both.
2026-03-27 08:57:18 -04:00
Kangheng Liu 5d0a03c822 fix: show bar when scrolling away from fullscreen window (#2089)
In niri there can be multiple fullscreen windows in a workspace
2026-03-27 08:56:35 -04:00
purian23 293c2a0035 refactor: Remove faillock support and related properties from settings 2026-03-25 22:19:09 -04:00
purian23 9a5fa50541 fix(pam): Update config selection logic for PAM context 2026-03-25 17:04:57 -04:00
purian23 d5ceea8a56 fix(lock): Restore system PAM fallback, faillock support, and auth feedback
- Re-add loginConfigWatcher so installs can still fall through to
  /etc/pam.d instead of the bundled PAM assets
- Add login-faillock bundled PAM asset at runtime. Use it as the bundled fallback when dankshell config is absent
- Fix invalid bare property writes (u2fPending, u2fState, unlockInProgress,
  state) in Pam.qml
- Improve lockscreen auth feedback
2026-03-25 16:39:37 -04:00
Jonas Bloch faa5e7e02d fix: set default value for matugenTemplateNeovimSetBackground (#2081) 2026-03-25 11:35:17 -04:00
Jonas Bloch 516c478f3d Neovim template enhancements (#2078)
* feat: add neovim-lualine template, set vim.o.background automatically based on dms light/dark mode

* feat(matugen): add option to follow dms background color or not on neovim

* chore: regenerate settings and translation index after merging master
2026-03-25 09:16:01 -04:00
Kangheng Liu 906c6a2501 feat: FileBrowser video thumbnail (#2077)
* feat(filebrowser): add filebrowser video thumbnails display

- Find cached thumbnails first
- If not found, generate with ffmpegthumbnailer
- Fallback to placeholder icon if dependency not met

* fix(filebrowser): create thumbnail cache dir if not exists

* refactor(filebrowser): prefer using Paths lib

* fix(filebrowser): only check filetype once for each file

* fix(filebrowser): early test for thumbnails

* feat: add xdgCache path
2026-03-25 09:14:59 -04:00
Viet Dinh 86d8fe4fa4 fix: pywalfox light theme template (#2075)
The current template doesn't work for an OOTB config of pywalfox
without manual configuration. This commit fixes the colors to work
better with its defaults.
2026-03-25 09:12:41 -04:00
Kangheng Liu 9b44bc3259 feat: add FullscreenToplevel detection (#2069)
* feat: add FullscreenToplevel detection

For animating bar for fullscreen events

* fix: respect overview reveal settings

perform the overview check first
2026-03-24 09:21:38 -04:00
bbedward 59b6d2237b i18n: add swedish and german 2026-03-23 12:04:37 -04:00
bbedward 7e559cc0bb cli/notify: append file:// prefix for --file arguments
fixes #1962
2026-03-23 11:57:17 -04:00
bbedward fd1facfce8 core: execute quickshell IPC with pid 2026-03-23 10:06:31 -04:00
bbedward 8f26193cc3 widgets: convert DankButtonGroup to Row instead of Flow 2026-03-23 09:50:56 -04:00
bbedward 43b2e5315d popout: avoid calling close on bad reference 2026-03-23 09:40:53 -04:00
bbedward 5cad89e9cc wallpaper: updatesEnabled set on screen changes 2026-03-23 09:26:49 -04:00
Jon Rogers 3804d2f00b fix(Scorer): honour _preScored for no-query when value exceeds typeBonus (#2065)
Plugin items can set _preScored to signal a priority boost (e.g. recently
used items). Previously _preScored was only respected when a search query
was active, so no-query default lists always fell back to typeBonus+frecency
scoring, making plugin-controlled ordering impossible.

Change the condition from:
  if (query && item._preScored !== undefined)
to:
  if (item._preScored !== undefined && (query || item._preScored > 900))

This respects _preScored in no-query mode only when the value exceeds 900
(the plugin typeBonus), which avoids changing behaviour for "all" mode items
whose _preScored is set to 900-j by the controller (≤ 900). Items without
_preScored set continue to use the existing typeBonus + frecency formula.
2026-03-23 09:25:20 -04:00
Jeff Corcoran 4d649468d5 fix(dropdown): sort fuzzy search results by score and fix empty results on reopen (#2051)
fzf.js relied on stable Array.sort to preserve score ordering, which is
not guaranteed in QML's JS engine. Results appeared in arbitrary order
with low-relevance matches above exact matches. The sort comparator now
explicitly sorts by score descending, with a length-based tiebreaker so
shorter matches rank first when scores are tied.

Also fixed Object.assign mutating the shared defaultOpts object, which
could cause options to leak between Finder instances.

DankDropdown's onOpened handler now reinitializes the search when previous
search text exists, fixing the empty results shown on reopen.

Added resetSearch() for consumers to clear search state externally.
2026-03-23 09:24:51 -04:00
Kangheng Liu c5f145be36 feat: animate dock apps on entry addition/deletion (#2064) 2026-03-23 09:21:21 -04:00
Kangheng Liu 76dff870a7 fix: fallback icon does not dim when not focused (#2063) 2026-03-23 09:19:18 -04:00
lpv 6c8d3fc007 feat(screenshot): add --no-confirm and --reset flags (#2059) 2026-03-23 09:18:56 -04:00
Patrick Fischerandpurian23 e7ffa23016 fix: restore lock screen U2F/fingerprint auth to working state (#2052)
* fix: restore lock screen U2F/fingerprint auth to working state

* fix(pam): Keep SettingsData as single source of truth for auth availability
- Restores SettingsData for fingerprint/U2F, keeping lock screen and New Greeter Settings UI in sync

---------

Co-authored-by: purian23 <[email protected]>
2026-03-22 19:23:10 -04:00
purian23 4266c064a9 refactor(Ubuntu): Update dual-series upload logic 2026-03-21 20:11:40 -04:00
purian23 5f631b36cd feat(Ubuntu): Initial Ubuntu 26.04 LTS Resolute Raccoon distro support 2026-03-21 17:56:08 -04:00
purian23 be8326f497 fix(Dock): Replace hardcoded max height mask in vertical mode 2026-03-21 13:28:17 -04:00
purian23 07dbba6c53 notifications(Settings): Update notifs popout settings overflow 2026-03-20 19:09:03 -04:00
Linken Quy Dinh a53b9afb44 fix: multi-monitor wallpaper cycling not working (#2042)
Fixed a QML property binding timing issue where dynamically created timers
and processes for per-monitor wallpaper cycling were being assigned to
properties and then immediately read back, which could return undefined
or stale values.

The fix stores the created object in a local variable before assigning
to the property map, ensuring a valid reference is always used.

Affected functions:
- startMonitorCycling() - timer creation
- cycleToNextWallpaper() - process creation
- cycleToPrevWallpaper() - process creation
2026-03-20 17:38:36 -04:00
purian23 a0c7ffd6b9 dankinstall(Arch): improve AUR package installation logic 2026-03-20 17:03:30 -04:00
bbedward 7ca1d2325a core: use QS_APP_ID instead of pragma 2026-03-20 12:38:42 -04:00
bbedward 8d0f256f74 fix: missing appID references 2026-03-20 10:04:55 -04:00
bbedward 1a9449da1b qs: set app ID to com.danklinux.dms 2026-03-20 10:03:33 -04:00
bbedward 1caf8942b7 popout: avoid calling functions on stale references 2026-03-20 09:35:58 -04:00
Dimariqe 9efbcbcd20 fix: redraw wallpaper after DMS lock screen is dismissed (#2037)
After unlocking the screen (startup lock or wake from sleep), the desktop
showed Hyprland's background color instead of the wallpaper.

WallpaperBackground disables QML updates via updatesEnabled after a 1-second
settle timer. While WlSessionLock is active, Hyprland does not composite the
background layer, so when the lock is released it needs a fresh Wayland buffer
— but none is committed because the render loop is already paused.

The previous attempt used SessionService.sessionUnlocked, which is unreliable
for the startup lock case: DMSService is not yet connected when lock() is
called at startup, so notifyLoginctl is a no-op and the loginctl state never
transitions, meaning sessionUnlocked never fires.

Fix by tracking the shell lock state directly from Lock.qml's shouldLock via
a new IdleService.isShellLocked property. WallpaperBackground watches this and
re-enables rendering for 1 second on unlock, ensuring a fresh buffer is
committed to Wayland before the compositor resumes displaying the layer.
2026-03-20 09:29:32 -04:00
Youseffo13 3d07b8c9c1 Fixed mux tab having same id as locale tab (#2031)
* Feat: fix mux tab having same id as locale tab

* Feat: updated some icon

* Update KeybindsModal.qml
2026-03-20 09:27:46 -04:00
bbedward dae74a40c0 wallpaper: tweak binding again for updatesEnabled 2026-03-20 09:22:42 -04:00
purian23 959190dcbc feat(Settings): Add sidebar state management on categories 2026-03-20 00:38:34 -04:00
bbedward 1e48976ae5 theme: add matugen contrast slider
fixes #2026
2026-03-19 14:44:14 -04:00
bbedward 0a8c111e12 wallpaper: fixes for updatesEnable handling 2026-03-19 14:19:36 -04:00
bbedward 19c786c0be launcher: add option to launch apps on dGPU by default
fixes #2027
2026-03-19 13:28:48 -04:00
bbedward 7f8b260560 workspaces: ignore X scroll events
fixes #2029
2026-03-19 13:21:53 -04:00
purian23 368536f698 fix(WallpaperBlur):Restore Blur on Overview mode on niri 2026-03-18 17:43:49 -04:00
bbedward b227221df6 i18n: sync terms 2026-03-18 09:31:02 -04:00
purian23 8e047f45f5 (notepad): Update tab loading with request ID validation 2026-03-18 00:10:35 -04:00
purian23 fbe8cbb23f DMS Shell: Remove dup import 2026-03-18 00:09:48 -04:00
purian23 28315a165f feat(Notepad): Implement tab reordering via drag-and-drop functionality 2026-03-18 00:09:19 -04:00
purian23 1b32829dac refactor(Notepad): Streamline hide behavior & auto-save function 2026-03-17 23:23:17 -04:00
purian23 1fce29324f dankinstall(debian): Minor update to ARM64 support 2026-03-17 21:23:01 -04:00
purian23 1fab90178a (greeter): Revise dir perms and add validations 2026-03-17 20:38:37 -04:00
bbedward eb04ab7dca launcher v2: simplify screen change bindings 2026-03-17 20:31:55 -04:00
bbedward e9fa2c78ee greeter: remove variable assignments 2026-03-17 15:28:29 -04:00
purian23 59dae954cd theme(greeter): fix auto theme accent variants & update selections 2026-03-17 13:27:57 -04:00
Youseffo13 5c4ce86da4 Wrapped all missing i18n strings (#2013)
* Feat(i18n): wrapped missing strings

* Feat(i18n): wrapped missing strings

* feat(i18n): added pluralization to some strings

* feat: updated en.json and template.json

* Update en.json

* Update template.json
2026-03-17 12:43:23 -04:00
Maddison Hellstrom 0cf2c40377 feat(color-picker): add openColor IPC handler to set color on open (#2017) 2026-03-17 12:42:59 -04:00
nick-linux8 679a59ad76 Fix(Greeter): Fixes #1992 Changed Greetd logic to include registryThemeVariants to pull in accent color (#2000) 2026-03-16 22:58:57 -04:00
purian23 db3209afbe refactor: Remove wireplumber directory from cache creation 2026-03-16 21:35:29 -04:00
bbedward f0be36062e dankbar: guard against nil screen names 2026-03-16 11:32:59 -04:00
bbedward 9578d6daf9 popout: fix focusing of password prompts when popout is open
undesired effect of closing the popout but its probably the best
solution
2026-03-16 11:30:31 -04:00
bbedward cc6766135d focused app: fallback to app name if no title in compact mode
fixes #2005
2026-03-16 11:25:50 -04:00
bbedward 28c9bb0925 cc: fix invalid number displays on percentages
fixes #2010
2026-03-16 11:18:42 -04:00
Ron Harel 7826d827dd feat: add configurable control center group ordering (#2006)
* Add grouped element reordering to control center setting popup.

Reorganize the control center widget menu into grouped rows and add drag handles for reordering.
Introduce controlCenterGroups to drive the grouped popup layout, along with dynamic content width calculation.
Disable dependent options when their parent icon is turned off, and refine DankToggle disabled colors to better distinguish checked and unchecked states.

* Apply Control Center group order to live widget rendering.

Apply persisted `controlCenterGroupOrder` to the actual control center button rendering path instead of only using it in the settings UI.
This refactors `ControlCenterButton.qml` to derive a normalized effective group order, build a small render model from that order, and use model-driven rendering for both vertical and horizontal layouts.

Highlights:
- add a default control center group order and normalize saved order data
- ignore unknown ids, deduplicate duplicates, and append missing known groups
- add shared group visibility helpers and derive a render model from them
- render both vertical and horizontal indicators from the effective order
- preserve existing icon, color, percent text, and visibility behavior
- keep the fallback settings icon outside persisted ordering
- reconnect cached interaction refs for audio, mic, and brightness to the real rendered group containers so wheel and right-click behavior still work
- clear and refresh interaction refs on orientation, visibility, and delegate lifecycle changes
- tighten horizontal composite group sizing by measuring actual rendered content, fixing extra spacing around the audio indicator

Also updates the settings widgets UI to persist and restore control center group ordering consistently with the live control center rendering.
2026-03-16 11:11:26 -04:00
Michael Erdely 7f392acc54 Implement ability to cycle through launcher modes (#2003)
Use Ctrl+Left/Right and Ctrl+H/L to move back and forward through the
modes of the launcher
2026-03-16 11:08:07 -04:00
Michael Erdely 190fd662ad Implement more intuitive keybinds for Launcher (#2002)
With programs like rofi, pressing the tab key advances to the next item
in the list. This change makes the Launcher behave in the same way,
moving the action cycling to Ctrl+Tab (and Ctrl+Shift+Tab for reverse.
2026-03-16 11:07:25 -04:00
Triệu Kha e18587c471 feat(calendar): add show week number option (#1990)
* increase DankDashPopout width to accommodate week number column

* add getWeekNumber function

* add week number column

* add showWeekNumber SettingsData

* add showWeekNumber SettingsSpec

* make dash popout width changes reponsively to showWeekNumber option

* complete and cleanup

* fix typo

* fix typo
2026-03-16 11:06:21 -04:00
Walid Salah ddb079b62d Add terminal multiplexer launcher (#1687)
* Add tmux

* Add mux modal

* Restore the settings config version

* Revert typo

* Use DankModal for InputModal

* Simplify terminal flags

* use showWithOptions for inputModals instead

* Fix translation

* use Quickshell.env("TERMINAL") to choose terminal

* Fix typo

* Hide muxModal after creating new session

* Add mux check, moved exclusion to service, And use ScriptModel

* Revert unrelated change

* Add blank line
2026-03-16 11:05:16 -04:00
purian23 e7c8d208e2 copr(fedora): Update Go Toolchain for compatibility 2026-03-15 23:26:45 -04:00
0e2162cf29 fix(nix/greeter): skip invalid customThemeFile in preStart (#1997)
* fix(nix/greeter): skip invalid customThemeFile in preStart

Avoid attempting to copy a null/empty/missing customThemeFile path by validating the jq result and file existence before cp.

Update distro/nix/greeter.nix

Co-authored-by: Lucas <[email protected]>

* nix/greeter: update customTheme verification

---------

Co-authored-by: Lucas <[email protected]>
Co-authored-by: LuckShiba <[email protected]>
2026-03-15 04:21:19 -03:00
NikSne 4cf9b0adc7 feat(nix/niri): add new includes for dms 1.4 (#1998) 2026-03-15 04:09:00 -03:00
purian23 1661d32641 (greeter): Trial fix for 30s auth delay & wireplumber state dir 2026-03-15 02:54:23 -04:00
Ron Harel aa59187403 Add Color Picker to DMS launcher. (#1999) 2026-03-14 23:31:21 -04:00
bbedward bb08e1233a matugen: bump default queue timeout to 90s 2026-03-13 15:40:55 -04:00
bbedward 5343e97ab2 core/server: initialize geolocation async on startup 2026-03-13 15:06:11 -04:00
purian23 edc544df7a dms(policy): Restore dms greeter sync in immutable distros 2026-03-13 14:27:15 -04:00
bbedward a880edd9fb core: restore core go version to 1.26.0 2026-03-13 13:40:11 -04:00
Jonas Bloch 7e1d808d70 New neovim theme engine (#1985)
* feat(matugen)!: rework completely neovim's theme engine

* fix: link to neovim theme plugin

* fix: expect AvengeMedia/base46 instead of Silzinc/base46
2026-03-13 13:37:16 -04:00
bbedward ce93f22669 chore: Makefile shouldnt build when installing 2026-03-13 13:36:44 -04:00
bbedward a58037b968 fix: missing import in Hyprland service 2026-03-13 13:25:20 -04:00
bbedward ccf0b60935 core: add toolchain directive to go.mod 2026-03-13 13:05:31 -04:00
bbedward aad7011b1c ci: fix hardcoded branch in vendor workflow 2026-03-13 12:22:27 -04:00
bbedward 3bde7ef4d3 nix: update flake 2026-03-13 12:13:58 -04:00
bbedward 04555dbfa7 nix: fix go regex matching 2026-03-13 12:03:42 -04:00
bbedward 3b494aa591 nix: dynamically resolve go version in flake 2026-03-13 11:58:08 -04:00
bbedward 365387c3cd ci: reveal errors in nix vendor hash update 2026-03-13 11:53:17 -04:00
Nek bb74a0ca4d fix(wallpaper): preserve per-monitor cycling when changing interval (#1981)
(#1816)
2026-03-13 11:46:02 -04:00
nick-linux8 9cf2ef84b7 Added Better Handling In Event Dispatcher Function (#1980) 2026-03-13 11:43:24 -04:00
bbedward 46aaf5ff77 fix(udev): avoid event loop termination
core: bump go to 1.26
2026-03-13 11:42:46 -04:00
Nek c544bda5df fix(matugen): detect Zed Linux binary aliases (#1982) 2026-03-13 11:29:51 -04:00
purian23 e86227f05f fix(greeter): add wireplumber state directory & update U2F env variables 2026-03-12 22:35:26 -04:00
bbedward 53da60e4ca settings: allow custom json to render all theme options 2026-03-12 17:58:42 -04:00
purian23 727d9c6c22 greeter(auth): Enhance fingerprint/U2F auth support w/Quickshell PAM
- Split auth capability state by lock screen and greeter
- Share detection between settings UI and lock runtime
- Broaden greeter PAM include detection across supported distros
2026-03-12 15:06:07 -04:00
purian23 908e1f600e dankinstall(distros): Enhance DMS minimal install logic
-Updated for Debian, Ubuntu, Fedora, and OpenSUSE
- New shared minimal installation logic to streamline package handling across distros
2026-03-12 14:55:02 -04:00
purian23 270d800df2 greeter(distros): Move comps to Suggests on Debian/OpenSUSE 2026-03-12 14:42:44 -04:00
bbedward d445d182ea fix(settings): fix animation speed binding in notifications tab
fixes #1974
2026-03-12 11:43:33 -04:00
Adarsh219 476256c9e7 fix(matugen): use single quotes for zed template paths (#1972) 2026-03-12 08:54:18 -04:00
Triệu Kha 06ea7373f7 parity(danktoggle): follow m3 disabled state color specs (#1973) 2026-03-12 08:54:00 -04:00
bbedward e78ba77def fix(idle): ensure timeouts can never be 0 2026-03-11 18:55:13 -04:00
purian23 7113afe9e2 fix(settings): Improve error handling for plugin settings loading 2026-03-11 18:03:31 -04:00
purian23 1a2b6524e6 (processes): Add environment flag checks for fprintd and U2F availability 2026-03-11 17:57:30 -04:00
purian23 95c4aa9e4c fix(greeter): Dup crash handlers 2026-03-11 17:13:46 -04:00
purian23 9f2518c9e1 (settings): Enhance authentication checks in Greeter & LockScreen tabs 2026-03-11 16:58:15 -04:00
purian23 76c50a654a fix(qmllint): Update distro detection logic for qmllint 2026-03-11 16:22:37 -04:00
purian23 ded2c38551 fix(greeter): Allow empty password submits to reach PAM 2026-03-11 16:13:26 -04:00
Triệu Kha 772094eacd feat(dropdown): have selected item at dropdown beginning on launch (#1968)
* fix(appdrawer): launcher launched via appdrawer doesnt respect size
setting

* feat(dropdown): have selected item at dropdown beginning on launch
2026-03-11 13:46:44 -04:00
Evgeny Zemtsov bddc2f6295 display: support generic wlr-output-management-unstable-v1 (#1840)
The display config UI only applied changes for compositors with a
config-file backend (niri, hyprland, dwl).  For any other compositor
that supports wlr-output-management-unstable-v1 the "Apply Changes"
button was silently a no-op.

Add WlrOutputService.applyOutputsConfig() as a high-level apply that
mirrors the generateOutputsConfig() pattern of the existing services
but applies directly via the protocol instead of writing a config file.
Route the default case in backendWriteOutputsConfig() to it.

This enables using dms-shell as a wayland compositor for emacs wayland
manager (ewm).
2026-03-11 13:28:14 -04:00
bbedward 25dce2961b fix(launcher): select first file search result by default
fixes #1967
2026-03-11 12:47:05 -04:00
nick-linux8andbbedward 653cfbe6e0 Issue:(Settings)Switched Neovim Mutagen Theme To Default False (#1964)
* Issue:(Settings)Switched Neovim Mutagen Theme To Default False

* also set to false in settingsData
- this is the case when file fails to parse

---------

Co-authored-by: bbedward <[email protected]>
2026-03-11 12:43:56 -04:00
bbedward c539311083 settings: update search index 2026-03-11 12:07:56 -04:00
bbedward 60118c5d5b fix: dsearch references 2026-03-11 12:07:44 -04:00
Vladimir c6b9b36566 feat: highlight active workspace app tiles (#1957)
* feat: highlight active workspace app tiles

* feat: add workspace active-app highlight toggle
2026-03-11 10:57:05 -04:00
Triệu Kha fd5b1b7c00 fix(appdrawer): launcher launched via appdrawer doesnt respect size (#1960)
setting
2026-03-11 10:55:54 -04:00
bbedward ebc77b62c8 plugins: fix list delegates 2026-03-11 09:57:54 -04:00
purian23 2ce888581f obs: Reduce retry timing 2026-03-10 23:00:01 -04:00
purian23 0e901b6404 distros: Update working copy 2026-03-10 22:12:07 -04:00
purian23 688b9076e7 workflows: Update node versioning 2026-03-10 21:30:19 -04:00
purian23 c6ec7579b6 distro: Update OBS workflows 2026-03-10 21:15:43 -04:00
bbedward 9417edac8d fix(popout): anchor cc and notification center to top and bottom 2026-03-10 15:56:40 -04:00
bbedward 6185cc79d7 tooling: make qmllint auto-resolution smarter 2026-03-10 15:41:22 -04:00
bbedward 4ecdba94c2 fix(lint): unused imports removed 2026-03-10 15:32:22 -04:00
Vladimir a11640d840 build: run qmllint through Quickshell tooling VFS (#1958) 2026-03-10 15:32:16 -04:00
purian23 177a4c4095 (greeter): PAM auth improvements and defaults update 2026-03-10 15:02:26 -04:00
lpv 63df19ab78 dock: restore Hyprland special workspace windows on click (#1924)
* dock: restore Hyprland special workspace windows on click

* settings: add dock special workspace restore key to spec
2026-03-10 12:55:36 -04:00
Adarsh219 54e0eb5979 feat: Add Zed editor theming support (#1954)
* feat: Add Zed editor theming support

* fix formatting and switch to CONFIG_DIR
2026-03-10 12:03:01 -04:00
bbedward 185284d422 fix(lock): restore login config fallback 2026-03-10 11:33:44 -04:00
bbedward ce240405d9 system tray: fix shadow consistency
fixes #1946
2026-03-10 11:10:18 -04:00
Marcin Jahn 58b700ed0d fix(shell): cover edge cases of compact focused app widget (#1918)
Fixes two cases:

- some apps (e.g., Zen browser use the "—" character at the end of
  webpage name)
- in compact mode, when app has only appName, and not window name, we
  should display the appName to avoid empty title.
2026-03-10 10:49:28 -04:00
Vladimir d436fa4920 fix(quickshell): stabilize control center numeric widths (#1943) 2026-03-10 10:48:13 -04:00
Augusto César Dias d58486193e feature(notification): show notification only on current focused display (#1923) 2026-03-10 10:46:04 -04:00
bbedward e9404eb9b6 i18n: add russian 2026-03-10 10:43:46 -04:00
purian23 0fef4d515e dankinstall: Update Arch/Quickshell installation 2026-03-09 18:10:55 -04:00
CaptainSpof 86f9cf4376 fix(wallpaper): follow symlinks when scanning wallpaper directory (#1947) 2026-03-09 08:53:22 -04:00
purian23 acf63c57e8 fix(Greeter): Multi-distro reliability updates
- Merge duplicate niri input/output KDL nodes instead of appending. Allows more overrides
- Guard AppArmor install/uninstall behind IsAppArmorEnabled() check
2026-03-08 22:28:32 -04:00
purian23 baa956c3a1 fix(Greeter): Don't stop greeter immediately upon uninstallation 2026-03-07 22:23:21 -05:00
purian23 bb2081a936 feat(Greeter): Add install/uninstall/activate cli commands & new UI opts
- AppArmor profile management
- Introduced `dms greeter uninstall` command to remove DMS greeter configuration and restore previous display manager.
- Implemented AppArmor profile installation and uninstallation for enhanced security.
2026-03-07 20:44:19 -05:00
purian23 c984b0b9ae fix(Clipboard) remove unused copyServe logic 2026-03-07 20:42:54 -05:00
micko 754bf8fa3c update deprecated syntax (#1928) 2026-03-06 21:13:03 -06:00
purian23 7840294517 fix(Clipboard): Epic RAM Growth
- Closes #1920
2026-03-06 22:12:24 -05:00
Connor Welsh caaee88654 fix(Calendar): add missing qs.Common import (#1926)
fixes calendar events getting dropped
2026-03-06 14:19:43 -05:00
Augusto César Dias e872ddc1e7 feature(vpn): add toggle to enable/disable auto connecting (#1925)
* feature(vpn): add toggle to enable/disable auto connecting

* refresh status after updating
2026-03-06 14:19:31 -05:00
purian23 1eca9b4c2c feat: Implement immutable DMS command policy
- Added pre-run checks for greeter and setup commands to enforce policy restrictions
- Created cli-policy.default.json to define blocked commands and user messages for immutable environments.
2026-03-05 23:08:27 -05:00
purian23 fe5bd42e25 greeter: New Greeter Settings UI & Sync fixes
- Add PAM Auth via GUI
- Added new sync flags
- Refactored cache directory management & many others
- Fix for wireplumber permissions
- Fix for polkit auth w/icon
- Add pam_fprintd timeout=5 to prevent 30s auth blocks when using password
2026-03-05 23:04:59 -05:00
purian23 32d16d0673 refactor(greeter): Update auth flows and add configurable opts
- Finally fix debug info logs before dms greeter loads
- prevent greeter/lockscreen auth stalls with timeout recovery and unlock-state sync
2026-03-04 14:17:56 -05:00
Lucas 27c26d35ab flake: allow extra QT packages in dms-shell package (#1903) 2026-03-03 21:47:45 -05:00
Michael Erdely e04c919d78 Not everyone uses paru or yay on Arch: Support pacman command (#1900)
* Not everyone uses paru or yay on Arch: Support pacman command
* Handle sudo properly when using pacman
* Move pacman to bottom per Purian23
* Remote duplicate which -- thanks Purian23!
2026-03-03 17:27:31 -05:00
Triệu Kha 246b6c44b0 fix(dock): Dock flickering when having cursor floating by the side (#1897) 2026-03-03 16:11:06 -05:00
Lucas 847ddf7d38 ipc: update DankBar selection (#1894)
* ipc: update DankBar selection

* ipc: use getPreferredBar in dash open

* ipc: don't toggle dash on dash open
2026-03-02 22:07:40 -05:00
Triệu Kha 16e8199f9e fix(osd): play/pause icon flipped in MediaPlaybackOSD (#1889) 2026-03-02 22:01:08 -05:00
purian23 7d1519f546 fix(dbar): Fixes autohide + click through edge case 2026-03-01 20:54:05 -05:00
purian23 1bf66ee482 fix(notifications): Allow duplicate history entry management w/unique IDs & source tracking 2026-03-01 19:39:00 -05:00
purian23 39a43f4de5 feat: Reintroduce app filters in v2 launcher 2026-03-01 18:34:13 -05:00
purian23 971a511edb fix(notifications): Apply appIdSubs to iconFrImage fallback path
- Consistent with the
appIcon PR changes in #1880.
2026-03-01 17:37:21 -05:00
odtandodtgit 0f8e0bc2b4 refactor(icons): centralize icon resolution into Paths.resolveIconPath/resolveIconUrl (#1880)
Supersedes #1878. Rather than duplicating the moddedAppId + file path
substitution pattern inline across 8 files, this introduces two
centralized functions in Paths.qml:

- resolveIconPath(iconName): for Quickshell.iconPath() callsites,
  with DesktopService.resolveIconPath() fallback
- resolveIconUrl(iconName): for image://icon/ URL callsites

All consumer files now use one-line calls. When no substitutions are
configured, moddedAppId() returns the original name unchanged (zero
cost), so this has no impact on users who don't use the feature.

Affected components:
- AppIconRenderer (8 lines → 1)
- NotificationCard, NotificationPopup, HistoryNotificationCard
- DockContextMenu, AppsDockContextMenu
- LauncherContent, LauncherTab (×3)

Co-authored-by: odtgit <[email protected]>
2026-03-01 17:31:51 -05:00
supposede 537c44e354 Update toolbar button styles with primary color (#1879) 2026-03-01 16:51:40 -05:00
bbedward db53a9a719 i18n: decouple time and language locale
fixes #1876
2026-03-01 15:17:34 -05:00
odtandodtgit f4a10de790 fix(icons): apply file path substitutions in launcher icon resolution (#1877)
Follow-up to #1867. The launcher's AppIconRenderer used its own
Quickshell.iconPath() call without going through appIdSubstitutions,
so PWA icons configured via regex file path rules were not resolved
in the app launcher.

Co-authored-by: odtgit <[email protected]>
2026-03-01 15:03:28 -05:00
bbedward 8c9fe84d02 wallpaper: bump render settle timer 2026-03-01 10:26:46 -05:00
purian23 f0fcc77bdb feat: Implement M3 design elevation & shadow effects
- Added global toggles in the Themes tab
- Light color & directional user ovverides
- Independent shadow overrides per/bar
- Refactored various components to sync the updated designs
2026-03-01 00:54:31 -05:00
purian23 cf4c4b7d69 clipboard: Fix thumbnail load & modal bottom margin 2026-03-01 00:45:38 -05:00
bbedward 7bb8499353 time: add system default option to first day of week dropdown 2026-02-28 20:40:32 -05:00
Jonas Bloch ee1a2bc7de feat: add setting for first day of the week (#1854)
* feat: add setting for first day of the week

* fix: extract settings indices

* fix: formatting mistake

* fix(ui): add outline rectangle between settings and reorder settings

* fix: don't set firstDayOfWeek automatically to system's locale
2026-02-28 20:37:16 -05:00
Giorgio De Trane 20d383d4ab feat(cups): add manual printer addition by IP/hostname (#1868)
Add a new "Add by Address" flow in the printer settings that allows
users to manually add printers by IP address or hostname, enabling
printing to devices not visible via mDNS/Avahi discovery (e.g.,
printers behind Tailscale subnet routers, VPNs, or across network
boundaries).

Go backend:
- New cups.testConnection IPC method that probes remote printers via
  IPP Get-Printer-Attributes with /ipp/print then / fallback
- Input validation with host sanitization and protocol allowlist
- Auth-aware probing (HTTP 401/403 reported as reachable)
- lpadmin CLI fallback for CreatePrinter/DeletePrinter when
  cups-pk-helper polkit authorization fails

QML frontend:
- "Add by Address" toggle alongside existing device discovery
- Manual entry form with host, port, protocol fields
- Test Connection button with loading state and result display
- Smart PPD auto-selection by probed makeModel with driverless fallback
- All strings use I18n.tr() with translator context

Includes 20+ unit tests covering validation, probe delegation, TLS
flag propagation, auth error detection, and handler routing.
2026-02-28 20:36:16 -05:00
odtandodtgit 9cb0d8baf2 feat(icons): support file path substitutions in getAppIcon (#1867)
Allow appIdSubstitutions to return absolute file paths (/, ~, file://)
that bypass Quickshell.iconPath theme lookup. This enables users to map
app IDs directly to icon files on disk via the existing substitution UI.

Fixes PWA icon resolution for Chrome, Chromium and Edge PWAs where
Qt's icon theme lookup fails to find icons installed to
~/.local/share/icons/hicolor/ by the browser.

Example substitutions (Settings → Running Apps → App ID Substitutions):

  ^msedge-_(.+)$ → ~/.local/share/icons/hicolor/128x128/apps/msedge-$1.png
  ^(chrome|msedge|chromium)-(.+)$ → ~/.local/share/icons/hicolor/128x128/apps/$1-$2.png

Tested with Chrome PWAs (YouTube, Twitch, ai-ta) and Edge PWAs
(Microsoft Teams, Outlook) on niri/Wayland.

Co-authored-by: odtgit <[email protected]>
2026-02-28 15:41:28 -05:00
bbedward 362ded3bc9 blurred wallpaper: defer update disabling much longer 2026-02-28 15:39:57 -05:00
bbedward 654f2ec7ad wallpaper: defer updatesEnabled binding 2026-02-28 01:10:04 -05:00
bbedward 3600e034b8 weather: fix geoclue IP fallback 2026-02-28 00:07:04 -05:00
İlkecan Bozdoğan d7c501e175 nix: add package option for dms-shell (#1864)
... to make it configurable.
2026-02-27 23:07:01 -05:00
bbedward b9e9da579f weather: fix fallback temporarily 2026-02-27 22:37:10 -05:00
Sunner 7bea6b4a62 Add GeoClue2 integration as alternative to IP location (#1856)
* feat: switch auto location in weather widget to use GeoClue2 instead of simple IP check

* nix: enable GeoClue2 service by default

* lint: fix line endings

* fix: fall back to IP location if GeoClue is not available
2026-02-27 22:29:08 -05:00
bbedward ab211266a6 loginctl: add fallbacks for session discovery 2026-02-27 10:00:41 -05:00
IrisandIris 4da22a4345 Change IsPluggedIn logic (#1859)
Co-authored-by: Iris <[email protected]>
2026-02-27 09:45:52 -05:00
bbedward fbc1ff62c7 locale: fix locale override persisting even when not explicitly set 2026-02-26 16:15:06 -05:00
Jonas Bloch 1fe72e1a66 feat: add setting to change and hotreload locale (#1817)
* feat: add setting to change and hotreload locale

* fix: typo in component id

* feat: add persistent locale setting

* feat: wrap useLocale in a settings set hook, enable locale hotreload when editing settings file

* chore: update translation and settings file

* feat: enable fuzzy search in locale setting

* fix: regenerate translations with official plugins cloned

* fix: revert back to system's locale for displaying certain time formats
2026-02-26 16:00:17 -05:00
Patrick Fischer f82d7610e3 feat: Add FIDO2/U2F security key support for lock screen (#1842)
* feat: Add FIDO2/U2F security key support for lock screen

Adds hardware security key authentication (e.g. YubiKey) with two modes:
Alternative (OR) and Second Factor (AND). Includes settings UI, PAM
integration, availability detection, and proper state cleanup.

Also fixes persist:false properties being reset on settings file reload.

* feat: Add U2F pending timeout and Escape to cancel

Cancel U2F second factor after 30s or on Escape key press,
returning to password/fingerprint input.

* fix: U2F detection honors custom PAM override for non-default key paths
2026-02-26 15:58:21 -05:00
Augusto César Dias bd6ad53875 feat(lockscreen): enable use of videos as screensaver in the lock screen (#1819)
* feat(lockscreen): enable use of videos as screensaver in the lock screen

* reducing debug logs

* feature becomes available only when QtMultimedia is available
2026-02-26 11:02:50 -05:00
Youseffo13 5d09acca4c Added plural support (#1750)
* Update it.json

* Enhance SettingsSliderRow: add resetText property and update reset button styling

* added i18n strings

* adjust reset button width to be dynamic based on content size

* added i18n strings

* Update template.json

* reverted changes

* Update it.json

* Update template.json

* Update NotificationSettings.qml

* added plurar support

* Update it.json

* Update ThemeColorsTab.qml
2026-02-26 09:36:42 -05:00
Jan Greimann b4e7c4a4cd Adjust SystemUpdate process (#1845)
This fixes the problem that the system update terminal closes when the package manager encounters a problem (exit code != 0), allowing the user to understand the problem.

Signed-off-by: Jan Phillip Greimann <[email protected]>
2026-02-26 09:05:06 -05:00
Kangheng Liuandbbedward a6269084c0 Systray: call context menu fallback for legacy protocol (#1839)
* systray: add call contextmenu fallback

directly call dbus contextmenu method. needs refactoring to be more
robust.

* add TODO

---------

Co-authored-by: bbedward <[email protected]>
2026-02-25 17:19:09 -05:00
bbedward 8271d8423d greeter: sync power menu options 2026-02-25 14:50:06 -05:00
bbedward c76e29c457 dankdash: fix menu overlays 2026-02-25 14:37:55 -05:00
purian23 4750a7553b feat: Add independent power action confirmation settings for dms greeter 2026-02-25 14:33:09 -05:00
Joaquim S. 60786921a9 matugen/template: Pasterizing neovim. (#1828)
* matugen/template: Pasterizing neovim.

* matugen/template: More contrast
2026-02-25 13:58:36 -05:00
bbedward 751bbcc127 desktop widgets: fix deactive loaders when widgets disabled fixes #1813 2026-02-25 12:34:09 -05:00
null 58e8dd5456 feat: add more disk usage viewing options (#1833)
* feat: show memory widget in gb

* cleanup

* even more cleanup

* fix

* feat: add more disk usage viewing options
2026-02-25 10:53:12 -05:00
bbedward 1586c25847 dankbar: layer enabled false + binding tweak 2026-02-25 10:45:08 -05:00
null cded5a7948 feat: show memory widget in gb (#1825)
* feat: show memory widget in gb

* cleanup

* even more cleanup

* fix
2026-02-25 08:01:41 -05:00
purian23 6238e065f2 distros: Workflows input type updates 2026-02-24 23:24:05 -05:00
purian23 72fbbfdd0d distros: Update workflows 2026-02-24 22:59:17 -05:00
purian23 2796c1cd4d fix: Defer DankCircularImage saving until the window is available 2026-02-24 21:23:36 -05:00
bbedward 54c9886627 settings: make horizontal change more smart 2026-02-24 20:48:42 -05:00
bbedward 05713cb389 settings: restore notifyHorizontalBarChanged 2026-02-24 19:42:29 -05:00
purian23 8bb3ee5f18 fix: Update HTML rendering injections 2026-02-24 19:34:46 -05:00
purian23 bc0b4825f1 dbar: Refactor to memoize dbar & widget state via json 2026-02-24 18:56:30 -05:00
purian23 ef7f17abf4 cpu widget: Fix monitor binding 2026-02-24 17:21:42 -05:00
Yamada.Kazuyoshi 876cd21f0b display battery consumption / charging W (#1814) 2026-02-24 15:42:47 -05:00
bbedward 5c92d49873 settings: use Image in theme colors tab wp preview 2026-02-24 15:22:47 -05:00
bbedward da47b573be popout: fully unload popout layers on close 2026-02-24 15:19:30 -05:00
bbedward 2f04be8778 wallpaper: handle initial load better, add dms randr command for quick
physical scale retrieval
2026-02-24 15:09:04 -05:00
bbedward 69178ddfd8 privacy indicator: fix width when not active 2026-02-24 13:59:38 -05:00
bbedward a310f6fff0 settings: use Image for per-mode previews 2026-02-24 13:36:57 -05:00
bbedward 7474abe286 matugen: skip theme refreshes if no colors changed 2026-02-24 13:28:13 -05:00
bbedward df2ba3a3c6 dock: fix tooltip positioning 2026-02-24 13:28:13 -05:00
bbedward e536456236 dankbar: fix some defaults in reset 2026-02-24 13:28:13 -05:00
bbedward 8d77122da3 widgets: set updatesEnabled false on background layers, if qs supports
it
2026-02-24 13:28:13 -05:00
bbedward fb66effa51 widgets: fix moddedAppID consistency 2026-02-24 13:28:13 -05:00
purian23 5052e71c31 settings: Re-adjust dbar layout 2026-02-24 12:24:05 -05:00
purian23 bfc78d16ca settings: Dankbar layout updates 2026-02-24 12:02:16 -05:00
purian23 c425e3562b fix: Clipboard button widget alignment 2026-02-24 11:50:36 -05:00
bbedward 1f26092aa9 dankbar: fix syncing settings to new bars 2026-02-24 11:00:43 -05:00
bbedward 2849bb96f4 i18n: term sync 2026-02-24 10:50:16 -05:00
bbedward 7b749f2a4c dankbar: restore horizontal change debounce 2026-02-24 10:49:09 -05:00
bbedward 8803c94ce0 dpms: disable fade overlay in onRequestMonitorOn 2026-02-24 10:38:12 -05:00
bbedward f5235c943b dankbar: optimize bindings in bar window 2026-02-24 10:22:43 -05:00
bbedward 59fec889b5 widgets: fix undefined icon warnings 2026-02-24 10:05:28 -05:00
null f42f04a807 feat: improve icon resolution and align switcher fallback styling (#1823)
- Implement deep search icon resolution in DesktopService with runtime caching.
- Update Paths.getAppIcon to utilize enhanced resolution for mismatched app IDs.
- Align Workspace Switcher fallback icons with AppsDock visual style.
- Synchronize fallback text logic between Switcher and Dock using app names.
2026-02-24 09:40:15 -05:00
purian23 51f6f37925 display: Fix output config on delete & popup height 2026-02-24 01:03:51 -05:00
purian23 9651a4ca98 template: Refine bug report tracker 2026-02-24 00:41:04 -05:00
purian23 2b7fd36322 feat: Refactor DankBar w/New granular options
- New background toggles
- New maxIcon & maxText widget sizes (global)
- Dedicated M3 padding slider
- New independent icon scale options
- Updated logic to improve performance on single & dual bar modes
2026-02-23 23:18:27 -05:00
purian23 b8014fd4df fix: Animated Image warnings 2026-02-23 23:18:02 -05:00
Lucas 07460f6e1f doctor: fix imageformats detection (#1811) 2026-02-23 19:44:51 -05:00
bbedward f7bf3b2afb keybinds: preserve scroll position of expanded item on list change
fixes #1766
2026-02-23 19:32:13 -05:00
bbedward 056f298cdf widgets: fallback when AnimatedImage probe fails to static Image 2026-02-23 19:03:14 -05:00
bbedward e83da53162 thememode: connect to loginctl PrepareForSleep event 2026-02-23 19:01:20 -05:00
purian23 9f38a47a02 dms-greeter: Update dankinstall greeter automation w/distro packages 2026-02-23 15:36:17 -05:00
bbedward 236a4d4a6d launcher: don't tie unload to visibility 2026-02-23 15:28:29 -05:00
purian23 0909471510 audio: Sync audio hide opts w/dash Output devices 2026-02-23 13:48:33 -05:00
bbedward 05eaf59c89 audio: fix cycle output, improve icon resolution for sink
fixes #1808
2026-02-23 13:21:43 -05:00
Lucas 7749613801 nix: update flake.lock (#1809) 2026-02-23 12:31:36 -05:00
bbedward e3dbaedbb4 audio: disable effects when mpris player is playing 2026-02-23 12:31:04 -05:00
bbedward 9f17ced6de launcher: implement memory for selected tab
fixes #1806
2026-02-23 10:19:31 -05:00
dms-ci[bot] de54ef871d nix: update vendorHash for go.mod changes 2026-02-23 15:04:39 +00:00
Jonas Blochandbbedward b0da45d6d0 Wifi QR code generation (requires testing) (#1794)
* fix: add mockery v2 to nix flake pkgs

* feat: add requests for generating wifi qr codes as png files in /tmp, and to delete them later. only supports NetworkManager backend for now.

* feat: add modal for sharing wifi via qr code and saving the code as png file.

* fix: uncomment QR code file deletion

* network: light refactor and cleanup for QR code generation

---------

Co-authored-by: bbedward <[email protected]>
2026-02-23 10:02:51 -05:00
bbedward 9b2a46fa92 widgets: make AnimatedImage conditional in DankCircularImage
- Cut potential overhead of always using AnimatedImage
2026-02-23 09:51:56 -05:00
bbedward 12099d2db6 osd: disable media playback OSD by default 2026-02-23 09:25:23 -05:00
Triệu Kha 84fa75936a clipboard: fix html elements get parsed in clipboard entry (#1798)
* clipboard: fix html elements get parsed in clipboard entry

* Revert "clipboard: fix html elements get parsed in clipboard entry"

This reverts commit 52b11eeb98.

* clipboard: fix html elements get parsed in clipboard entry
2026-02-23 09:16:15 -05:00
Jonas Bloch d78d8121a1 fix(notepad): decode path URI when saving/creating a file (#1805) 2026-02-23 09:15:51 -05:00
Jonas Bloch a9a3a52872 feat: add support for animated gifs as profile pictures (#1804) 2026-02-23 09:15:26 -05:00
purian23 912e3bdfce dms-greeter: Enable greetd via dms greeter install all-in-one cmd 2026-02-22 23:26:40 -05:00
bbedward ee1b25d9e8 matugen: unconditionally run portal sync even if matugen errors 2026-02-22 22:57:51 -05:00
purian23 20ef5e2c18 dms-greeter: Enhance DMS Greeter dankinstall & packaging across distros
- Added support for Debian, Ubuntu, Fedora, Arch, and OpenSUSE on dankinstall / dms greeter install
2026-02-22 22:40:51 -05:00
bbedward 6ee419bc52 launcher: fix frecency ranking in search results
fixes #1799
2026-02-22 22:34:26 -05:00
bbedward 85b00d3c76 scripts: fix shellcheck 2026-02-22 22:13:57 -05:00
bbedward bc4ad31d48 bluetooth: expose trust/untrust on devices 2026-02-22 22:10:07 -05:00
长夜月玩Fedora 71aad8ee32 Add support for 'evernight' distribution in Fedora (#1786) 2026-02-22 22:02:37 -05:00
Triệu Kha 8bb8231559 Fix dock visible when theres no app (#1797)
* clipboard: improve image thumbnail
- thumbnail image is now bigger
- circular mask has been replaced with rounded rectangular mask

* dock: fix dock still visible when there's no app
2026-02-22 21:56:33 -05:00
purian23 3cf9caae89 feat: DMS Greeter packaging for Debian/OpenSUSE on OBS 2026-02-22 15:44:42 -05:00
Lucas f983c67135 zen: add more commands to detection (#1792) 2026-02-21 14:58:36 -05:00
bbedward f2aef5b93f Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2026-02-21 10:43:42 -05:00
purian23 46d4288969 ipc: Fix DankDash Wallpaper call 2026-02-21 00:52:08 -05:00
purian23 65516e872f theme: Fix Light/Dark mode portal sync 2026-02-21 00:31:26 -05:00
Connor Welsh 171329246c distro: add cups-pk-helper as suggested dependency (#1670) 2026-02-20 14:26:59 -05:00
bbedward b2bee699e0 window rules: default to fixed for width/height
part of #1774
2026-02-20 13:51:03 -05:00
purian23 95c66b4d67 ubuntu: Fix dms-git Go versioning to restore builds 2026-02-20 13:12:12 -05:00
bbedward babc8feb2b clipboard: fix memory leak from unbounded offer maps and unguarded file reads 2026-02-20 11:39:41 -05:00
bbedward 2f445c546a keybinds/niri: fix quote preservation 2026-02-20 11:37:02 -05:00
bbedward a0283b3e3e dankdash: fix widgets across different bar section
fixes #1764s
2026-02-20 11:28:56 -05:00
bbedward 61bd156fb0 core/screenshot: light cleanups 2026-02-20 11:16:53 -05:00
Patrick Fischer 8ad0cf8e5f screensaver: emit ActiveChanged on lock/unlock (#1761) 2026-02-20 11:05:30 -05:00
Triệu Kha ecd6d70da6 clipboard: improve image thumbnail (#1759)
- thumbnail image is now bigger
- circular mask has been replaced with rounded rectangular mask
2026-02-20 11:02:20 -05:00
purian23 359617d927 template: Default install method 2026-02-20 10:15:20 -05:00
purian23 38c286329a issues: Template fix 2026-02-20 09:57:58 -05:00
purian23 401b4095cc templates: Fix GitHub issue labels 2026-02-20 09:44:54 -05:00
shorinkiwata 06ab1a8ef0 feat(distros): allow CatOS to run DMS installer (#1768)
- This PR adds support for **CatOS**
- CatOS is fully compatible with Arch Linux
2026-02-20 09:28:42 -05:00
purian23 726fb8b015 templates: Update DMS issue formats 2026-02-20 09:26:20 -05:00
bbedward b3b5c7a59f running apps: fix ordering on niri 2026-02-19 20:46:14 -05:00
purian23 d18f934978 fedora: Update trial run of bundled go deps 2026-02-19 19:21:01 -05:00
bbedward e67f1f79bc launcher/dsearch: support for folder search and extra filters 2026-02-19 18:19:46 -05:00
purian23 e931829411 distros: Fix Go versioning on dms-git builds 2026-02-19 17:20:22 -05:00
bbedward db8ebd606c launcher: fix premature exit of file search
fixes #1749
2026-02-19 16:46:50 -05:00
Jonas Bloch 072a358a94 Search keybinds fixes (#1748)
* fix: close keybind cheatsheet on escape press

* feat: match all space separated words in keybind cheatsheet search
2026-02-19 16:26:45 -05:00
bbedward 6ceb1b150c audio: fix hide device not working 2026-02-19 16:24:28 -05:00
bbedward a4e03e1877 i18n: term sync 2026-02-19 13:32:06 -05:00
Youseffo13 02b3e4277b Added missing i18n strings and changed reset button (#1746)
* Update it.json

* Enhance SettingsSliderRow: add resetText property and update reset button styling

* added i18n strings

* adjust reset button width to be dynamic based on content size

* added i18n strings

* Update template.json

* reverted changes

* Update it.json

* Update template.json
2026-02-19 13:31:24 -05:00
bbedward 37daf801e6 dankbar: remove behaviors from monitoring widgets 2026-02-19 11:49:55 -05:00
bbedward 68d9f7eeb2 dgop: round computed values to match display format 2026-02-19 11:02:56 -05:00
bbedward 526e2420ca flake: fix dev flake for go 1.25 and ashellchheck 2026-02-19 09:30:05 -05:00
bbedward a9cc58fc28 hyprland: add serial to output model generator 2026-02-19 09:22:10 -05:00
bbedward 77889ce1c6 dock: fix context menu styling
fixes #1742
2026-02-19 09:15:26 -05:00
bbedward 549073119e dock: fix transparency setting
fixes #1739
2026-02-19 09:11:30 -05:00
bbedward 5c5af5795f launcher: improve perf of settings search 2026-02-19 08:46:19 -05:00
bbedward 68e10934e4 launcher: always heuristic lookup cached entries 2026-02-18 23:47:00 -05:00
bbedward c67bb1444a launcher v2: always heuristicLookup tab actions 2026-02-18 19:07:03 -05:00
bbedward 07389a152e i18n: term updates 2026-02-18 18:34:41 -05:00
bbedward e562e21555 system tray: fix to take up 0 space when empty 2026-02-18 18:31:33 -05:00
Youseffo13 86dfe7dd3f Added Missing i18n strings (#1729)
* inverted dock visibility and position option

* added missing I18n strings

* added missing i18n strings

* added i18n strings

* Added missing i18n strings

* updated translations

* Update it.json
2026-02-18 18:28:57 -05:00
bbedward ac0a8f3449 widgets: add openWith/toggleWith modes for dankbar widgets 2026-02-18 16:23:42 -05:00
bbedward 8e4a63db67 keybinds: fix escape in keybinds modal 2026-02-18 14:57:34 -05:00
bbedward c02c63806f launcher v2: remove calc
cc: enhancements for plugins to size details
2026-02-18 14:48:20 -05:00
beluch-dev 42e5d7f6e9 fix: correct parameter name in Hyprland windowrule (no_initial_focus) (#1726)
##Description
This PR corrects the parameter name to match new Hyprland standard.

## Changes
-Before: 'noinitialfocus'
-After: 'no_initial_focus'
2026-02-18 13:41:12 -05:00
bbedward d8cf1af422 plugins: fix settings focus loss 2026-02-18 13:36:23 -05:00
Evgeny Zemtsov 9723661c80 handle recycled server object IDs for workspace/group handles (#1725)
When switching tabs rapidly or closing multiple tabs, the taskbar shows
"ghost" workspaces — entries with no name, no coordinates, and no active
state. The ghosts appear at positions where workspaces were removed and
then recreated by the compositor.

When a compositor removes a workspace (sends `removed` event) and the
client calls Destroy(), the proxy is marked as zombie but stays in the
Context.objects map. For server-created objects (IDs >= 0xFF000000), the
server never sends `delete_id`, so the zombie proxy persists indefinitely.

When the compositor later creates a new workspace that gets a recycled
server object ID, GetProxy() returns the old zombie proxy. The dispatch
loop in GetDispatch() checks IsZombie() and silently drops ALL events
for zombie proxies — including property events (name, id, coordinates,
state, capabilities) intended for the new workspace. This causes the
ghost workspaces with empty properties in the UI.

Fix: check IsZombie() when handling `workspace` and `workspace_group`
events that carry a `new_id` argument. If the existing proxy is a
zombie, treat it as absent and create a fresh proxy via
registerServerProxy(), which replaces the zombie in the map. Subsequent
property events are then dispatched to the live proxy.
2026-02-18 12:51:17 -05:00
bbedward 81cba7ad97 popout: decouple shadow from content layer 2026-02-18 10:45:45 -05:00
bbedward c23f58de40 popout: disable layer after animation 2026-02-18 10:34:07 -05:00
purian23 2cf67ca7da notifications: Maintain shadow during expansion 2026-02-18 10:27:57 -05:00
purian23 392bd850ea notifications: Update initial popup height surfaces 2026-02-18 10:16:11 -05:00
bbedward 3b2ad9d1bd running apps: fix scroll events being propagated
fixes #1724
2026-02-18 10:12:15 -05:00
bbedward 27b7474180 matugen: make v4 detection more resilient 2026-02-18 09:55:45 -05:00
bbedward 63948d728e process list: fix scaling with fonts
fixes #1721
2026-02-18 09:55:45 -05:00
purian23 d219d3b873 dankinstall: Fix Debian ARM64 detection 2026-02-18 09:41:36 -05:00
bbedward 93ab290bc1 matugen: detect emacs directory
fixes #1720
2026-02-18 09:23:13 -05:00
bbedward 7335c5d79a osd: optimize bindings 2026-02-18 09:04:39 -05:00
bbedward 242ead722a screenshot: adjust cursor CLI option to be more explicit 2026-02-17 22:28:19 -05:00
bbedward 8a6d9696a8 settings: workaround crash 2026-02-17 22:20:01 -05:00
purian23 896b7ea242 notifications: Tweak animation scale & settings 2026-02-17 22:05:19 -05:00
bbedward 0c7f4c7828 settings: guard internal writes from watcher 2026-02-17 22:03:36 -05:00
bbedward 3d35af2a87 cc: fix plugin reloading in bar position changes 2026-02-17 17:24:22 -05:00
bbedward fed3c36f84 popout: anchor height changing popout surfaces to top and bottom 2026-02-17 17:18:07 -05:00
bbedward 414d81aa40 workspaces: fix named workspace icons 2026-02-17 16:02:13 -05:00
bbedward d548803769 dankinstall: no_anim on dms layers 2026-02-17 15:32:08 -05:00
bbedward 1180258394 system updater: fix hide no update option 2026-02-17 13:53:17 -05:00
bbedward 48a566a24b launcher: fix kb navigation not always showing last delegate in view 2026-02-17 13:07:43 -05:00
bbedward 3bc5d1df81 doctor: add qt6-imageformats check 2026-02-17 12:58:09 -05:00
bbedward c7222e2e86 bump version, codename, disable changelog 2026-02-17 12:02:36 -05:00
1847 changed files with 485667 additions and 141182 deletions

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
# Agent Skills
This directory contains agent skills following the [Agent Skills](https://agentskills.io) open standard - a portable, version-controlled format for giving AI agents specialized capabilities.
Each skill is a directory with a `SKILL.md` entrypoint, optional reference docs, scripts, and templates. Agents load skills progressively: metadata at startup, full instructions on activation, and supporting files on demand.
## Available Skills
| 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. |
## Installation
The `.agents/skills/` directory at the project root is the standard location defined by the agentskills.io spec. Many agents discover skills from this path automatically. Some agents use their own directory conventions and need a symlink or copy.
### Claude Code
Claude Code discovers skills from `.claude/skills/` (project-level) or `~/.claude/skills/` (personal). To make skills from `.agents/skills/` available, symlink them into the Claude Code skills directory:
**Project-level** (this repo only):
```bash
mkdir -p .claude/skills
ln -s ../../.agents/skills/dms-plugin-dev .claude/skills/dms-plugin-dev
```
**Personal** (all your projects):
```bash
ln -s /path/to/DankMaterialShell/.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.
See the [Claude Code skills docs](https://code.claude.com/docs/en/skills) for more on skill configuration, invocation control, and frontmatter options.
### Cursor
Cursor discovers skills from `.cursor/skills/` in the project root:
```bash
mkdir -p .cursor/skills
ln -s ../../.agents/skills/dms-plugin-dev .cursor/skills/dms-plugin-dev
```
See [Cursor skills docs](https://cursor.com/docs/context/skills) for details.
### VS Code (Copilot)
VS Code Copilot discovers skills from `.github/skills/` or `.vscode/skills/`:
```bash
mkdir -p .github/skills
ln -s ../../.agents/skills/dms-plugin-dev .github/skills/dms-plugin-dev
```
See [VS Code skills docs](https://code.visualstudio.com/docs/copilot/customization/agent-skills) for details.
### Gemini CLI
Gemini CLI discovers skills from `.gemini/skills/` in the project root:
```bash
mkdir -p .gemini/skills
ln -s ../../.agents/skills/dms-plugin-dev .gemini/skills/dms-plugin-dev
```
See [Gemini CLI skills docs](https://geminicli.com/docs/cli/skills/) for details.
### OpenAI Codex
Codex discovers skills from `.codex/skills/` in the project root:
```bash
mkdir -p .codex/skills
ln -s ../../.agents/skills/dms-plugin-dev .codex/skills/dms-plugin-dev
```
See [Codex skills docs](https://developers.openai.com/codex/skills/) for details.
### Other Agents
The Agent Skills standard is supported by 30+ tools including Goose, Roo Code, JetBrains Junie, Amp, OpenCode, OpenHands, Kiro, and more. Most discover skills from a dot-directory at the project root (e.g., `.goose/skills/`, `.roo/skills/`). Some read `.agents/skills/` directly.
Check the [Agent Skills client showcase](https://agentskills.io/clients) for setup instructions specific to your agent.
The general pattern is:
```bash
mkdir -p .<agent>/skills
ln -s ../../.agents/skills/dms-plugin-dev .<agent>/skills/dms-plugin-dev
```
## Adding New Skills
To add a new skill to this directory:
1. Create a subdirectory named with lowercase letters, numbers, and hyphens (e.g., `my-new-skill/`)
2. Add a `SKILL.md` file with YAML frontmatter (`name`, `description`) and markdown instructions
3. Optionally add `references/`, `scripts/`, and `assets/` subdirectories
4. Keep `SKILL.md` under 500 lines - move detailed content to reference files
See the [Agent Skills specification](https://agentskills.io/specification) for the full format.
+640
View File
@@ -0,0 +1,640 @@
---
name: dms-plugin-dev
description: >
Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on
Quickshell. Supports five plugin types: widget (bar + Control Center), daemon (background
service), launcher (search + actions), desktop (draggable desktop widgets), and composite
(multi-surface). Covers manifest creation, QML component development, startup checks,
settings UI, data persistence, theme integration, PopoutService usage, IPC runtime
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.
compatibility: Designed for Claude Code (or similar products)
metadata:
author: DankMaterialShell
version: "1.1"
domain: qml-desktop-development
framework: DankMaterialShell
languages: qml, javascript
allowed-tools: Bash Read Write Edit
---
# DankMaterialShell Plugin Development
## Overview
DMS plugins extend the desktop shell with custom widgets, background services, launcher
integrations, and desktop widgets. Plugins are QML components discovered from
`~/.config/DankMaterialShell/plugins/`.
**Minimum plugin structure:**
```
~/.config/DankMaterialShell/plugins/YourPlugin/
plugin.json # Required: manifest with metadata
YourComponent.qml # Required: main QML component
YourSettings.qml # Optional: settings UI
*.js # Optional: JavaScript utilities
```
**Plugin registry:** Community plugins are available at https://plugins.danklinux.com/
**Five plugin types:**
| Type | Purpose | Base Component | Bar pills | CC integration |
|-------------|--------------------------------|----------------------------|-----------|----------------|
| `widget` | Bar widget + popout | `PluginComponent` | Yes | Yes |
| `daemon` | Background service | `PluginComponent` (no UI) | No | Optional |
| `launcher` | Searchable items in launcher | `Item` | No | No |
| `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No |
| `composite` | Multi-surface plugin | One component per surface | Optional | Optional |
## Step 1: Determine Plugin Type
Choose the type based on what the plugin does:
- **Shows in the bar?** - Use `widget`. Displays a pill in DankBar, optionally opens a popout,
optionally integrates with Control Center.
- **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper
changes, notifications, battery level, etc.).
- **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher
with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).
- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the
desktop layer.
- **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
QML component file.
## Step 2: Create the Manifest
Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md](references/plugin-manifest-reference.md) for the full schema.
**Minimal manifest:**
```json
{
"id": "yourPlugin",
"name": "Your Plugin Name",
"description": "Brief description of what your plugin does",
"version": "1.0.0",
"author": "Your Name",
"type": "widget",
"capabilities": ["your-capability"],
"component": "./YourWidget.qml"
}
```
**With settings, startup check, and permissions:**
```json
{
"id": "yourPlugin",
"name": "Your Plugin Name",
"description": "Brief description",
"version": "1.0.0",
"author": "Your Name",
"type": "widget",
"capabilities": ["your-capability"],
"component": "./YourWidget.qml",
"icon": "extension",
"settings": "./Settings.qml",
"startupCheck": "./StartupCheck.qml",
"requires_dms": ">=0.1.0",
"dependencies": ["mytool"],
"permissions": ["settings_read", "settings_write"]
}
```
**Composite plugin (multi-surface):**
```json
{
"id": "myComposite",
"name": "My Composite Plugin",
"description": "Daemon + widget + desktop from one plugin",
"version": "1.0.0",
"author": "Your Name",
"type": "composite",
"capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
"icon": "extension",
"components": {
"daemon": "./MyDaemon.qml",
"widget": "./MyBarWidget.qml",
"desktop": "./MyDesktopWidget.qml"
},
"settings": "./Settings.qml",
"permissions": ["settings_read", "settings_write"]
}
```
**Key rules:**
- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`
- `version` must be semver (e.g., `1.0.0`)
- Provide either `component` (single-surface) or `components` (multi-surface), not both
- `component` / component paths must start with `./` and end with `.qml`
- `type: "launcher"` (or a `components` object with a `launcher` key) requires a `trigger` field
- `settings_write` permission is **required** if the plugin has a settings component
- `dependencies` replaces the deprecated `requires` field
## Step 3: Create the Main Component
### Widget
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
property var popoutService: null
horizontalBarPill: Component {
StyledRect {
width: label.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: label
anchors.centerIn: parent
text: "Hello"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
}
}
verticalBarPill: Component {
StyledRect {
width: parent.widgetThickness
height: label.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: label
anchors.centerIn: parent
text: "Hi"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
rotation: 90
}
}
}
}
```
See [widget-plugin-guide.md](references/widget-plugin-guide.md) for popouts, CC integration, and advanced features.
### Launcher
```qml
import QtQuick
import qs.Services
Item {
id: root
property var pluginService: null
property string trigger: "#"
signal itemsChanged()
function getItems(query) {
const items = [
{ name: "Item One", icon: "material:star", comment: "Description",
action: "toast:Hello!", categories: ["MyPlugin"] }
]
if (!query) return items
const q = query.toLowerCase()
return items.filter(i => i.name.toLowerCase().includes(q))
}
function executeItem(item) {
const [type, ...rest] = item.action.split(":")
const data = rest.join(":")
if (type === "toast") ToastService?.showInfo(data)
else if (type === "copy") Quickshell.execDetached(["dms", "cl", "copy", data])
}
}
```
See [launcher-plugin-guide.md](references/launcher-plugin-guide.md) for triggers, icon types, context menus, and image tiles.
### Desktop
```qml
import QtQuick
import qs.Common
Item {
id: root
property var pluginService: null
property string pluginId: ""
property bool editMode: false
property real widgetWidth: 200
property real widgetHeight: 200
property real minWidth: 150
property real minHeight: 150
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: 0.85
border.color: root.editMode ? Theme.primary : "transparent"
border.width: root.editMode ? 2 : 0
Text {
anchors.centerIn: parent
text: "Desktop Widget"
color: Theme.surfaceText
}
}
}
```
See [desktop-plugin-guide.md](references/desktop-plugin-guide.md) for sizing, persistence, and edit mode.
### Daemon
```qml
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
property var popoutService: null
Connections {
target: SessionData
function onSomeSignal() {
console.log("Event received")
}
}
}
```
See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
### Composite
For composite plugins, create a separate QML file per surface. Each surface uses the same
base component as the corresponding single-surface type (PluginComponent for widget/daemon,
Item for launcher, etc.). All surfaces share the same `pluginId` and `pluginService`.
```
MyCompositePlugin/
plugin.json
MyBarWidget.qml # PluginComponent (widget surface)
MyDaemon.qml # PluginComponent (daemon surface)
MyDesktopWidget.qml # Item with desktop widget properties
Settings.qml # Shared settings for all surfaces
```
Use `pluginService.pluginHasSurface(pluginId, "widget")` to check whether a specific surface
is registered for a plugin at runtime.
## Step 4: Add Startup Check (Optional)
Gate plugin activation on dependency checks by providing a `startupCheck` component. This
runs before the plugin loads and blocks activation if a required tool or condition is missing.
Create a `StartupCheck.qml` (non-visual QtObject):
```qml
import QtQuick
import qs.Common
QtObject {
function check(done) {
Proc.runCommand("myPlugin.depCheck", ["sh", "-c", "command -v mytool"], (stdout, exitCode) => {
if (exitCode === 0) {
done(null);
return;
}
done({
"title": I18n.tr("mytool is required"),
"details": I18n.tr("Install 'mytool' and re-enable this plugin.")
});
});
}
}
```
The `done` callback accepts:
- `null` - allow activation
- A string - block with a short error message
- `{ title, details }` - block with a title and expandable details body
A synchronous variant (no `done` parameter, return the result directly) is also supported.
Failed checks show a toast error and store the error in `pluginService.pluginLoadErrors`.
Add to your manifest:
```json
{
"startupCheck": "./StartupCheck.qml",
"dependencies": ["mytool"]
}
```
## Step 5: Add Settings (Optional)
Wrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "yourPlugin"
StringSetting {
settingKey: "apiKey"
label: "API Key"
description: "Your API key"
placeholder: "sk-..."
}
ToggleSetting {
settingKey: "enabled"
label: "Enable Feature"
defaultValue: true
}
SelectionSetting {
settingKey: "interval"
label: "Refresh Interval"
options: [
{ label: "1 min", value: "60" },
{ label: "5 min", value: "300" }
]
defaultValue: "300"
}
}
```
**Available setting components:** StringSetting, ToggleSetting, SelectionSetting, SliderSetting, ColorSetting, ListSetting, ListSettingWithInput.
See [settings-components-reference.md](references/settings-components-reference.md) for full property lists.
**Important:** Your plugin must declare `"permissions": ["settings_write"]` in plugin.json, or the settings UI will show an error.
## Step 6: Use Data Persistence
Three tiers of persistence:
| API | Persisted | Use case |
|-----|-----------|----------|
| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |
| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |
| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |
| `pluginService.getPluginPath(id)` | N/A | Get the plugin's installation directory path |
- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings
- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`
- Global vars sync across all instances (multi-monitor, multiple bar sections)
See [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.
## Step 7: Theme Integration
Always use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.
**Essential properties:**
- Colors: `Theme.surfaceContainerHigh`, `Theme.surfaceText`, `Theme.primary`, `Theme.onPrimary`
- Fonts: `Theme.fontSizeSmall` (12), `Theme.fontSizeMedium` (14), `Theme.fontSizeLarge` (16), `Theme.fontSizeXLarge` (20)
- Spacing: `Theme.spacingXS`, `Theme.spacingS`, `Theme.spacingM`, `Theme.spacingL`, `Theme.spacingXL`
- Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge`
- Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32)
**Common widgets from `qs.Widgets`:** `StyledText`, `StyledRect`, `DankIcon`, `DankButton`, `DankToggle`, `DankTextField`, `DankSlider`, `DankGridView`, `CachingImage`.
See [theme-reference.md](references/theme-reference.md) for the complete property list.
## Step 8: Add Popout Content (Widgets Only)
Add a popout that opens when the bar pill is clicked:
```qml
PluginComponent {
popoutWidth: 400
popoutHeight: 300
popoutContent: Component {
PopoutComponent {
headerText: "My Plugin"
detailsText: "Optional subtitle"
showCloseButton: true
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Content here"
color: Theme.surfaceText
}
}
}
}
horizontalBarPill: Component { /* ... */ }
verticalBarPill: Component { /* ... */ }
}
```
**PopoutComponent properties:** `headerText`, `detailsText`, `showCloseButton`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly).
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
## Step 9: Control Center Integration (Widgets Only)
Add your widget to the Control Center grid:
```qml
PluginComponent {
ccWidgetIcon: "toggle_on"
ccWidgetPrimaryText: "My Feature"
ccWidgetSecondaryText: isActive ? "On" : "Off"
ccWidgetIsActive: isActive
onCcWidgetToggled: {
isActive = !isActive
pluginService?.savePluginData(pluginId, "active", isActive)
}
// Optional: expandable detail panel (for CompoundPill)
ccDetailContent: Component {
Rectangle {
implicitHeight: 200
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
}
}
}
```
**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).
## Step 10: External Commands and Clipboard
**Run commands and capture output:**
```qml
import qs.Common
Proc.runCommand(
"myPlugin.fetch",
["curl", "-s", "https://api.example.com/data"],
(stdout, exitCode) => {
if (exitCode === 0) processData(stdout)
},
500 // debounce ms
)
```
**Fire-and-forget (clipboard, notifications):**
```qml
import Quickshell
Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
```
**Long-running processes:** Use the `Process` QML component from `Quickshell.Io` with `StdioCollector`.
**Shell commands with pipes:** `["sh", "-c", "ps aux | grep foo"]`
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
## Translations (Optional)
Ship per-locale files in a `translations/` directory; DMS loads the one matching the active locale and re-reads it on locale changes:
```
MyPlugin/
└── translations/
├── es.json
└── zh_CN.json
```
File names follow DMS locale naming (`es.json`, `pt.json`, `zh_CN.json`); resolution tries the exact tag, then the hyphenated form, then the bare language. No `en.json` - the QML terms are the English strings.
Each file maps the English term to its translation, using the term as its own context bucket:
```json
{
"Cycle Speed": {
"Cycle Speed": "Velocidad de ciclo"
}
}
```
In QML, call `I18n.trFor` with the plugin id as a literal string matching `plugin.json`:
```qml
text: I18n.trFor("myPlugin", "Cycle Speed")
```
Lookup order: plugin file, then the global DMS catalog, then the English term. `ExampleEmojiPlugin` in `quickshell/PLUGINS/` demonstrates the full setup. Registry plugins can apply for central POEditor translation via the registry's CONTRIBUTING guide.
## Step 11: Validate and Test
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`
3. Open Settings > Plugins > Scan for Plugins
4. Enable your plugin and add it to the DankBar layout
**Runtime plugin discovery via IPC:**
Plugins can be scanned, rescanned, and reloaded at runtime without restarting the shell:
```bash
dms ipc plugin-scan scan # Trigger a full rescan of all plugin directories
dms ipc plugin-scan rescan <id> # Force rescan of a specific plugin
dms ipc plugin-scan reload <id> # Force reload of a loaded plugin
dms ipc plugin-scan list # List all known plugins (TSV: id, loaded, type, name)
dms ipc plugin-scan status <id> # Get status of a specific plugin (TSV: loaded, type, error)
```
Plugin IDs are validated against `^[a-zA-Z0-9_\-:]{1,64}$`.
**Common issues:**
- Plugin not detected: check plugin.json syntax with `jq . plugin.json`
- Widget not showing: ensure it's enabled AND added to a DankBar section
- Settings error: verify `settings_write` permission is declared
- Data not persisting: check pluginService injection and permissions
- Startup check failing: check `pluginService.pluginLoadErrors` or run `dms ipc plugin-scan status <id>`
## Common Mistakes
1. **Missing `settings_write` permission** - Settings UI shows error without it
2. **Missing `property var popoutService: null`** - Must declare for injection to work
3. **Missing vertical bar pill** - Widget disappears when bar is on left/right edge
4. **Hardcoded colors** - Use `Theme.*` properties, not hex values
5. **Using `globalThis.clipboard`** - Does not exist; use `Quickshell.execDetached(["dms", "cl", "copy", text])`
6. **Wrong Theme property names** - `Theme.fontSizeS` does not exist, use `Theme.fontSizeSmall`
7. **Wrong import for Quickshell** - Use `import Quickshell` (not `import QtQuick` for execDetached)
8. **Forgetting `categories` in launcher items** - Items won't display without it
9. **Not handling null pluginService** - Always use optional chaining or null checks
10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`
11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`
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
## Quick Reference: Imports
**Widget / Daemon:**
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
```
**Launcher:**
```qml
import QtQuick
import qs.Services
```
**Desktop:**
```qml
import QtQuick
import qs.Common
```
**For clipboard/exec:** `import Quickshell`
**For processes:** `import Quickshell.Io`
**For networking:** `import Quickshell.Networking`
**For toast notifications:** access `ToastService` from `qs.Services`
## Quick Reference: File Naming
- **Directory name:** PascalCase (e.g., `MyAwesomePlugin/`)
- **Plugin ID:** camelCase (e.g., `myAwesomePlugin`)
- **QML files:** PascalCase (e.g., `MyWidget.qml`, `Settings.qml`)
- **Component paths in manifest:** relative with `./` prefix (e.g., `"./MyWidget.qml"`)
- **JS utility files:** camelCase (e.g., `utils.js`, `apiAdapter.js`)
## Reference Files
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
- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar pills, popouts, click actions, CC integration
- [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
- [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution
- [settings-components-reference.md](references/settings-components-reference.md) - All 7 setting components with complete property lists
- [theme-reference.md](references/theme-reference.md) - Theme colors, spacing, fonts, radii, common patterns
- [data-persistence-guide.md](references/data-persistence-guide.md) - pluginData, state API, global variables
- [popout-service-reference.md](references/popout-service-reference.md) - PopoutService API for controlling shell popouts and modals
- [advanced-patterns.md](references/advanced-patterns.md) - Variants, JS utilities, qmldir, IPC, multi-file plugins
@@ -0,0 +1,177 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://danklinux.com/schemas/plugin.json",
"title": "DankMaterialShell Plugin Manifest",
"description": "Schema for DankMaterialShell plugin.json manifest files",
"type": "object",
"required": [
"id",
"name",
"description",
"version",
"author",
"type",
"capabilities"
],
"properties": {
"id": {
"type": "string",
"description": "Unique plugin identifier (camelCase, no spaces)",
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
},
"name": {
"type": "string",
"description": "Human-readable plugin name",
"minLength": 1
},
"description": {
"type": "string",
"description": "Short description of plugin functionality",
"minLength": 1
},
"version": {
"type": "string",
"description": "Semantic version string (e.g., '1.0.0')",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
},
"author": {
"type": "string",
"description": "Plugin creator name or email",
"minLength": 1
},
"type": {
"type": "string",
"description": "Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.",
"enum": ["widget", "daemon", "launcher", "desktop", "composite"]
},
"capabilities": {
"type": "array",
"description": "Array of plugin capabilities",
"items": {
"type": "string"
},
"minItems": 1
},
"component": {
"type": "string",
"description": "Relative path to main QML component file. Required unless 'components' is provided.",
"pattern": "^\\./.*\\.qml$"
},
"components": {
"type": "object",
"description": "Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.",
"properties": {
"widget": {
"type": "string",
"description": "Bar/Control Center widget component (PluginComponent)",
"pattern": "^\\./.*\\.qml$"
},
"desktop": {
"type": "string",
"description": "Desktop widget component",
"pattern": "^\\./.*\\.qml$"
},
"daemon": {
"type": "string",
"description": "Background daemon component (instantiated once)",
"pattern": "^\\./.*\\.qml$"
},
"launcher": {
"type": "string",
"description": "Launcher provider component (requires 'trigger')",
"pattern": "^\\./.*\\.qml$"
}
},
"additionalProperties": false,
"minProperties": 1
},
"trigger": {
"type": "string",
"description": "Trigger string for launcher activation (required for launcher type)"
},
"icon": {
"type": "string",
"description": "Material Design icon name"
},
"settings": {
"type": "string",
"description": "Path to settings component QML file",
"pattern": "^\\./.*\\.qml$"
},
"startupCheck": {
"type": "string",
"description": "Path to a non-visual (QtObject) component exposing a check(done) function that gates activation. done(null) allows; done(error) blocks, where error is a string or { title, details }.",
"pattern": "^\\./.*\\.qml$"
},
"requires_dms": {
"type": "string",
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
},
"dependencies": {
"type": "array",
"description": "Array of required system tools/dependencies (registry metadata)",
"items": {
"type": "string"
}
},
"requires": {
"type": "array",
"description": "Deprecated alias for 'dependencies'.",
"items": {
"type": "string"
}
},
"permissions": {
"type": "array",
"description": "Required capabilities",
"items": {
"type": "string",
"enum": [
"settings_read",
"settings_write",
"process",
"network"
]
}
}
},
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "launcher"
}
}
},
"then": {
"required": ["trigger"]
}
},
{
"if": {
"required": ["components"],
"properties": {
"components": {
"required": ["launcher"]
}
}
},
"then": {
"required": ["trigger"]
}
},
{
"anyOf": [
{
"required": ["component"]
},
{
"required": ["components"]
}
]
}
],
"additionalProperties": true
}
@@ -0,0 +1,48 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
property var popoutService: null
// TODO: Read configuration from settings
property string configValue: pluginData?.configValue || ""
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId !== pluginId) return
configValue = pluginService.loadPluginData(pluginId, "configValue", "")
}
}
// TODO: Connect to the service events you need
// Connections {
// target: SessionData
// function onWallpaperPathChanged() {
// console.log("[MyDaemon] Wallpaper changed:", SessionData.wallpaperPath)
// handleEvent(SessionData.wallpaperPath)
// }
// }
function handleEvent(data) {
Proc.runCommand(
"myDaemon.handle",
["echo", "Event received:", data],
(stdout, exitCode) => {
if (exitCode === 0) {
console.log("[MyDaemon] Output:", stdout)
} else {
console.error("[MyDaemon] Failed:", exitCode)
ToastService?.showInfo("Daemon action failed")
}
}
)
}
Component.onCompleted: {
console.log("[MyDaemon] Started")
}
}
@@ -0,0 +1,16 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myDaemon"
StringSetting {
settingKey: "configValue"
label: "Configuration"
description: "Value used by the daemon"
placeholder: "Enter value..."
defaultValue: ""
}
}
@@ -0,0 +1,13 @@
{
"id": "myDaemon",
"name": "My Daemon",
"description": "A background service that reacts to events",
"version": "1.0.0",
"author": "Your Name",
"type": "daemon",
"capabilities": ["background-service"],
"component": "./Daemon.qml",
"icon": "settings",
"settings": "./Settings.qml",
"permissions": ["settings_read", "settings_write", "process"]
}
@@ -0,0 +1,18 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myDesktopWidget"
SliderSetting {
settingKey: "opacity"
label: "Opacity"
description: "Widget background opacity"
defaultValue: 85
minimum: 10
maximum: 100
unit: "%"
}
}
@@ -0,0 +1,47 @@
import QtQuick
import qs.Common
Item {
id: root
property var pluginService: null
property string pluginId: ""
property bool editMode: false
property real widgetWidth: 200
property real widgetHeight: 200
property real minWidth: 150
property real minHeight: 150
// TODO: Load settings reactively
property real bgOpacity: {
if (!pluginService) return 0.85
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
return val / 100
}
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId !== pluginId) return
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
bgOpacity = val / 100
}
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: root.bgOpacity
border.color: root.editMode ? Theme.primary : "transparent"
border.width: root.editMode ? 2 : 0
// TODO: Add your widget content here
Text {
anchors.centerIn: parent
text: "Desktop Widget"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
}
}
}
@@ -0,0 +1,13 @@
{
"id": "myDesktopWidget",
"name": "My Desktop Widget",
"description": "A custom desktop widget",
"version": "1.0.0",
"author": "Your Name",
"type": "desktop",
"capabilities": ["desktop-widget"],
"component": "./Widget.qml",
"icon": "widgets",
"settings": "./Settings.qml",
"permissions": ["settings_read", "settings_write"]
}
@@ -0,0 +1,59 @@
import QtQuick
import Quickshell
import qs.Services
Item {
id: root
property var pluginService: null
property string trigger: "#"
signal itemsChanged()
// TODO: Define your items
property var allItems: [
{
name: "Example Item",
icon: "material:star",
comment: "An example launcher item",
action: "toast:Hello from my launcher!",
categories: ["MyLauncher"]
}
]
function getItems(query) {
if (!query || query.length === 0) return allItems
var q = query.toLowerCase()
return allItems.filter(function(item) {
return item.name.toLowerCase().includes(q) ||
item.comment.toLowerCase().includes(q)
})
}
function executeItem(item) {
var actionParts = item.action.split(":")
var actionType = actionParts[0]
var actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "toast":
if (typeof ToastService !== "undefined")
ToastService.showInfo(actionData)
break
case "copy":
Quickshell.execDetached(["dms", "cl", "copy", actionData])
if (typeof ToastService !== "undefined")
ToastService.showInfo("Copied to clipboard")
break
default:
console.warn("Unknown action type:", actionType)
}
}
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")
}
}
}
@@ -0,0 +1,23 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myLauncher"
StringSetting {
settingKey: "trigger"
label: "Trigger"
description: "Type this prefix in the launcher to activate the plugin"
placeholder: "#"
defaultValue: "#"
}
ToggleSetting {
settingKey: "noTrigger"
label: "Always Visible"
description: "Show items alongside regular apps without needing a trigger"
defaultValue: false
}
}
@@ -0,0 +1,14 @@
{
"id": "myLauncher",
"name": "My Launcher",
"description": "Custom launcher plugin with searchable items",
"version": "1.0.0",
"author": "Your Name",
"type": "launcher",
"capabilities": ["launcher"],
"component": "./Launcher.qml",
"trigger": "#",
"icon": "search",
"settings": "./Settings.qml",
"permissions": ["settings_read", "settings_write"]
}
@@ -0,0 +1,23 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myWidget"
StringSetting {
settingKey: "text"
label: "Display Text"
description: "Text shown in the bar widget"
placeholder: "Hello"
defaultValue: "Hello"
}
ToggleSetting {
settingKey: "showIcon"
label: "Show Icon"
description: "Display an icon next to the text"
defaultValue: true
}
}
@@ -0,0 +1,75 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property var popoutService: null
// TODO: Read settings reactively
property string displayText: pluginData?.text || "Hello"
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId !== pluginId) return
displayText = pluginService.loadPluginData(pluginId, "text", "Hello")
}
}
horizontalBarPill: Component {
StyledRect {
width: label.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: label
anchors.centerIn: parent
text: root.displayText
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
}
}
verticalBarPill: Component {
StyledRect {
width: parent.widgetThickness
height: label.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: label
anchors.centerIn: parent
text: root.displayText
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
rotation: 90
}
}
}
// TODO: Uncomment and customize popout content
// popoutWidth: 350
// popoutHeight: 300
// popoutContent: Component {
// PopoutComponent {
// headerText: "My Widget"
// showCloseButton: true
//
// Column {
// width: parent.width
// spacing: Theme.spacingM
//
// StyledText {
// text: "Popout content here"
// color: Theme.surfaceText
// }
// }
// }
// }
}
@@ -0,0 +1,13 @@
{
"id": "myWidget",
"name": "My Widget",
"description": "A custom bar widget",
"version": "1.0.0",
"author": "Your Name",
"type": "widget",
"capabilities": ["dankbar-widget"],
"component": "./Widget.qml",
"icon": "extension",
"settings": "./Settings.qml",
"permissions": ["settings_read", "settings_write"]
}
@@ -0,0 +1,320 @@
# Advanced Patterns
Patterns observed in production DMS plugins that go beyond the basics.
## Plugin Variants
Create multiple widget instances from a single plugin definition. Each variant has its own configuration.
### Manifest
No special manifest changes needed - the variant system is built into PluginComponent.
### Widget with Variant Support
```qml
PluginComponent {
property string variantId: ""
property var variantData: ({})
property string displayText: variantData?.text || "Default"
horizontalBarPill: Component {
StyledRect {
width: label.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
StyledText {
id: label
anchors.centerIn: parent
text: root.displayText
}
}
}
}
```
Widget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)
### Settings with Variant Management
```qml
PluginSettings {
pluginId: "exampleVariants"
// Variant creation UI
DankButton {
text: "Add New Instance"
onClicked: {
var id = "variant_" + Date.now()
root.createVariant(id, { name: "New Instance", text: "Hello" })
}
}
// Per-variant configuration
Repeater {
model: root.variants
delegate: Column {
StringSetting {
settingKey: modelData.id + "_text"
label: modelData.name || modelData.id
}
}
}
}
```
## JavaScript Utility Files
For complex logic, split into `.js` files:
### utils.js
```javascript
.pragma library
function formatDuration(ms) {
if (ms < 60000) return "just now"
if (ms < 3600000) return Math.floor(ms / 60000) + "m ago"
return Math.floor(ms / 3600000) + "h ago"
}
function parseResponse(json) {
try {
return JSON.parse(json)
} catch (e) {
return null
}
}
```
### Using in QML
```qml
import "utils.js" as Utils
Item {
StyledText {
text: Utils.formatDuration(Date.now() - timestamp)
}
}
```
The `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.
## qmldir for Singleton Services
For plugins with internal singleton services:
### qmldir
```
singleton MyService 1.0 MyService.qml
```
### MyService.qml
```qml
pragma Singleton
import QtQuick
QtObject {
property var cache: ({})
function getData(key) {
return cache[key] || null
}
function setData(key, value) {
cache[key] = value
}
}
```
### Using the singleton
```qml
import "." as Local
Item {
Component.onCompleted: {
Local.MyService.setData("key", "value")
}
}
```
## Inline Component Declarations
Reusable sub-components defined inline:
```qml
Item {
component StatusBadge: Rectangle {
property string label: ""
property color badgeColor: Theme.primary
width: badgeText.implicitWidth + Theme.spacingM * 2
height: 24
radius: 12
color: badgeColor
StyledText {
id: badgeText
anchors.centerIn: parent
text: label
color: Theme.onPrimary
font.pixelSize: Theme.fontSizeSmall
}
}
Row {
spacing: Theme.spacingS
StatusBadge { label: "Running"; badgeColor: Theme.success }
StatusBadge { label: "Stopped"; badgeColor: Theme.error }
}
}
```
## Multi-Provider Adapter Pattern
For plugins supporting multiple backends (AI providers, API services):
### apiAdapters.js
```javascript
.pragma library
function createAdapter(provider) {
switch (provider) {
case "openai": return {
url: "https://api.openai.com/v1/chat/completions",
headers: (key) => ({ "Authorization": "Bearer " + key }),
formatRequest: (messages) => JSON.stringify({ model: "gpt-4", messages: messages }),
parseResponse: (text) => JSON.parse(text).choices[0].message.content
}
case "anthropic": return {
url: "https://api.anthropic.com/v1/messages",
headers: (key) => ({ "x-api-key": key, "anthropic-version": "2023-06-01" }),
formatRequest: (messages) => JSON.stringify({ model: "claude-sonnet-4-20250514", messages: messages }),
parseResponse: (text) => JSON.parse(text).content[0].text
}
default: return null
}
}
```
## IPC Integration
For plugins that respond to keyboard shortcuts or external commands:
```qml
PluginComponent {
Connections {
target: DMSIpc
function onCommandReceived(command, args) {
if (command === "myPlugin.toggle") {
doToggle()
} else if (command === "myPlugin.next") {
goNext()
}
}
}
}
```
External trigger: `dms ipc call myPlugin.toggle`
## Networking with Quickshell.Networking
For API calls using the built-in networking module:
```qml
import Quickshell.Networking
Item {
NetworkRequest {
id: request
url: "https://api.example.com/data"
method: "GET"
onResponseReceived: (response) => {
const data = JSON.parse(response.body)
processData(data)
}
onErrorOccurred: (error) => {
console.error("Network error:", error)
}
}
function fetchData() {
request.send()
}
}
```
## Toast Notifications
Show user feedback:
```qml
import qs.Services
// Info toast
ToastService?.showInfo("Operation completed")
// With title
ToastService?.showInfo("Plugin Name", "Data refreshed successfully")
```
Always use optional chaining since ToastService may not be available in all contexts.
## Clipboard Operations
```qml
import Quickshell
function copyToClipboard(text) {
Quickshell.execDetached(["dms", "cl", "copy", text])
ToastService?.showInfo("Copied to clipboard")
}
```
Do NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.
## Multi-File Plugin Architecture
Large plugins can be split across multiple files:
```
MyPlugin/
plugin.json
Main.qml # Main widget component
Settings.qml # Settings UI
DetailView.qml # Popout detail view
utils.js # Utility functions
apiAdapter.js # API adapter layer
qmldir # Optional: singleton registrations
```
Import sibling files:
```qml
// In Main.qml
import "." as Local
Item {
Loader {
source: "DetailView.qml"
}
}
```
## Performance Tips
1. Use `Proc.runCommand` with appropriate debounce for external commands
2. Pre-cache images and thumbnails for image-heavy plugins
3. Limit concurrent network requests
4. Use `Timer` with reasonable intervals (don't poll faster than needed)
5. Lazy-load heavy content (use `Loader` for complex popout content)
6. Avoid blocking the UI thread with synchronous operations
@@ -0,0 +1,272 @@
# Daemon Plugin Guide
Daemon plugins are invisible background services that react to events and execute actions. They have no bar pills or desktop presence.
## Base Component
Daemons use `PluginComponent` with no bar pills:
```qml
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
property var popoutService: null
// Event-driven logic goes here
}
```
## When to Use Daemons
- Monitor system events (wallpaper changes, battery level, notifications)
- Run periodic background tasks (polling APIs, checking system state)
- Execute scripts in response to events
- Control shell UI via PopoutService based on conditions
## Event-Driven Pattern
Use `Connections` to react to service signals:
```qml
PluginComponent {
property var popoutService: null
Connections {
target: SessionData
function onWallpaperPathChanged() {
console.log("Wallpaper changed to:", SessionData.wallpaperPath)
runScript(SessionData.wallpaperPath)
}
}
Connections {
target: BatteryService
function onPercentageChanged() {
if (BatteryService.percentage < 10 && !BatteryService.isCharging) {
popoutService?.openBattery()
}
}
}
}
```
## Available Services
Common services daemons can connect to:
| Service | Signals/Properties | Description |
|---------|-------------------|-------------|
| `SessionData` | `wallpaperPath`, `onWallpaperPathChanged` | Desktop session state |
| `BatteryService` | `percentage`, `isCharging`, `batteryAvailable` | Battery status |
| `NotificationService` | `onNotificationReceived(notification)` | Desktop notifications |
| `PluginService` | `onPluginLoaded`, `onGlobalVarChanged` | Plugin lifecycle |
Import services from `qs.Services`.
## Process Execution
### Simple command with Proc
```qml
import qs.Common
PluginComponent {
function runScript(arg) {
Proc.runCommand(
"myDaemon.script",
["bash", "-c", "echo 'Processing: " + arg + "'"],
(stdout, exitCode) => {
if (exitCode === 0) {
console.log("Script output:", stdout)
} else {
ToastService?.showInfo("Script failed: exit " + exitCode)
}
}
)
}
}
```
### Long-running process with Process component
```qml
import Quickshell.Io
PluginComponent {
property string scriptPath: ""
Process {
id: proc
command: ["bash", scriptPath]
running: false
stdout: StdioCollector {
onTextReceived: (text) => {
console.log("stdout:", text)
}
}
stderr: StdioCollector {
onTextReceived: (text) => {
console.error("stderr:", text)
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
ToastService?.showInfo("Process failed: exit " + exitCode)
}
}
}
function startProcess() {
if (scriptPath && !proc.running) {
proc.running = true
}
}
}
```
## Timer-Based Polling
```qml
PluginComponent {
Timer {
interval: 60000 // every minute
running: true
repeat: true
onTriggered: checkStatus()
}
function checkStatus() {
Proc.runCommand(
"myDaemon.check",
["sh", "-c", "systemctl is-active myservice"],
(stdout, exitCode) => {
const active = stdout.trim() === "active"
PluginService.setGlobalVar("myDaemon", "serviceActive", active)
}
)
}
}
```
## Data Persistence
Daemons access PluginService directly (it's injected via PluginComponent):
```qml
PluginComponent {
property string configuredScript: pluginData?.scriptPath || ""
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId === pluginId) {
configuredScript = pluginService.loadPluginData(pluginId, "scriptPath", "")
}
}
}
}
```
## PopoutService Usage
Daemons can control shell UI via the injected popoutService:
```qml
PluginComponent {
property var popoutService: null
function showAlert() {
popoutService?.openNotificationCenter()
}
function openSettings() {
popoutService?.openSettings()
}
}
```
See [popout-service-reference.md](popout-service-reference.md) for the full API.
## Complete Example
Based on the WallpaperWatcherDaemon:
```qml
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
property var popoutService: null
property string scriptPath: pluginData?.scriptPath || ""
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId === pluginId) {
scriptPath = pluginService.loadPluginData(pluginId, "scriptPath", "")
}
}
}
Connections {
target: SessionData
function onWallpaperPathChanged() {
if (scriptPath) {
runWallpaperScript(SessionData.wallpaperPath)
}
}
}
function runWallpaperScript(wallpaperPath) {
console.log("[WallpaperWatcher] Running script:", scriptPath, wallpaperPath)
Proc.runCommand(
"wallpaperWatcher.run",
["bash", scriptPath, wallpaperPath],
(stdout, exitCode) => {
if (exitCode === 0) {
console.log("[WallpaperWatcher] Script output:", stdout)
} else {
console.error("[WallpaperWatcher] Script failed:", exitCode)
ToastService?.showInfo("Wallpaper script failed")
}
}
)
}
Component.onCompleted: {
console.log("[WallpaperWatcher] Daemon started")
}
}
```
## Manifest Example
```json
{
"id": "wallpaperWatcher",
"name": "Wallpaper Watcher",
"description": "Runs a script when the wallpaper changes",
"version": "1.0.0",
"author": "Developer",
"type": "daemon",
"capabilities": ["wallpaper-automation"],
"component": "./WallpaperWatcher.qml",
"icon": "wallpaper",
"settings": "./Settings.qml",
"permissions": ["settings_read", "settings_write", "process"]
}
```
@@ -0,0 +1,186 @@
# Data Persistence Guide
DMS plugins have three tiers of data persistence, each suited for different use cases.
## Tier 1: Plugin Data (Settings)
Persisted to `settings.json`. Use for user preferences and configuration.
### Saving
```qml
pluginService.savePluginData(pluginId, "key", value)
```
### Loading
```qml
var value = pluginService.loadPluginData(pluginId, "key", defaultValue)
```
### Reactive Access via pluginData
`PluginComponent` has a reactive `pluginData` property that auto-loads from settings:
```qml
PluginComponent {
property string displayText: pluginData?.text || "Default"
property bool showIcon: pluginData?.showIcon !== undefined ? pluginData.showIcon : true
}
```
### Reacting to Settings Changes
When settings are changed (e.g., from the settings UI), react with `Connections`:
```qml
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId !== pluginId) return
displayText = pluginService.loadPluginData(pluginId, "text", "Default")
showIcon = pluginService.loadPluginData(pluginId, "showIcon", true)
}
}
```
## Tier 2: Plugin State
Persisted to a separate state file. Use for runtime state that should survive restarts but is not user-configurable (history, cache, counters).
### Saving
```qml
pluginService.savePluginState(pluginId, "key", value)
```
### Loading
```qml
var state = pluginService.loadPluginState(pluginId, "key", defaultValue)
```
### Additional Methods
```qml
pluginService.clearPluginState(pluginId)
pluginService.removePluginStateKey(pluginId, "key")
```
### Example: Persistent History
```qml
Item {
property var history: []
Component.onCompleted: {
history = pluginService?.loadPluginState(pluginId, "history", []) || []
}
function addToHistory(entry) {
history.unshift({
text: entry,
timestamp: Date.now()
})
if (history.length > 100) history = history.slice(0, 100)
pluginService?.savePluginState(pluginId, "history", history)
}
function clearHistory() {
history = []
pluginService?.removePluginStateKey(pluginId, "history")
}
}
```
## Tier 3: Global Variables (Runtime Only)
NOT persisted. Shared across all instances of a plugin. Use for cross-instance state synchronization (multi-monitor consistency, multi-instance widgets).
### Using PluginGlobalVar Component
```qml
import qs.Modules.Plugins
PluginComponent {
PluginGlobalVar {
id: globalCounter
varName: "counter"
defaultValue: 0
}
horizontalBarPill: Component {
StyledRect {
// ...
StyledText {
text: "Count: " + globalCounter.value
}
MouseArea {
onClicked: globalCounter.set(globalCounter.value + 1)
}
}
}
}
```
**PluginGlobalVar properties:**
| Property | Type | Description |
|----------|------|-------------|
| `varName` | string | Required: name of the global variable |
| `defaultValue` | any | Optional: default if not set |
| `value` | any | Readonly: current value |
**Methods:**
- `set(newValue)` - update the value (triggers reactivity across all instances)
### Using PluginService API Directly
```qml
import qs.Services
property int counter: PluginService.getGlobalVar("myPlugin", "counter", 0)
Connections {
target: PluginService
function onGlobalVarChanged(pluginId, varName) {
if (pluginId === "myPlugin" && varName === "counter") {
counter = PluginService.getGlobalVar("myPlugin", "counter", 0)
}
}
}
function increment() {
var current = PluginService.getGlobalVar("myPlugin", "counter", 0)
PluginService.setGlobalVar("myPlugin", "counter", current + 1)
}
```
## Decision Matrix
| Need | API | Persisted | Scope |
|------|-----|-----------|-------|
| User preferences (API keys, themes, intervals) | `savePluginData` / `loadPluginData` | Yes (settings.json) | Per plugin |
| Runtime state (history, cache, counters) | `savePluginState` / `loadPluginState` | Yes (state file) | Per plugin |
| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |
| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |
## Plugin Path
Retrieve a plugin's installation directory at runtime:
```qml
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.
## Important Notes
1. **pluginData is reactive** - bindings update automatically when data changes
2. **Global vars are NOT persistent** - they reset when the shell restarts
3. **State vs Data** - data is for user-facing settings, state is for internal runtime data
4. **Null safety** - always check `pluginService` is not null before calling methods
5. **Signal namespacing** - global var signals include `pluginId` to filter for your plugin
6. **Performance** - global vars are efficient for frequent updates; settings writes are batched
@@ -0,0 +1,240 @@
# Desktop Plugin Guide
Desktop plugins are widgets that appear on the desktop background layer. They support drag-and-drop positioning and resize via corner handles.
## Base Component
Desktop widgets use a plain `Item` with injected properties:
```qml
import QtQuick
import qs.Common
Item {
id: root
property var pluginService: null
property string pluginId: ""
property bool editMode: false
property real widgetWidth: 200
property real widgetHeight: 200
property real minWidth: 150
property real minHeight: 150
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: 0.85
// Your content here
}
}
```
## Injected Properties
These are set automatically by the DesktopPluginWrapper:
| Property | Type | Description |
|----------|------|-------------|
| `pluginService` | object | PluginService reference for data persistence |
| `pluginId` | string | Plugin's unique identifier |
| `editMode` | bool | `true` when user is dragging/resizing |
| `widgetWidth` | real | Current widget container width |
| `widgetHeight` | real | Current widget container height |
## Optional Properties
Define these on your root item to customize behavior:
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `minWidth` | real | 100 | Minimum allowed width during resize |
| `minHeight` | real | 100 | Minimum allowed height during resize |
## Position and Size Persistence
Position (`desktopX`, `desktopY`) and size (`desktopWidth`, `desktopHeight`) are automatically managed by the DesktopPluginWrapper. You do not need to handle persistence for positioning.
## Edit Mode
When `editMode` is true, the user is repositioning or resizing. Use this to:
- Show visual indicators (borders, handles)
- Disable interactive elements to prevent accidental actions
- Display additional controls
```qml
Rectangle {
anchors.fill: parent
border.color: root.editMode ? Theme.primary : "transparent"
border.width: root.editMode ? 2 : 0
MouseArea {
anchors.fill: parent
enabled: !root.editMode
onClicked: doSomething()
}
}
```
## Loading and Saving Data
Use the injected `pluginService` for data persistence:
```qml
property string displayMode: {
if (!pluginService) return "default"
return pluginService.loadPluginData(pluginId, "displayMode", "default")
}
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId !== pluginId) return
root.displayMode = pluginService.loadPluginData(pluginId, "displayMode", "default")
}
}
function saveMode(mode) {
pluginService?.savePluginData(pluginId, "displayMode", mode)
}
```
## Settings Component
Desktop plugin settings use the same `PluginSettings` wrapper as other types:
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myDesktopWidget"
SliderSetting {
settingKey: "opacity"
label: "Opacity"
description: "Widget background opacity"
defaultValue: 85
minimum: 10
maximum: 100
unit: "%"
}
SelectionSetting {
settingKey: "style"
label: "Display Style"
options: [
{ label: "Compact", value: "compact" },
{ label: "Expanded", value: "expanded" }
]
defaultValue: "compact"
}
}
```
## User Interaction
Desktop widgets support:
1. **Drag** - click and drag anywhere (in edit mode)
2. **Resize** - drag bottom-right corner handle (in edit mode)
3. **Edit mode toggle** - via the desktop edit button
## Complete Example
Based on the ExampleDesktopClock pattern:
```qml
import QtQuick
import qs.Common
Item {
id: root
property var pluginService: null
property string pluginId: ""
property bool editMode: false
property real widgetWidth: 250
property real widgetHeight: 250
property real minWidth: 150
property real minHeight: 150
property string clockStyle: {
if (!pluginService) return "digital"
return pluginService.loadPluginData(pluginId, "clockStyle", "digital")
}
property real bgOpacity: {
if (!pluginService) return 0.85
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
return val / 100
}
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId !== pluginId) return
clockStyle = pluginService.loadPluginData(pluginId, "clockStyle", "digital")
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
bgOpacity = val / 100
}
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: root.bgOpacity
border.color: root.editMode ? Theme.primary : "transparent"
border.width: root.editMode ? 2 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatTime(new Date(), "hh:mm:ss")
color: Theme.surfaceText
font.pixelSize: root.widgetWidth * 0.15
font.weight: Font.Bold
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatDate(new Date(), "ddd, MMM d")
color: Theme.onSurfaceVariant
font.pixelSize: Theme.fontSizeMedium
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: root.widgetWidth = root.widgetWidth // force update
}
}
```
## Manifest Example
```json
{
"id": "myDesktopClock",
"name": "Desktop Clock",
"description": "Analog and digital clock for the desktop",
"version": "1.0.0",
"author": "Developer",
"type": "desktop",
"capabilities": ["desktop-widget"],
"component": "./ClockWidget.qml",
"icon": "schedule",
"settings": "./Settings.qml",
"permissions": ["settings_read", "settings_write"]
}
```
@@ -0,0 +1,308 @@
# Launcher Plugin Guide
Launcher plugins extend the DMS launcher with custom searchable items and actions. They use trigger-based filtering and integrate directly into the app drawer.
## Base Component
Launchers use a plain `Item` (not PluginComponent):
```qml
import QtQuick
import qs.Services
Item {
id: root
property var pluginService: null
property string trigger: "#"
signal itemsChanged()
function getItems(query) {
// Return array of items
return []
}
function executeItem(item) {
// Handle item selection
}
}
```
## Required Interface
| Member | Type | Description |
|--------|------|-------------|
| `pluginService` | property | Injected PluginService reference (declare as `null`) |
| `trigger` | property | Trigger string for activation |
| `itemsChanged` | signal | Emit when item list changes (triggers UI refresh) |
| `getItems(query)` | function | Return array of items matching query |
| `executeItem(item)` | function | Handle item selection |
## Item Structure
Each item returned by `getItems()`:
```javascript
{
name: "Item Display Name", // Required: shown in launcher
icon: "material:star", // Optional: icon specification
comment: "Description text", // Required: subtitle text
action: "type:data", // Required: action identifier
categories: ["MyPlugin"], // Required: array with plugin category
imageUrl: "https://..." // Optional: image for tile view
}
```
## Icon Types
### 1. Material Design Icons
```javascript
{ icon: "material:lightbulb" }
{ icon: "material:terminal" }
{ icon: "material:translate" }
```
Uses the Material Symbols Rounded font.
### 2. Unicode / Emoji Icons
```javascript
{ icon: "unicode:smile_face" }
```
Rendered at 70-80% of icon size with theming.
### 3. Desktop Theme Icons
```javascript
{ icon: "firefox" }
{ icon: "folder" }
```
Uses the user's installed icon theme.
### 4. No Icon
Omit the `icon` field entirely. The launcher hides the icon area and gives full width to the item name.
## Trigger System
**Custom trigger** (items only appear when trigger is typed):
```json
{ "trigger": "#" }
```
- Type `#` alone: shows all plugin items
- Type `# query`: filters plugin items by query
- The query string (without trigger) is passed to `getItems(query)`
**No trigger** (items always visible alongside regular apps):
```json
{ "trigger": "" }
```
Save empty trigger at runtime:
```qml
Component.onCompleted: {
trigger = pluginService?.loadPluginData(pluginId, "trigger", "#") ?? "#"
}
```
## Action Execution
Parse action strings in `executeItem()`:
```qml
function executeItem(item) {
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "toast":
ToastService?.showInfo(actionData)
break
case "copy":
Quickshell.execDetached(["dms", "cl", "copy", actionData])
ToastService?.showInfo("Copied to clipboard")
break
case "exec":
Quickshell.execDetached(actionData.split(" "))
break
case "url":
Quickshell.execDetached(["xdg-open", actionData])
break
default:
console.warn("Unknown action type:", actionType)
}
}
```
## Search / Filtering
The `query` parameter in `getItems()` contains the user's search text (without the trigger prefix).
```qml
function getItems(query) {
const allItems = [
{ name: "Calculator", icon: "material:calculate",
comment: "Open calculator", action: "exec:gnome-calculator",
categories: ["Tools"] },
{ name: "Terminal", icon: "material:terminal",
comment: "Open terminal", action: "exec:alacritty",
categories: ["Tools"] }
]
if (!query || query.length === 0) return allItems
const q = query.toLowerCase()
return allItems.filter(item =>
item.name.toLowerCase().includes(q) ||
item.comment.toLowerCase().includes(q)
)
}
```
## Context Menu Actions
Add right-click actions to launcher items:
```qml
function getContextMenuActions(item) {
return [
{ name: "Copy", icon: "material:content_copy",
action: "copy:" + item.name },
{ name: "Open in Browser", icon: "material:open_in_new",
action: "url:" + item.url }
]
}
```
Context menu actions use the same `executeItem()` handler.
## Image Tile View
For image-heavy launchers (GIF search, sticker pickers), use tile view:
In `plugin.json`:
```json
{
"viewMode": "tile",
"viewModeEnforced": true
}
```
In items:
```javascript
{
name: "Image Title",
imageUrl: "https://example.com/image.png",
comment: "Description",
action: "copy:https://example.com/image.png",
categories: ["MyPlugin"]
}
```
## State Persistence
For plugins with persistent state (notes, history, favorites):
```qml
property var notes: []
Component.onCompleted: {
const saved = pluginService?.loadPluginState(pluginId, "notes", [])
if (saved) notes = saved
}
function addNote(text) {
notes.push({ text: text, timestamp: Date.now() })
pluginService?.savePluginState(pluginId, "notes", notes)
itemsChanged()
}
```
Use `savePluginState/loadPluginState` for runtime data and `savePluginData/loadPluginData` for user preferences.
## Settings for Trigger Configuration
Provide a PluginSettings component for trigger customization:
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myLauncher"
StringSetting {
settingKey: "trigger"
label: "Trigger"
description: "Type this prefix to activate the launcher plugin"
placeholder: "#"
defaultValue: "#"
}
ToggleSetting {
settingKey: "noTrigger"
label: "Always Visible"
description: "Show items alongside regular apps (no trigger needed)"
defaultValue: false
}
}
```
## Complete Example
```qml
import QtQuick
import Quickshell
import qs.Services
Item {
id: root
property var pluginService: null
property string trigger: "!"
signal itemsChanged()
property var commands: [
{ name: "Lock Screen", icon: "material:lock",
comment: "Lock the session", action: "exec:loginctl lock-session" },
{ name: "Screenshot", icon: "material:screenshot_monitor",
comment: "Take a screenshot", action: "exec:grim" },
{ name: "File Manager", icon: "material:folder",
comment: "Open file manager", action: "exec:nautilus" }
]
function getItems(query) {
if (!query) return commands
const q = query.toLowerCase()
return commands.filter(c =>
c.name.toLowerCase().includes(q) ||
c.comment.toLowerCase().includes(q)
)
}
function executeItem(item) {
const [type, ...rest] = item.action.split(":")
const data = rest.join(":")
if (type === "exec") {
Quickshell.execDetached(data.split(" "))
}
}
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("quickCommands", "trigger", "!")
}
}
}
```
@@ -0,0 +1,188 @@
# Plugin Manifest Reference (plugin.json)
## Required Fields
| Field | Type | Description | Validation |
|-------|------|-------------|------------|
| `id` | string | Unique plugin identifier | camelCase, pattern `^[a-zA-Z][a-zA-Z0-9]*$` |
| `name` | string | Human-readable name | 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.-]+)?$` |
| `author` | string | Creator name or email | Non-empty |
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `composite` |
| `capabilities` | array | Plugin capabilities | At least 1 string item |
One of `component` or `components` is required (not both):
| Field | Type | Description | Validation |
|-------|------|-------------|------------|
| `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` |
## Conditional Requirements
| Condition | Required Field | Description |
|-----------|---------------|-------------|
| `type: "launcher"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |
| `components` has `launcher` key | `trigger` | Same requirement applies to composite plugins with a launcher surface |
## Optional Fields
| Field | Type | Description |
|-------|------|-------------|
| `icon` | string | Material Design icon name (displayed in plugin list UI) |
| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |
| `startupCheck` | string | Path to a QtObject component that gates plugin activation via a `check(done)` function (must start with `./`, end with `.qml`). See Startup Check section below. |
| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\|<=?\|=\|>\|<)\d+\.\d+\.\d+$` |
| `dependencies` | array | System tool dependencies (e.g., `["curl", "jq"]`). Registry metadata. |
| `requires` | array | Deprecated alias for `dependencies` |
| `permissions` | array | Required permissions |
| `trigger` | string | Launcher trigger string (required for launcher type) |
## Permissions
| Permission | Description | Enforced |
|------------|-------------|----------|
| `settings_read` | Read plugin configuration | No (not currently enforced) |
| `settings_write` | Write plugin configuration / use PluginSettings | **Yes** |
| `process` | Execute system commands | No (not currently enforced) |
| `network` | Network access | No (not currently enforced) |
If your plugin has a `settings` component but does not declare `settings_write`, users will see an error instead of the settings UI.
## Capabilities
Capabilities are free-form strings that describe what the plugin does. Common values:
- `dankbar-widget` - general bar widget
- `control-center` - integrates with Control Center
- `monitoring` - system/service monitoring
- `launcher` - launcher search provider
- `desktop-widget` - desktop background widget
- `ai` - AI/LLM integration
- `slideout` - uses slideout panel
## Startup Check
The `startupCheck` field points to a non-visual `QtObject` component that gates plugin activation on dependency checks. The component must expose a `check(done)` function:
```qml
import QtQuick
import qs.Common
QtObject {
function check(done) {
Proc.runCommand("myPlugin.depCheck", ["sh", "-c", "command -v mytool"], (stdout, exitCode) => {
if (exitCode === 0) {
done(null);
return;
}
done({
"title": I18n.tr("mytool is required"),
"details": I18n.tr("Install 'mytool' and re-enable this plugin.")
});
});
}
}
```
The `done` callback accepts:
- `null` - allow activation
- A string - block with a short error message
- `{ title, details }` - block with a title and expandable details
A synchronous variant (no `done` parameter, return the result directly) is also supported.
Failed startup checks show a toast error and store the error in `pluginService.pluginLoadErrors`.
## Components (Composite Plugins)
The `components` field maps surface names to QML paths, allowing a single plugin to register multiple surfaces:
```json
{
"id": "myComposite",
"name": "My Composite Plugin",
"description": "Daemon + widget + desktop from one plugin",
"version": "1.0.0",
"author": "Developer Name",
"type": "composite",
"capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
"icon": "extension",
"components": {
"daemon": "./MyDaemon.qml",
"widget": "./MyBarWidget.qml",
"desktop": "./MyDesktopWidget.qml"
},
"settings": "./Settings.qml",
"permissions": ["settings_read", "settings_write"]
}
```
Valid surface keys: `widget`, `desktop`, `daemon`, `launcher`. Provide any subset. Each surface is loaded independently in the appropriate registry.
## Complete Example
```json
{
"id": "myPlugin",
"name": "My Plugin",
"description": "A sample plugin demonstrating all fields",
"version": "1.0.0",
"author": "Developer Name",
"type": "widget",
"capabilities": ["dankbar-widget", "control-center"],
"component": "./MyWidget.qml",
"icon": "extension",
"settings": "./Settings.qml",
"startupCheck": "./StartupCheck.qml",
"requires_dms": ">=0.1.18",
"dependencies": ["curl", "jq"],
"permissions": ["settings_read", "settings_write", "process", "network"]
}
```
## Launcher Example
```json
{
"id": "myLauncher",
"name": "My Launcher",
"description": "Search and execute custom actions",
"version": "1.0.0",
"author": "Developer Name",
"type": "launcher",
"capabilities": ["launcher"],
"component": "./MyLauncher.qml",
"trigger": "#",
"icon": "search",
"settings": "./Settings.qml",
"requires_dms": ">=0.1.18",
"permissions": ["settings_read", "settings_write"]
}
```
## JSON Schema
The complete JSON schema is available at `assets/plugin-schema.json` in this skill. Validate with:
```bash
# Using python
python3 -c "
import json, jsonschema
schema = json.load(open('path/to/plugin-schema.json'))
manifest = json.load(open('plugin.json'))
jsonschema.validate(manifest, schema)
print('Valid!')
"
# Using jq (syntax check only)
jq . plugin.json
```
## Additional Properties
The schema allows additional properties (`"additionalProperties": true`), so plugins can include custom fields. Common custom fields seen in production plugins:
- `viewMode` - launcher display mode (`"tile"` for image grids)
- `viewModeEnforced` - lock launcher to specific view mode (`true`/`false`)
@@ -0,0 +1,120 @@
# PopoutService Reference
The `PopoutService` singleton lets plugins control all DMS popouts and modals. It is automatically injected into widget, daemon, and settings components.
## Setup
Declare the property in your component for injection to work:
```qml
property var popoutService: null
```
Without this declaration, injection fails with: `Cannot assign to non-existent property "popoutService"`
## Popouts (DankPopout-based)
| Component | Open | Close | Toggle |
|-----------|------|-------|--------|
| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |
| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |
| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |
| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |
| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |
| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |
| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |
| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |
## Modals (DankModal-based)
| Modal | Show | Hide | Notes |
|-------|------|------|-------|
| Settings | `openSettings()` | `closeSettings()` | Full settings interface |
| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |
| Launcher | `openDankLauncherV2()` | `closeDankLauncherV2()` | Also has `toggleDankLauncherV2()` |
| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |
| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Has `toggleProcessListModal()` |
| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |
| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |
| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network auth |
| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |
## Slideouts
| Component | Open | Close | Toggle |
|-----------|------|-------|--------|
| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |
## Usage Examples
### Simple toggle
```qml
MouseArea {
onClicked: popoutService?.toggleControlCenter()
}
```
### Conditional popout
```qml
Connections {
target: BatteryService
function onPercentageChanged() {
if (BatteryService.percentage < 10 && !BatteryService.isCharging) {
popoutService?.openBattery()
}
}
}
```
### Context menu with multiple actions
```qml
MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) contextMenu.popup()
else popoutService?.toggleControlCenter()
}
}
Menu {
id: contextMenu
MenuItem { text: "Settings"; onClicked: popoutService?.openSettings() }
MenuItem { text: "Notifications"; onClicked: popoutService?.toggleNotificationCenter() }
MenuItem { text: "Power"; onClicked: popoutService?.openPowerMenu() }
}
```
### Position-aware toggle (from bar pill)
Some toggle functions accept position parameters for proper popout placement:
```qml
pillClickAction: (x, y, width, section, screen) => {
popoutService?.toggleControlCenter(x, y, width, section, screen)
}
```
## Best Practices
1. **Always use optional chaining** (`?.`) - the service may not be injected yet
2. **Check feature availability** before opening feature-specific popouts:
```qml
if (BatteryService.batteryAvailable) {
popoutService?.openBattery()
}
```
3. **Lazy loading** - first access may activate lazy loaders; this is normal
4. **Popouts are shared** - avoid opening conflicting popouts simultaneously
5. **User intent** - only trigger popouts from user actions or critical system events
6. **Multi-monitor** - positioned popouts are screen-aware when using position parameters
## Injection Locations
The service is injected at these points:
- `DMSShell.qml` - daemon plugins
- `WidgetHost.qml` - widget plugins in left/right bar sections
- `CenterSection.qml` - center bar widgets
- `PluginsTab.qml` - settings components
@@ -0,0 +1,273 @@
# Settings Components Reference
All plugin settings use the `PluginSettings` wrapper. Setting components auto-save on change and auto-load on creation.
## PluginSettings Wrapper
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "yourPlugin" // Required: must match plugin.json id
// Setting components go here
}
```
**Important:** The plugin must declare `"permissions": ["settings_write"]` in plugin.json for the settings UI to render. Without it, users see an error.
**PluginSettings provides to children:**
- `saveValue(key, value)` - save a setting value
- `loadValue(key, defaultValue)` - load a setting value
- `saveState(key, value)` - save plugin state (separate file)
- `loadState(key, defaultValue)` - load plugin state
- `clearState()` - clear all plugin state
- Variant management functions (for variant plugins)
## StringSetting
Text input field.
```qml
StringSetting {
settingKey: "apiKey" // Required: storage key
label: "API Key" // Required: display label
description: "Your API key" // Optional: help text
placeholder: "sk-..." // Optional: input placeholder
defaultValue: "" // Optional: default (default: "")
}
```
**Layout:** Vertical stack - label, description, input field.
## ToggleSetting
Boolean toggle switch.
```qml
ToggleSetting {
settingKey: "notifications" // Required: storage key
label: "Enable Notifications" // Required: display label
description: "Show alerts" // Optional: help text
defaultValue: true // Optional: default (default: false)
}
```
**Layout:** Horizontal - label/description on left, toggle on right.
## SelectionSetting
Dropdown menu.
```qml
SelectionSetting {
settingKey: "theme" // Required: storage key
label: "Theme" // Required: display label
description: "Color scheme" // Optional: help text
options: [ // Required: array of options
{ label: "Dark", value: "dark" },
{ label: "Light", value: "light" },
{ label: "Auto", value: "auto" }
]
defaultValue: "dark" // Optional: default value
}
```
Options can be `{ label, value }` objects or simple strings. Stores the `value` field, displays the `label` field.
**Layout:** Horizontal - label/description on left, dropdown on right.
**Reacting to changes:**
```qml
SelectionSetting {
settingKey: "updateInterval"
label: "Update Interval"
options: [
{ label: "1 minute", value: "60" },
{ label: "5 minutes", value: "300" }
]
defaultValue: "300"
onValueChanged: (newValue) => {
console.log("Interval changed to:", newValue)
}
}
```
## SliderSetting
Numeric slider with min/max.
```qml
SliderSetting {
settingKey: "opacity" // Required: storage key
label: "Opacity" // Required: display label
description: "Background" // Optional: help text
defaultValue: 85 // Optional: default value
minimum: 0 // Required: min value
maximum: 100 // Required: max value
unit: "%" // Optional: unit label shown after value
leftIcon: "dark_mode" // Optional: Material icon on left
rightIcon: "light_mode" // Optional: Material icon on right
}
```
## ColorSetting
Color picker.
```qml
ColorSetting {
settingKey: "accentColor" // Required: storage key
label: "Accent Color" // Required: display label
description: "Custom accent" // Optional: help text
defaultValue: "#ff5722" // Optional: default hex color
}
```
Displays a color swatch that opens a color picker dialog.
## ListSetting
Manage a list of items with manual add/remove. Use when you need custom UI for adding items.
```qml
ListSetting {
id: itemList
settingKey: "items" // Required: storage key
label: "Saved Items" // Required: display label
description: "Your items" // Optional: help text
defaultValue: [] // Optional: default array
delegate: Component { // Optional: custom item display
StyledRect {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData.name
color: Theme.surfaceText
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
width: 60
height: 28
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: "Remove"
color: Theme.errorText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: removeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: itemList.removeItem(index)
}
}
}
}
}
```
**Methods:**
- `addItem(item)` - add an item to the list
- `removeItem(index)` - remove item at index
## ListSettingWithInput
Complete list management with built-in form. Best for collecting structured data.
```qml
ListSettingWithInput {
settingKey: "locations" // Required: storage key
label: "Locations" // Required: display label
description: "Track zones" // Optional: help text
defaultValue: [] // Optional: default array
fields: [ // Required: field definitions
{
id: "name", // Required: key in saved object
label: "Name", // Required: column header
placeholder: "Home", // Optional: input placeholder
width: 150, // Optional: column width (default: 200)
required: true // Optional: must have value to add
},
{
id: "timezone",
label: "Timezone",
placeholder: "America/New_York",
width: 200,
required: true
}
]
}
```
Automatically generates: column headers, input fields, add button with validation, list display, remove buttons.
## Mixing Custom UI with Settings
You can interleave regular QML elements with setting components:
```qml
PluginSettings {
pluginId: "myPlugin"
StyledText {
width: parent.width
text: "General Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StringSetting {
settingKey: "name"
label: "Display Name"
}
StyledText {
width: parent.width
text: "Advanced Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
topPadding: Theme.spacingL
}
ToggleSetting {
settingKey: "debug"
label: "Debug Mode"
defaultValue: false
}
}
```
## Default Values
Define sensible defaults in every setting component. The default is used when no saved value exists:
```qml
StringSetting { settingKey: "text"; defaultValue: "Hello" }
ToggleSetting { settingKey: "enabled"; defaultValue: true }
SelectionSetting { settingKey: "mode"; defaultValue: "auto" }
SliderSetting { settingKey: "opacity"; defaultValue: 85 }
ColorSetting { settingKey: "color"; defaultValue: "#ff5722" }
ListSetting { settingKey: "items"; defaultValue: [] }
ListSettingWithInput { settingKey: "data"; defaultValue: [] }
```
@@ -0,0 +1,216 @@
# Theme Property Reference
All theme properties are accessed via the `Theme` singleton from `qs.Common`. Always use these instead of hardcoded values.
## Font Sizes
```qml
Theme.fontSizeSmall // 12px (scaled by SettingsData.fontScale)
Theme.fontSizeMedium // 14px (scaled)
Theme.fontSizeLarge // 16px (scaled)
Theme.fontSizeXLarge // 20px (scaled)
```
## Icon Sizes
```qml
Theme.iconSizeSmall // 16px
Theme.iconSize // 24px (default)
Theme.iconSizeLarge // 32px
```
## Spacing
```qml
Theme.spacingXS // Extra small
Theme.spacingS // Small
Theme.spacingM // Medium
Theme.spacingL // Large
Theme.spacingXL // Extra large
```
## Border Radius
```qml
Theme.cornerRadius // Standard
Theme.cornerRadiusSmall // Smaller
Theme.cornerRadiusLarge // Larger
```
## Surface Colors
```qml
Theme.surface
Theme.surfaceContainerLow
Theme.surfaceContainer
Theme.surfaceContainerHigh
Theme.surfaceContainerHighest
```
## Text Colors
```qml
Theme.onSurface // Primary text on surface
Theme.onSurfaceVariant // Secondary text on surface
Theme.surfaceText // Alias for primary surface text
Theme.surfaceVariantText // Alias for secondary surface text
Theme.outline // Border/divider color
```
## Semantic Colors
```qml
Theme.primary
Theme.onPrimary
Theme.secondary
Theme.onSecondary
Theme.error
Theme.errorHover
Theme.errorText
Theme.warning
Theme.success
```
## Special Functions
```qml
Theme.popupBackground() // Popup background with proper opacity
```
## Common Widget Patterns
### Icon with Text
```qml
import qs.Widgets
Row {
spacing: Theme.spacingS
DankIcon {
name: "icon_name"
color: Theme.onSurface
font.pixelSize: Theme.iconSize
}
StyledText {
text: "Label"
color: Theme.onSurface
font.pixelSize: Theme.fontSizeMedium
}
}
```
### Container with Border
```qml
Rectangle {
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
}
```
### Hover Effect
```qml
Rectangle {
id: container
color: Theme.surfaceContainerHigh
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: container.color = Qt.lighter(Theme.surfaceContainerHigh, 1.1)
onExited: container.color = Theme.surfaceContainerHigh
}
}
```
### Clickable Pill
```qml
StyledRect {
width: content.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: mouseArea.containsMouse
? Qt.lighter(Theme.surfaceContainerHigh, 1.1)
: Theme.surfaceContainerHigh
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
}
Row {
id: content
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "star"
color: Theme.surfaceText
font.pixelSize: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Label"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
anchors.verticalCenter: parent.verticalCenter
}
}
}
```
## Common Mistakes
**Wrong property names** (these do NOT exist):
```qml
Theme.fontSizeS // Use Theme.fontSizeSmall
Theme.iconSizeS // Use Theme.iconSizeSmall
Theme.spacingSmall // Use Theme.spacingS
Theme.borderRadius // Use Theme.cornerRadius
```
**Hardcoded values** (do NOT do this):
```qml
color: "#1e1e1e" // Use Theme.surfaceContainerHigh
color: "white" // Use Theme.surfaceText
font.pixelSize: 14 // Use Theme.fontSizeMedium
```
## Available Widgets from qs.Widgets
| Widget | Description |
|--------|-------------|
| `StyledText` | Themed text with proper color defaults |
| `StyledRect` | Themed rectangle |
| `DankIcon` | Material Symbols icon renderer |
| `DankNFIcon` | Nerd Font icon renderer |
| `DankButton` | Themed button |
| `DankToggle` | Toggle switch |
| `DankTextField` | Text input field |
| `DankSlider` | Slider control |
| `DankDropdown` | Dropdown menu |
| `DankGridView` | Grid layout view |
| `DankListView` | List layout view |
| `DankFlickable` | Scrollable container |
| `DankTabBar` | Tab bar navigation |
| `DankCollapsibleSection` | Collapsible content section |
| `DankTooltip` | Hover tooltip |
| `DankNumberStepper` | Number +/- control |
| `DankFilterChips` | Filter chip row |
| `CachingImage` | Image with disk cache |
| `NumericText` | Fixed-width numeric display |
## Checking All Properties
```bash
grep "property" Common/Theme.qml
```
@@ -0,0 +1,371 @@
# Widget Plugin Guide
Widgets are bar plugins that display pills in DankBar, optionally open popouts, and can integrate with the Control Center.
## Base Component
Widgets use `PluginComponent` from `qs.Modules.Plugins`.
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
property var popoutService: null
horizontalBarPill: Component { /* ... */ }
verticalBarPill: Component { /* ... */ }
popoutContent: Component { /* ... */ }
popoutWidth: 400
popoutHeight: 300
}
```
## Injected Properties
These are automatically set by the plugin host:
| Property | Type | Description |
|----------|------|-------------|
| `axis` | object | Bar axis info (horizontal/vertical) |
| `section` | string | Bar section: `"left"`, `"center"`, or `"right"` |
| `parentScreen` | object | Screen reference for multi-monitor |
| `widgetThickness` | real | Widget size perpendicular to bar edge |
| `barThickness` | real | Bar thickness parallel to edge |
| `pluginId` | string | This plugin's ID |
| `pluginService` | object | PluginService reference |
| `pluginData` | object | Reactive plugin settings data |
## Bar Pills
Define `horizontalBarPill` (for top/bottom bars) and `verticalBarPill` (for left/right bars).
### Horizontal Bar Pill
```qml
horizontalBarPill: Component {
StyledRect {
width: content.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Row {
id: content
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "star"
color: Theme.surfaceText
font.pixelSize: Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Label"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
```
### Vertical Bar Pill
```qml
verticalBarPill: Component {
StyledRect {
width: parent.widgetThickness
height: content.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: content
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "star"
color: Theme.surfaceText
font.pixelSize: Theme.iconSizeSmall
}
}
}
}
```
**Important:** Always define both pills. If a pill is missing, the widget disappears when the bar is on that orientation's edge.
## Popout Content
Open a popout window when the bar pill is clicked:
```qml
PluginComponent {
popoutWidth: 400
popoutHeight: 300
popoutContent: Component {
PopoutComponent {
headerText: "My Plugin"
detailsText: "Optional subtitle"
showCloseButton: true
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Content here"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
}
}
}
}
```
**PopoutComponent properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `headerText` | string | `""` | Main header (bold, large). 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. |
| `headerHeight` | int | (readonly) | Height of header area (0 if hidden). |
| `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). |
**Content sizing:** Content children render below the header/details. Calculate available height: `popoutHeight - headerHeight - detailsHeight - spacing`
## Custom Click Actions
Override the default popout behavior:
```qml
PluginComponent {
// Simple no-args handler
pillClickAction: () => {
popoutService?.toggleControlCenter()
}
// With position params (x, y, width, section, screen)
pillClickAction: (x, y, width, section, screen) => {
popoutService?.toggleControlCenter(x, y, width, section, screen)
}
pillRightClickAction: () => {
popoutService?.openSettings()
}
}
```
## Control Center Integration
Add CC properties to show your widget in the Control Center grid:
```qml
PluginComponent {
ccWidgetIcon: "toggle_on"
ccWidgetPrimaryText: "Feature Name"
ccWidgetSecondaryText: isActive ? "Active" : "Off"
ccWidgetIsActive: isActive
onCcWidgetToggled: {
isActive = !isActive
pluginService?.savePluginData(pluginId, "active", isActive)
}
}
```
**CC properties:**
| Property | Type | Description |
|----------|------|-------------|
| `ccWidgetIcon` | string | Material icon name |
| `ccWidgetPrimaryText` | string | Main label |
| `ccWidgetSecondaryText` | string | Subtitle / status text |
| `ccWidgetIsActive` | bool | Active state (changes styling) |
**CC signals:**
| Signal | When fired |
|--------|-----------|
| `ccWidgetToggled()` | Icon area clicked |
| `ccWidgetExpanded()` | Expand area clicked (CompoundPill only) |
**CC sizing rules:**
- 25% width - SmallToggleButton (icon only)
- 50% width - ToggleButton (no detail) or CompoundPill (with ccDetailContent)
- Users can resize in CC edit mode
### Detail Content (CompoundPill)
Add an expandable panel below the CC widget:
```qml
ccDetailContent: Component {
Rectangle {
implicitHeight: 200
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
// Detail UI here
}
}
}
```
## Visibility Control
Conditionally show/hide the bar pill:
```qml
PluginComponent {
visibilityCommand: "pgrep -x myapp"
visibilityInterval: 5 // seconds between checks
}
```
**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.
## Popout Namespace
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
```qml
PluginComponent {
layerNamespacePlugin: true
}
```
## Reading Plugin Data
Access saved settings reactively via the injected `pluginData`:
```qml
PluginComponent {
property string displayText: pluginData?.text || "Default"
Connections {
target: pluginService
function onPluginDataChanged(changedId) {
if (changedId === pluginId)
displayText = pluginService.loadPluginData(pluginId, "text", "Default")
}
}
}
```
## Complete Example
Based on the ExampleEmojiPlugin pattern:
```qml
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
property var popoutService: null
property var emojis: ["star", "heart", "smile"]
property int currentIndex: 0
Timer {
interval: 2000
running: true
repeat: true
onTriggered: currentIndex = (currentIndex + 1) % emojis.length
}
popoutWidth: 350
popoutHeight: 400
horizontalBarPill: Component {
StyledRect {
width: label.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: label
anchors.centerIn: parent
text: root.emojis[root.currentIndex]
font.pixelSize: Theme.fontSizeLarge
}
}
}
verticalBarPill: Component {
StyledRect {
width: parent.widgetThickness
height: label.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: label
anchors.centerIn: parent
text: root.emojis[root.currentIndex]
font.pixelSize: Theme.fontSizeMedium
}
}
}
popoutContent: Component {
PopoutComponent {
headerText: "Emoji Picker"
showCloseButton: true
DankGridView {
width: parent.width
height: 300
cellWidth: 50
cellHeight: 50
model: root.emojis
delegate: Rectangle {
width: 48
height: 48
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
Text {
anchors.centerIn: parent
text: modelData
font.pixelSize: 24
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Quickshell.execDetached(["dms", "cl", "copy", modelData])
ToastService?.showInfo("Copied " + modelData)
}
}
}
}
}
}
}
```
+31
View File
@@ -0,0 +1,31 @@
## Description
<!-- What does this PR do and why? -->
## Type of change
<!-- Check all that apply. -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that changes existing behavior)
- [ ] Refactor / internal cleanup
- [ ] Documentation
- [ ] Other
## Related issues
<!-- e.g. "Fixes #123", "Closes #123". Leave blank if none. -->
## Screenshots / video
<!-- Include screenshots or a video for any user-facing or visual change. -->
## Checklist
- [ ] My code follows the conventions in CONTRIBUTING.md
- [ ] I have tested my changes locally
- [ ] 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
- [ ] 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
+23
View File
@@ -0,0 +1,23 @@
# Categories for GitHub's auto-generated release notes
changelog:
exclude:
authors:
- github-actions[bot]
- dependabot[bot]
labels:
- internal
categories:
- title: Breaking Changes
labels: [breaking]
- title: Features
labels: [feature]
- title: Fixes
labels: [fix]
- title: Packaging
labels: [packaging]
- title: Internationalization
labels: [i18n]
- title: Documentation
labels: [docs]
- title: Other Changes
labels: ["*"]
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# Prunes retired XBPS packages from the R2 bucket so it stays inside the
# Cloudflare free tier. Only objects under ARCHIVE_PREFIX are eligible; live
# repositories (<repo>/current/) are never touched.
#
# publish-void-r2.sh writes retired packages to archive/<repo>/<published_at>/.
# Snapshots are dropped oldest first:
# 1. snapshots older than KEEP_DAYS, keeping KEEP_MIN_SNAPSHOTS per repo
# 2. then more until the whole bucket is <= MAX_BUCKET_GB
set -euo pipefail
require_env() {
local name
for name in "$@"; do
if [[ -z "${!name:-}" ]]; then
echo "error: $name is required" >&2
exit 2
fi
done
}
require_env R2_BUCKET R2_ENDPOINT
ARCHIVE_PREFIX="${ARCHIVE_PREFIX:-archive/}"
MAX_BUCKET_GB="${MAX_BUCKET_GB:-8}"
KEEP_DAYS="${KEEP_DAYS:-30}"
KEEP_MIN_SNAPSHOTS="${KEEP_MIN_SNAPSHOTS:-2}"
DRY_RUN="${DRY_RUN:-false}"
case "$ARCHIVE_PREFIX" in
*/) ;;
*) echo "error: ARCHIVE_PREFIX must end with /" >&2; exit 2 ;;
esac
aws_r2() {
aws --endpoint-url "$R2_ENDPOINT" "$@"
}
human() {
numfmt --to=iec-i --suffix=B "$1"
}
objects="$(aws_r2 s3api list-objects-v2 \
--bucket "$R2_BUCKET" \
--output json \
--query 'Contents[].{key:Key,size:Size}')"
[[ -n "$objects" && "$objects" != null ]] || objects='[]'
total="$(jq 'map(.size) | add // 0' <<<"$objects")"
budget="$(awk -v g="$MAX_BUCKET_GB" 'BEGIN { printf "%d", g * 1073741824 }')"
now="$(date -u +%s)"
snapshots="$(jq -c --arg p "$ARCHIVE_PREFIX" '
map(select(.key | startswith($p)))
| map(.rel = (.key[($p | length):] | split("/")))
| map(select((.rel | length) >= 3))
| group_by(.rel[0:2])
| map({
repo: .[0].rel[0],
stamp: .[0].rel[1],
prefix: ($p + (.[0].rel[0:2] | join("/")) + "/"),
size: (map(.size) | add),
count: length
})
| sort_by(.stamp)
' <<<"$objects")"
to_delete="$(jq -c \
--argjson now "$now" \
--argjson keepDays "$KEEP_DAYS" \
--argjson keepMin "$KEEP_MIN_SNAPSHOTS" \
--argjson total "$total" \
--argjson budget "$budget" '
def epoch:
.stamp
| sub("T(?<h>[0-9]{2})-(?<m>[0-9]{2})-(?<s>[0-9]{2})Z$"; "T\(.h):\(.m):\(.s)Z")
| try fromdateiso8601 catch null;
(group_by(.repo) | map({key: .[0].repo, value: length}) | from_entries) as $perRepo
| reduce .[] as $s ({kept: $perRepo, total: $total, delete: []};
($s | epoch) as $e
| (.kept[$s.repo] > $keepMin and $e != null and ($now - $e) > $keepDays * 86400) as $expired
| (.total > $budget) as $over
| if $expired or $over then
.delete += [$s + {reason: (if $over then "budget" else "age" end)}]
| .total -= $s.size
| .kept[$s.repo] -= 1
else . end)
| .delete
' <<<"$snapshots")"
echo "bucket: $R2_BUCKET ($(human "$total") in $(jq length <<<"$objects") objects)"
echo "budget: $(human "$budget") (MAX_BUCKET_GB=$MAX_BUCKET_GB)"
echo "archive: $(jq length <<<"$snapshots") snapshots, $(human "$(jq 'map(.size) | add // 0' <<<"$snapshots")")"
echo "policy: KEEP_DAYS=$KEEP_DAYS KEEP_MIN_SNAPSHOTS=$KEEP_MIN_SNAPSHOTS DRY_RUN=$DRY_RUN"
deleted=0
freed=0
while IFS=$'\t' read -r prefix size count reason; do
[[ -n "$prefix" ]] || continue
# Belt and braces: never delete outside the archive or anything live.
case "$prefix" in
"$ARCHIVE_PREFIX"*) ;;
*) echo "error: refusing to delete $prefix (outside $ARCHIVE_PREFIX)" >&2; exit 1 ;;
esac
case "$prefix" in
*/current/*) echo "error: refusing to delete live prefix $prefix" >&2; exit 1 ;;
esac
if [[ "$DRY_RUN" == "true" ]]; then
echo "would delete $prefix ($count objects, $(human "$size"), $reason)"
else
echo "deleting $prefix ($count objects, $(human "$size"), $reason)"
aws_r2 s3 rm "s3://${R2_BUCKET}/${prefix}" --recursive --only-show-errors
fi
deleted=$((deleted + 1))
freed=$((freed + size))
done < <(jq -r '.[] | [.prefix, .size, .count, .reason] | @tsv' <<<"$to_delete")
remaining=$((total - freed))
echo "pruned: $deleted snapshots, $(human "$freed") freed, $(human "$remaining") remaining"
if (( remaining > budget )); then
echo "::warning::bucket is still $(human "$remaining") after pruning (budget $(human "$budget")); live repositories exceed the budget"
fi
note=""
if [[ "$DRY_RUN" == "true" ]]; then
note=" (dry run)"
fi
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
{
echo "### R2 prune: \`$R2_BUCKET\`"
echo
echo "| | |"
echo "|---|---|"
echo "| Before | $(human "$total") |"
echo "| Budget | $(human "$budget") |"
echo "| Snapshots pruned | $deleted$note |"
echo "| Freed | $(human "$freed") |"
echo "| After | $(human "$remaining") |"
if (( deleted > 0 )); then
echo
echo "| Snapshot | Objects | Bytes | Reason |"
echo "|---|---|---|---|"
jq -r '.[] | "| `\(.prefix)` | \(.count) | \(.size) | \(.reason) |"' <<<"$to_delete"
fi
} >> "$GITHUB_STEP_SUMMARY"
fi
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "usage: $0 download|publish" >&2
exit 2
}
require_env() {
local name
for name in "$@"; do
if [[ -z "${!name:-}" ]]; then
echo "error: $name is required" >&2
exit 2
fi
done
}
require_env R2_BUCKET R2_ENDPOINT R2_PREFIX REPOSITORY_DIR
aws_r2() {
aws --endpoint-url "$R2_ENDPOINT" "$@"
}
download() {
mkdir -p "$REPOSITORY_DIR/current" "$REPOSITORY_DIR/previous-current"
aws_r2 s3 sync \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/" \
"$REPOSITORY_DIR/current/" \
--only-show-errors
cp -a "$REPOSITORY_DIR/current/." "$REPOSITORY_DIR/previous-current/"
}
build_manifest() {
local packages='[]'
local file filename pkgver name version revision sha size
for file in "$REPOSITORY_DIR"/current/*.xbps; do
filename="$(basename "$file")"
pkgver="$(xbps-uhelper binpkgver "$filename")"
name="$(xbps-uhelper getpkgname "$pkgver")"
version="$(xbps-uhelper getpkgversion "$pkgver")"
revision="$(xbps-uhelper getpkgrevision "$pkgver")"
version="${version%_"${revision}"}"
sha="$(sha256sum "$file" | cut -d' ' -f1)"
size="$(stat -c '%s' "$file")"
packages="$(jq \
--arg name "$name" \
--arg version "$version" \
--arg revision "$revision" \
--arg filename "$filename" \
--arg sha256 "$sha" \
--argjson size "$size" \
'. + [{name: $name, version: $version, revision: $revision, filename: $filename, sha256: $sha256, size: $size}]' \
<<<"$packages")"
done
jq -n \
--arg repository "$R2_PREFIX" \
--arg source_commit "$SOURCE_COMMIT" \
--arg published_at "$PUBLISHED_AT" \
--argjson packages "$packages" \
'{schema: 1, repository: $repository, source_commit: $source_commit, published_at: $published_at, packages: $packages}' \
> "$REPOSITORY_DIR/current/manifest.json"
}
verify_immutable_packages() {
local file previous
for file in "$REPOSITORY_DIR"/current/*.xbps "$REPOSITORY_DIR"/current/*.sig2; do
previous="$REPOSITORY_DIR/previous-current/$(basename "$file")"
if [[ -f "$previous" ]] && ! cmp -s "$previous" "$file"; then
echo "error: refusing to replace immutable object $(basename "$file")" >&2
echo "bump the XBPS revision or version before publishing a changed build" >&2
exit 1
fi
done
}
archive_retired() {
local old filename
local archive_prefix="archive/${R2_PREFIX}/${PUBLISHED_AT//:/-}"
shopt -s nullglob
for old in "$REPOSITORY_DIR"/previous-current/*.xbps "$REPOSITORY_DIR"/previous-current/*.sig2; do
filename="$(basename "$old")"
if [[ ! -e "$REPOSITORY_DIR/current/$filename" ]]; then
aws_r2 s3 cp \
"$old" \
"s3://${R2_BUCKET}/${archive_prefix}/${filename}" \
--cache-control 'private,no-store' \
--only-show-errors
fi
done
}
upload_current() {
local file filename old
# Versioned package objects must exist before repodata can reference them.
for file in "$REPOSITORY_DIR"/current/*.xbps "$REPOSITORY_DIR"/current/*.sig2; do
filename="$(basename "$file")"
aws_r2 s3 cp \
"$file" \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/${filename}" \
--cache-control 'public,max-age=31536000,immutable' \
--only-show-errors
done
aws_r2 s3 cp \
"$REPOSITORY_DIR/current/x86_64-repodata" \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/x86_64-repodata" \
--cache-control 'no-cache' \
--only-show-errors
# The manifest is the publication marker and is always uploaded last.
aws_r2 s3 cp \
"$REPOSITORY_DIR/current/manifest.json" \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/manifest.json" \
--cache-control 'no-cache' \
--only-show-errors
# Once the new index and marker are live, remove objects no longer referenced.
shopt -s nullglob
for old in "$REPOSITORY_DIR"/previous-current/*; do
filename="$(basename "$old")"
if [[ ! -e "$REPOSITORY_DIR/current/$filename" ]]; then
aws_r2 s3 rm \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/${filename}" \
--only-show-errors
fi
done
}
publish() {
require_env SOURCE_COMMIT
PUBLISHED_AT="${PUBLISHED_AT:-$(date -u +'%Y-%m-%dT%H:%M:%SZ')}"
export PUBLISHED_AT
shopt -s nullglob
local packages=("$REPOSITORY_DIR"/current/*.xbps)
if (( ${#packages[@]} == 0 )); then
echo "error: refusing to publish an empty XBPS repository" >&2
exit 1
fi
[[ -s "$REPOSITORY_DIR/current/x86_64-repodata" ]] || {
echo "error: x86_64-repodata is missing or empty" >&2
exit 1
}
for file in "${packages[@]}"; do
[[ -s "${file}.sig2" ]] || {
echo "error: signature is missing for $(basename "$file")" >&2
exit 1
}
done
verify_immutable_packages
build_manifest
archive_retired
upload_current
}
case "${1:-}" in
download) download ;;
publish) publish ;;
*) usage ;;
esac
+115
View File
@@ -0,0 +1,115 @@
name: Claude Review
# `/claude review` on a PR by a member of an allowed team. Review only, no edits or pushes.
# Secrets: CLAUDE_CODE_OAUTH_TOKEN (claude setup-token), CLAUDE_REVIEW_ORG_READ_TOKEN (PAT, org Members: read)
on:
issue_comment:
types: [created]
permissions: {}
jobs:
gate:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/claude review')
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
outputs:
allowed: ${{ steps.team.outputs.allowed }}
steps:
- name: Check team membership
id: team
env:
GH_TOKEN: ${{ secrets.CLAUDE_REVIEW_ORG_READ_TOKEN }}
ORG: ${{ github.repository_owner }}
USER: ${{ github.event.comment.user.login }}
TEAMS: dms-collaborators owners
run: |
allowed=false
for team in $TEAMS; do
state=$(gh api "orgs/$ORG/teams/$team/memberships/$USER" --jq .state 2>/dev/null || true)
[ "$state" = active ] && allowed=true && break
done
echo "allowed=$allowed" >> "$GITHUB_OUTPUT"
- name: React
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ALLOWED: ${{ steps.team.outputs.allowed }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
[ "$ALLOWED" = true ] && content=eyes || content=-1
gh api -X POST "repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions" -f "content=$content"
review:
needs: gate
if: needs.gate.outputs.allowed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: claude-review-${{ github.event.issue.number }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
ref: refs/pull/${{ github.event.issue.number }}/head
fetch-depth: 0
submodules: recursive
- uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
Review pull request #${{ github.event.issue.number }} in ${{ github.repository }}, requested by @${{ github.event.comment.user.login }}:
---
${{ github.event.comment.body }}
---
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.
Steps:
1. Read CONTRIBUTING.md.
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.
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.
6. Post one new summary comment with `gh pr comment ${{ github.event.issue.number }} --body ...`. Never edit earlier comments. Format:
## Claude review
<one line verdict>
- <finding, file:line> per inline comment posted this run
Checked: <one line>. Model: claude-opus-5.
No findings: "No issues found." plus what was checked.
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.
- 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.
- 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.
- 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.
Review only. Never edit files, commit, push, approve or request changes. Post each finding once. Keep comments short.
claude_args: |
--model claude-opus-5
--max-turns 80
--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:*)"
--disallowedTools "Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch"
- if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr comment ${{ github.event.issue.number }} --body "Claude review failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+56
View File
@@ -0,0 +1,56 @@
name: Sync flake.lock to dank-qml-common
on:
workflow_dispatch:
push:
paths:
- "dank-qml-common"
branches:
- master
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Point flake input at the submodule commit
run: |
set -euo pipefail
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only)
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])")
[ "$submodule_rev" = "$flake_rev" ] && { echo "flake.lock already matches $submodule_rev"; exit 0; }
nix flake lock --override-input dank-qml-common "github:AvengeMedia/dank-qml-common/$submodule_rev"
- name: Commit and push flake.lock update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if git diff --quiet flake.lock; then
echo "No changes to flake.lock"
exit 0
fi
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.lock
git commit -m "nix: sync flake.lock to dank-qml-common submodule"
git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
+56 -3
View File
@@ -7,20 +7,23 @@ on:
permissions:
contents: write
issues: write
jobs:
update-stable:
# skip prerelease tags
if: ${{ !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
@@ -28,4 +31,54 @@ jobs:
- name: Push to stable branch
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force
run: |
set -euo pipefail
# don't roll stable backwards
if git fetch origin stable --quiet 2>/dev/null; then
stable_tag=$(git describe --tags --abbrev=0 FETCH_HEAD 2>/dev/null || echo "v0.0.0")
newest=$(printf '%s\n%s\n' "$stable_tag" "${GITHUB_REF_NAME}" | sort -V | tail -1)
if [ "$newest" != "${GITHUB_REF_NAME}" ]; then
echo "skipping: ${GITHUB_REF_NAME} is older than stable (${stable_tag})"
exit 0
fi
fi
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" HEAD:refs/heads/stable --force
cut-release-branch:
# create stable-X.Y at each vX.Y.0 tag
if: ${{ !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Create release branch
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if [[ ! "${GITHUB_REF_NAME}" =~ ^v([0-9]+)\.([0-9]+)\.0$ ]]; then
echo "not a vX.Y.0 tag, no release branch to cut"
exit 0
fi
branch="stable-${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
if git ls-remote --exit-code origin "refs/heads/${branch}" >/dev/null 2>&1; then
echo "${branch} already exists"
exit 0
fi
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:refs/heads/${branch}"
echo "created ${branch} at ${GITHUB_REF_NAME}"
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh label create "port ${branch}" \
--repo "${{ github.repository }}" \
--description "Auto-port merged PR to ${branch}" --color 0e8a16 ||
echo "label 'port ${branch}' may already exist"
+50 -9
View File
@@ -8,7 +8,7 @@ on:
- "core/**"
- ".github/workflows/go-ci.yml"
pull_request:
branches: [master, main]
branches: [master, main, "stable-*"]
paths:
- "core/**"
- ".github/workflows/go-ci.yml"
@@ -26,21 +26,30 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
run: sudo apt-get update && sudo apt-get install -y flatpak
- name: Add flathub
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Cache flatpak runtime
uses: actions/cache@v4
with:
path: ~/.local/share/flatpak
key: flatpak-org.freedesktop.Platform-24.08
- name: Add a flatpak that mutagen could support
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Install a flatpak for tests
run: |
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak info org.freedesktop.Platform//24.08 >/dev/null 2>&1 || \
flatpak install --user -y org.freedesktop.Platform//24.08
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache-dependency-path: ./core/go.sum
- name: Test
run: go test -v ./...
@@ -48,8 +57,40 @@ jobs:
- name: Build dms
run: go build -v ./cmd/dms
- name: Build dms (embedded shell)
run: make build
- name: Build dms (distropkg)
run: go build -v -tags distro_binary ./cmd/dms
run: go build -v -tags 'distro_binary withshell' ./cmd/dms
- name: Build dankinstall
run: go build -v ./cmd/dankinstall
build-freebsd:
runs-on: ubuntu-latest
defaults:
run:
working-directory: core
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache-dependency-path: ./core/go.sum
- name: Build all packages
env:
GOOS: freebsd
CGO_ENABLED: 0
run: go build -v ./...
- name: Build dms (distropkg, embedded shell)
run: |
make sync-shell
GOOS=freebsd CGO_ENABLED=0 go build -v -tags 'distro_binary withshell' ./cmd/dms
+22 -4
View File
@@ -1,23 +1,41 @@
name: Check nix flake
name: Nix flake and NixOS tests
on:
pull_request:
branches: [master, main]
branches: [master, main, "stable-*"]
paths:
- "flake.*"
- "distro/nix/**"
jobs:
check-flake:
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
enable_kvm: true
extra_nix_config: |
system-features = nixos-test benchmark big-parallel kvm
- name: Check the flake
run: nix flake check
run: nix flake check -L
- name: Run NixOS module test
run: nix build .#nixosTests.x86_64-linux.nixos-module -L
- name: Run NixOS service start test
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
- name: Run home-manager module test
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
- name: Run niri home-manager module test
run: nix build .#nixosTests.x86_64-linux.niri-home-module -L
+107
View File
@@ -0,0 +1,107 @@
name: Point release
# Cuts vX.Y.Z from stable-X.Y: runs the port audit (warn-only), bumps
# quickshell/VERSION, tags, and dispatches the Release workflow. Distro
# builds are dispatched separately.
on:
workflow_dispatch:
inputs:
version:
description: "Point release version (e.g. 1.5.1)"
required: true
type: string
notify_issues:
description: "Comment on open related/fixes issues asking reporters to retest"
required: false
type: boolean
default: true
permissions:
contents: write
actions: write
concurrency:
group: point-release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
env:
VERSION: ${{ inputs.version }}
steps:
- name: Validate version and derive branch
id: derive
run: |
set -euo pipefail
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::version must be X.Y.Z (got '$VERSION')"; exit 1
fi
echo "branch=stable-${VERSION%.*}" >> "$GITHUB_OUTPUT"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout release branch
uses: actions/checkout@v6
with:
ref: ${{ steps.derive.outputs.branch }}
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Preflight — verify Release workflow is dispatchable
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh api "repos/${{ github.repository }}/actions/workflows/release.yml" \
--jq '.state' | grep -qx active ||
{ echo "::error::release.yml is not dispatchable; aborting before any push"; exit 1; }
- name: Port audit (informational)
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
bash scripts/port-audit.sh "${{ steps.derive.outputs.branch }}" ||
echo "::warning::port audit failed; continuing"
- name: Bump VERSION, tag, and push
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
TAG: ${{ steps.derive.outputs.tag }}
BRANCH: ${{ steps.derive.outputs.branch }}
run: |
set -euo pipefail
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
echo "::error::tag ${TAG} already exists"; exit 1
fi
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
echo "${TAG}" > quickshell/VERSION
git add quickshell/VERSION
git commit -m "bump VERSION to ${TAG}"
git tag "${TAG}"
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH}" "refs/tags/${TAG}"
- name: Dispatch Release workflow
env:
GH_TOKEN: ${{ github.token }}
run: |
gh workflow run release.yml --ref "${{ steps.derive.outputs.tag }}" \
-f tag="${{ steps.derive.outputs.tag }}" \
-f notify_issues="${{ inputs.notify_issues }}"
- name: Next steps
run: |
{
echo "## ${{ steps.derive.outputs.tag }} tagged on ${{ steps.derive.outputs.branch }} — Release workflow dispatched"
echo ""
echo "Distro builds are manual: run the per-distro workflows (COPR/OBS/PPA/XBPS) once the release is published."
} >> "$GITHUB_STEP_SUMMARY"
+30
View File
@@ -0,0 +1,30 @@
name: Port audit
# On-demand report of master commits not yet ported to a release branch.
# Updates the "Port status: <branch>" tracking issue and the step summary.
on:
workflow_dispatch:
inputs:
target:
description: "Release branch to audit (default: newest release/*)"
required: false
type: string
permissions:
contents: read
issues: write
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run audit
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash scripts/port-audit.sh ${{ inputs.target }} --issue
+101
View File
@@ -0,0 +1,101 @@
name: Port to release branch
# Ports flagged commits from master onto stable-X.Y branches:
# - "port 1.5" flag in a commit message pushed to master — own line or
# mid-line; separators : / - or space; "stable-1.5" also accepted;
# comma lists ("Port: 1.5, 1.4") work in the own-line form
# - "port stable-1.5" label on a merged PR
# Conflicts are reported to the "Port status: <branch>" tracking issue.
on:
push:
branches: [master]
pull_request_target:
types: [closed, labeled]
permissions:
contents: write
issues: write
concurrency:
group: port-engine
cancel-in-progress: false
jobs:
port:
name: Port flagged commits
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Base-repo code only; PR head code is never checked out or executed.
- name: Checkout
uses: actions/checkout@v6
with:
ref: master
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Port trailer-flagged commits
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
COMMITS: ${{ toJSON(github.event.commits) }}
run: |
set -euo pipefail
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
for sha in $(jq -r '.[].id' <<<"$COMMITS"); do
git cat-file -e "$sha" 2>/dev/null || continue
# skip merge commits (handled by the label path)
[ "$(git rev-list --no-walk --count --min-parents=2 "$sha")" -eq 0 ] || continue
# own-line form (supports comma lists), validated version-shaped
t1=$(git log -1 --format=%B "$sha" |
{ grep -iE '^Port[:/ -]' || true; } | sed -E 's|^port[-:/ ]+||I' | tr ',' '\n' |
sed 's/[[:space:]]//g; /^$/d' | sed -E 's,^stable-,,I' |
{ grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?$' || true; })
# mid-line form, e.g. "fix thing fixes #2802 port 1.5"
t2=$(git log -1 --format=%B "$sha" |
{ grep -oiE '\bport[-: /]+(stable-)?[0-9]+\.[0-9]+(\.[0-9]+)?\b' || true; } |
sed -E 's|^port[-:/ ]+||I' | sed -E 's,^stable-,,I')
targets=$(printf '%s\n%s\n' "$t1" "$t2" | sed '/^$/d' | sort -u)
for ver in $targets; do
echo "::group::port $sha -> stable-$ver"
bash scripts/port.sh "stable-$ver" "$sha"
echo "::endgroup::"
done
done
- name: Port label-flagged PR
if: >
github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
(github.event.action == 'closed' ||
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'port ')))
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
PORT_SOURCE_PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
targets=$(jq -r '.[] | select(startswith("port ")) | sub("^port +"; "")' <<<"$LABELS" |
sed -E 's,^stable-,,' | sort -u)
[ -n "$targets" ] || { echo "no port labels, nothing to do"; exit 0; }
[ -n "$MERGE_SHA" ] || { echo "::error::PR has no merge commit sha"; exit 1; }
for ver in $targets; do
echo "::group::port PR #${PORT_SOURCE_PR} ($MERGE_SHA) -> stable-$ver"
bash scripts/port.sh "stable-$ver" "$MERGE_SHA"
echo "::endgroup::"
done
+6 -4
View File
@@ -3,13 +3,15 @@ name: Pre-commit Checks
on:
push:
pull_request:
branches: [master, main]
branches: [master, main, "stable-*"]
jobs:
pre-commit-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
@@ -21,9 +23,9 @@ jobs:
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: core/go.mod
- name: run pre-commit hooks
uses: j178/prek-action@v1
uses: j178/prek-action@v2
+66
View File
@@ -0,0 +1,66 @@
name: Prune Void R2 archive
# Keeps the danklinux-void bucket inside the Cloudflare R2 free tier by
# deleting the oldest archive/<repo>/<timestamp>/ snapshots written by
# publish-void-r2.sh. Live repositories (<repo>/current/) are never touched.
on:
schedule:
- cron: "30 6 * * *"
workflow_dispatch:
inputs:
dry_run:
description: 'Report what would be deleted without deleting'
type: boolean
required: false
default: false
max_bucket_gb:
description: 'Total bucket size to prune down to (GB)'
type: string
required: false
default: '8'
keep_days:
description: 'Delete archive snapshots older than this many days'
type: string
required: false
default: '30'
keep_min_snapshots:
description: 'Always keep at least this many snapshots per repo (age rule only)'
type: string
required: false
default: '2'
permissions:
contents: read
# Serialize with the XBPS publisher so a snapshot is never pruned mid-publish.
concurrency:
group: void-xbps-${{ github.ref }}
cancel-in-progress: false
jobs:
prune:
name: Prune archive snapshots
runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: true
R2_BUCKET: danklinux-void
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
ARCHIVE_PREFIX: archive/
MAX_BUCKET_GB: ${{ github.event.inputs.max_bucket_gb || '8' }}
KEEP_DAYS: ${{ github.event.inputs.keep_days || '30' }}
KEEP_MIN_SNAPSHOTS: ${{ github.event.inputs.keep_min_snapshots || '2' }}
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Prune archive
run: bash .github/scripts/prune-void-r2.sh
+195 -58
View File
@@ -7,21 +7,50 @@ on:
description: "Tag to release (e.g., v1.0.1)"
required: true
type: string
notify_issues:
description: "Comment on open related/fixes issues asking reporters to retest"
required: false
type: boolean
default: false
permissions:
contents: write
actions: write
issues: write
concurrency:
group: release-${{ inputs.tag }}
cancel-in-progress: true
jobs:
verify-version:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag }}
- name: Verify quickshell/VERSION matches the tag
env:
TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
version=$(cat quickshell/VERSION)
if [ "${version#v}" != "${TAG#v}" ]; then
echo "::error::quickshell/VERSION is '${version}' but the tag is '${TAG}'"; exit 1
fi
build-core:
needs: [verify-version]
runs-on: ubuntu-latest
strategy:
matrix:
arch: [amd64, arm64]
include:
- { goos: linux, arch: amd64 }
- { goos: linux, arch: arm64 }
- { goos: freebsd, arch: amd64 }
- { goos: freebsd, arch: arm64 }
defaults:
run:
@@ -29,20 +58,24 @@ jobs:
env:
TAG: ${{ inputs.tag }}
# linux assets keep their historical arch-only names
ASSET: ${{ matrix.goos == 'linux' && matrix.arch || format('{0}-{1}', matrix.goos, matrix.arch) }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
- name: Format check
if: matrix.goos == 'linux'
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
@@ -51,38 +84,43 @@ jobs:
fi
- name: Run tests
if: matrix.goos == 'linux'
run: go test -v ./...
- name: Build dankinstall (${{ matrix.arch }})
- name: Build dankinstall (${{ env.ASSET }})
if: matrix.goos == 'linux'
env:
GOOS: linux
GOOS: ${{ matrix.goos }}
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ matrix.arch }}
-o ../../dankinstall-${{ env.ASSET }}
cd ../..
gzip -9 -k dankinstall-${{ matrix.arch }}
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
gzip -9 -k dankinstall-${{ env.ASSET }}
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
- name: Build dms (${{ matrix.arch }})
- name: Sync embedded shell
run: make sync-shell
- name: Build dms (${{ env.ASSET }})
env:
GOOS: linux
GOOS: ${{ matrix.goos }}
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ matrix.arch }}
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ env.ASSET }}
cd ../..
gzip -9 -k dms-${{ matrix.arch }}
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
gzip -9 -k dms-${{ env.ASSET }}
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
- name: Generate shell completions
if: matrix.arch == 'amd64'
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
run: |
set -eux
chmod +x dms-amd64
@@ -90,46 +128,58 @@ jobs:
./dms-amd64 completion fish > completion.fish
./dms-amd64 completion zsh > completion.zsh
- name: Build dms-distropkg (${{ matrix.arch }})
- name: Build dms-distropkg (${{ env.ASSET }})
env:
GOOS: linux
GOOS: ${{ matrix.goos }}
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ matrix.arch }}
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ env.ASSET }}
cd ../..
gzip -9 -k dms-distropkg-${{ matrix.arch }}
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
gzip -9 -k dms-distropkg-${{ env.ASSET }}
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
- name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v4
- name: Upload artifacts (${{ env.ASSET }})
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ matrix.arch }}
name: core-assets-${{ env.ASSET }}
path: |
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
core/dankinstall-${{ env.ASSET }}.gz
core/dankinstall-${{ env.ASSET }}.gz.sha256
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts (${{ env.ASSET }}, no installer)
if: matrix.goos != 'linux'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ env.ASSET }}
path: |
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts with completions
if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v4
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ matrix.arch }}
name: core-assets-${{ env.ASSET }}
path: |
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
core/dankinstall-${{ env.ASSET }}.gz
core/dankinstall-${{ env.ASSET }}.gz.sha256
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
core/completion.bash
core/completion.fish
core/completion.zsh
@@ -147,7 +197,7 @@ jobs:
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
# - name: Checkout
# uses: actions/checkout@v4
# uses: actions/checkout@v6
# with:
# token: ${{ steps.app_token.outputs.token }}
# fetch-depth: 0
@@ -181,23 +231,25 @@ jobs:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
submodules: recursive
- name: Fetch updated tag after version bump
run: |
git fetch origin --force tag ${TAG}
git checkout ${TAG}
git submodule update --init --recursive
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
- name: Download core artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
pattern: core-assets-*
merge-multiple: true
@@ -205,13 +257,23 @@ jobs:
- name: Generate Changelog
id: changelog
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
else
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
CHANGELOG=""
if [ -n "$PREVIOUS_TAG" ]; then
# PR-based notes with author credits; falls back to raw git log below
CHANGELOG=$(python3 scripts/release-notes.py "${PREVIOUS_TAG}..${TAG}" --format github --bare 2>/dev/null || true)
fi
if [ -z "$CHANGELOG" ]; then
echo "release-notes.py unavailable or empty, using git log fallback"
if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
else
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
fi
fi
cat > RELEASE_BODY.md << 'EOF'
@@ -226,16 +288,23 @@ jobs:
### Complete Packages
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
- **`dms-full-freebsd-amd64.tar.gz`** - Complete package for FreeBSD x86_64 systems
- **`dms-full-freebsd-arm64.tar.gz`** - Complete package for FreeBSD ARM64 systems
### Individual Components
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
- **`dms-cli-freebsd-amd64.gz`** - DMS CLI binary for FreeBSD x86_64 systems
- **`dms-cli-freebsd-arm64.gz`** - DMS CLI binary for FreeBSD ARM64 systems
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dms-distropkg-freebsd-amd64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD x86_64 systems
- **`dms-distropkg-freebsd-arm64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD ARM64 systems
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
- **`dms-qml.tar.gz`** - QML source code only
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging)
### Checksums
- **`*.sha256`** - SHA256 checksums for verifying download integrity
@@ -283,6 +352,22 @@ jobs:
# Copy completions
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Replace the DankCommon symlink with real submodule content for packaging
rm quickshell/DankCommon
cp -r dank-qml-common/DankCommon quickshell/DankCommon
# Create full source tarball (GitHub tag archives never contain submodule content)
VERSION_NUM=${TAG#v}
tar --exclude='.git' \
--exclude='.github' \
--exclude='dank-qml-common' \
--exclude='_release_assets' \
--exclude='_core_assets' \
--exclude='RELEASE_BODY.md' \
--transform "s,^\.,DankMaterialShell-${VERSION_NUM},S" \
-czf _release_assets/dms-source.tar.gz .
(cd _release_assets && sha256sum dms-source.tar.gz > dms-source.tar.gz.sha256)
# Create QML source package (exclude build artifacts and git files)
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
@@ -299,8 +384,8 @@ jobs:
# Generate checksum for QML package
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
# Create full packages for each architecture
for arch in amd64 arm64; do
# Create full packages for each os/architecture
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
mkdir -p _temp_full/dms
mkdir -p _temp_full/bin
mkdir -p _temp_full/completions
@@ -343,18 +428,16 @@ jobs:
## Installation Steps
1. **Install quickshell assets:**
```bash
mkdir -p ~/.config/quickshell
cp -r dms ~/.config/quickshell/
```
The Quickshell UI is embedded in the `dms` binary. The bundled `dms/`
tree is optional — pass it with `-c` or `DMS_SHELL_DIR` to run a
modified copy instead of the embedded UI.
2. **Install the DMS CLI binaries:**
1. **Install the DMS CLI binaries:**
```bash
sudo install -m 755 bin/dms /usr/local/bin/dms
```
3. **Install shell completions (optional):**
2. **Install shell completions (optional):**
```bash
# Bash
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
@@ -366,7 +449,7 @@ jobs:
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
```
4. **Start the shell:**
3. **Start the shell:**
```bash
dms run
```
@@ -417,3 +500,57 @@ jobs:
prerelease: ${{ contains(env.TAG, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Ask reporters on still-open "related/fixes #N" issues to retest.
# Opt-in via notify_issues (Point release defaults it on). Skipped for
# prereleases. Preview: scripts/notify-issues.sh <prev> <tag> --dry-run
- name: Notify open issues to retest
if: ${{ inputs.notify_issues && !contains(env.TAG, '-') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)
if [ -z "$PREVIOUS_TAG" ]; then
echo "no previous tag; skipping issue notify"
exit 0
fi
bash scripts/notify-issues.sh "$PREVIOUS_TAG" "$TAG"
# 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
# repo has no commits of its own since its last tag.
# fanout:
# name: Pin ${{ matrix.repo }}
# runs-on: ubuntu-latest
# needs: [release]
# if: ${{ !contains(inputs.tag, '-') }}
# strategy:
# fail-fast: false
# matrix:
# repo:
# - dgop
# - dankgo
# - dank-qml-common
# - dankcalendar
# - danksearch
# - dank-greeter
# steps:
# - name: Create GitHub App token
# id: app_token
# uses: actions/create-github-app-token@v3
# with:
# app-id: ${{ secrets.APP_ID }}
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
# owner: ${{ github.repository_owner }}
# repositories: ${{ matrix.repo }}
#
# - name: Dispatch pin-release
# env:
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
# REPO: ${{ github.repository_owner }}/${{ matrix.repo }}
# TAG: ${{ inputs.tag }}
# run: |
# set -euo pipefail
# gh workflow run pin-release.yml --repo "$REPO" -f version="$TAG"
# echo "dispatched pin-release ${TAG} to ${REPO}" >> "$GITHUB_STEP_SUMMARY"
+4 -12
View File
@@ -4,14 +4,12 @@ on:
workflow_dispatch:
inputs:
package:
description: 'Package to build (dms, dms-greeter, or both)'
description: 'Package to build'
required: false
default: 'dms'
type: choice
options:
- dms
- dms-greeter
- both
version:
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
required: false
@@ -31,11 +29,7 @@ jobs:
id: set-packages
run: |
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
if [ "$PACKAGE_INPUT" = "both" ]; then
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
else
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
fi
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
build-and-upload:
needs: determine-packages
@@ -46,7 +40,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Determine version
id: version
@@ -134,7 +128,7 @@ jobs:
rpm -qpi "$SRPM"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
@@ -163,8 +157,6 @@ jobs:
PACKAGE="${{ matrix.package }}"
if [ "$PACKAGE" = "dms" ]; then
COPR_PROJECT="avengemedia/dms"
elif [ "$PACKAGE" = "dms-greeter" ]; then
COPR_PROJECT="avengemedia/danklinux"
else
echo "❌ Unknown package: $PACKAGE"
exit 1
+29 -62
View File
@@ -10,7 +10,6 @@ on:
options:
- dms
- dms-git
- dms-greeter
- all
default: "dms"
rebuild_release:
@@ -24,6 +23,7 @@ jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell'
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
@@ -32,9 +32,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Check for updates
id: check
@@ -73,27 +74,12 @@ jobs:
fi
}
# Helper function to check dms-greeter stable tag
check_dms_greeter_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
# Run from tag with no package specified - update both stable packages
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
# Run from tag with no package specified - update stable package
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
@@ -119,19 +105,14 @@ jobs:
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
# Check the stable package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
fi
if check_dms_greeter_stable; then
PACKAGES_TO_UPDATE+=("dms-greeter")
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -164,18 +145,6 @@ jobs:
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_dms_greeter_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
@@ -196,9 +165,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Wait before OBS upload
run: sleep 3
- name: Determine packages to update
id: packages
@@ -216,9 +189,9 @@ jobs:
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch
# Determine version for dms stable and dms-greeter 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
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ 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 "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
@@ -245,7 +218,7 @@ jobs:
fi
- name: Update dms-git spec version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
if: contains(steps.packages.outputs.packages, 'dms-git')
run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
@@ -266,7 +239,7 @@ jobs:
} > distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
if: contains(steps.packages.outputs.packages, 'dms-git')
run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
@@ -284,7 +257,7 @@ jobs:
echo " -- Avenge Media <[email protected]> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog"
- name: Update stable version (dms + dms-greeter)
- name: Update stable version (dms)
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
@@ -320,32 +293,19 @@ jobs:
fi
fi
# Update dms-greeter changelog when dms-greeter is in the upload list
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <[email protected]> $CHANGELOG_DATE"
} > "distro/debian/dms-greeter/debian/changelog"
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
fi
# Update Debian _service files for packages in upload list (download_url paths)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms, dms-greeter stable)
# Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
done
- name: Install Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
@@ -365,6 +325,16 @@ jobs:
EOF
chmod 600 ~/.config/osc/oscrc
# Cache OBS bundled Go toolchains
- name: Cache OBS bundled Go toolchains (dms-git)
if: contains(steps.packages.outputs.packages, 'dms-git')
uses: actions/cache@v4
with:
path: /home/runner/.cache/dms-obs-go-toolchain
key: dms-obs-go-toolchain-${{ runner.os }}-${{ hashFiles('core/go.mod') }}
restore-keys: |
dms-obs-go-toolchain-${{ runner.os }}-
- name: Upload to OBS
id: upload
env:
@@ -389,7 +359,7 @@ jobs:
UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# PACKAGES can be a space-separated list (from the "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
echo ""
@@ -492,9 +462,6 @@ jobs:
dms-git)
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
+77 -207
View File
@@ -4,9 +4,14 @@ on:
workflow_dispatch:
inputs:
package:
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: false
default: "dms-git"
description: "Package to upload"
required: true
type: choice
options:
- dms
- dms-git
- all
default: "dms"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false
@@ -16,147 +21,83 @@ on:
jobs:
check-updates:
name: Check for updates
name: Check package/series updates
runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell'
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
targets: ${{ steps.check.outputs.targets }}
targets_json: ${{ steps.check.outputs.targets_json }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq curl git
- name: Check for updates
id: check
run: |
# Helper function to check dms-git commit
check_dms_git() {
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
return 0 # Update needed
fi
}
# Helper function to check stable package tag
check_stable_package() {
local PKG="$1"
local PPA_NAME="$2"
# Use git ls-remote to find the latest tag, sorted by version (descending)
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
chmod +x distro/scripts/ppa-sync-plan.sh
if [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - check dms-git only
echo "packages=dms-git" >> $GITHUB_OUTPUT
if check_dms_git; then
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow trigger
PKG="${{ github.event.inputs.package }}"
if [[ -n "$REBUILD" ]]; then
# Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
if check_dms_git; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms" ]]; then
if check_stable_package "dms" "dms"; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_stable_package "dms-greeter" "danklinux"; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: $PKG"
fi
PACKAGE="dms-git"
else
# Fallback
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
PACKAGE="${{ github.event.inputs.package }}"
fi
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
ARGS=(--package "$PACKAGE" --json)
if [[ -n "$REBUILD_RELEASE" ]]; then
ARGS+=(--rebuild "$REBUILD_RELEASE")
fi
TARGETS_JSON=$(distro/scripts/ppa-sync-plan.sh "${ARGS[@]}" 2> ppa-audit.log)
cat ppa-audit.log
TARGETS=$(echo "$TARGETS_JSON" | jq -r 'join(" ")')
if [[ "$TARGETS_JSON" != "[]" ]]; then
echo "has_updates=true" >> "$GITHUB_OUTPUT"
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
echo "targets_json=$TARGETS_JSON" >> "$GITHUB_OUTPUT"
echo "Package/series targets: $TARGETS"
else
echo "has_updates=false" >> "$GITHUB_OUTPUT"
echo "targets=" >> "$GITHUB_OUTPUT"
echo "targets_json=[]" >> "$GITHUB_OUTPUT"
echo "No package/series uploads needed"
fi
upload-ppa:
name: Upload to PPA
name: Upload ${{ matrix.target }}
needs: check-updates
runs-on: ubuntu-latest
if: needs.check-updates.outputs.has_updates == 'true'
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
target: ${{ fromJson(needs.check-updates.outputs.targets_json) }}
concurrency:
group: ppa-dms-${{ matrix.target }}
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache: false
@@ -171,7 +112,8 @@ jobs:
lftp \
build-essential \
fakeroot \
dpkg-dev
dpkg-dev \
openssh-client
- name: Configure GPG
env:
@@ -179,102 +121,30 @@ jobs:
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> "$GITHUB_ENV"
- name: Determine packages to upload
id: packages
- name: Upload target
env:
TARGET: ${{ matrix.target }}
LAUNCHPAD_SSH_PRIVATE_KEY: ${{ secrets.LAUNCHPAD_SSH_PRIVATE_KEY }}
LAUNCHPAD_SSH_LOGIN: ${{ secrets.LAUNCHPAD_SSH_LOGIN }}
run: |
# Use packages determined by check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
IFS=':' read -r PACKAGE UBUNTU_SERIES PPA_NUM <<< "$TARGET"
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
fi
case "$PACKAGE" in
dms) PPA_NAME="dms" ;;
dms-git) PPA_NAME="dms-git" ;;
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
esac
- name: Upload to PPA
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!"
exit 0
fi
# Export REBUILD_RELEASE so ppa-build.sh can use it
if [[ -n "$REBUILD_RELEASE" ]]; then
export REBUILD_RELEASE
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
# Map package to PPA name
case "$PKG" in
dms)
PPA_NAME="dms"
;;
dms-git)
PPA_NAME="dms-git"
;;
dms-greeter)
PPA_NAME="danklinux"
;;
*)
echo "⚠️ Unknown package: $PKG, skipping"
continue
;;
esac
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PKG to PPA $PPA_NAME..."
if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
done
echo "Uploading $PACKAGE to $PPA_NAME/$UBUNTU_SERIES with ppa$PPA_NUM"
bash distro/scripts/ppa-upload.sh "$PACKAGE" "$PPA_NAME" "$UBUNTU_SERIES" "$PPA_NUM"
- name: Summary
if: always()
run: |
echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ -z "$PACKAGES" ]]; then
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
else
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do
case "$PKG" in
dms)
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
;;
dms-git)
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
fi
echo "### PPA Package Upload" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
+313
View File
@@ -0,0 +1,313 @@
name: Void Linux XBPS Repository
on:
schedule:
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
# release trigger disabled; dispatch manually after a release
# release:
# types: [published]
workflow_dispatch:
inputs:
version:
description: 'Stable release tag/version to build (e.g. v1.5.0 or 1.5.0). Leave blank to use the selected ref/template version'
type: string
required: false
force_rebuild:
description: 'Force rebuilding packages even if they already exist in the repository'
type: boolean
required: false
default: false
build_git:
description: 'Build dms-git package'
type: boolean
required: false
default: true
build_dms:
description: 'Build stable dms package'
type: boolean
required: false
default: true
permissions:
contents: read
concurrency:
group: void-xbps-${{ github.ref }}
cancel-in-progress: false
jobs:
preflight:
name: Check for unpublished changes
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
steps:
- name: Check published commit
id: check
env:
MANIFEST_URL: https://void.danklinux.com/dms/current/manifest.json
run: |
SHOULD_BUILD=true
if [ "${{ github.event_name }}" = "schedule" ]; then
PUBLISHED_COMMIT="$(curl -fsSL --retry 3 "$MANIFEST_URL" | jq -r '.source_commit // empty' 2>/dev/null || true)"
if [ "$PUBLISHED_COMMIT" = "${{ github.sha }}" ]; then
SHOULD_BUILD=false
echo "${{ github.sha }} is already published; skipping the scheduled build."
fi
fi
echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT"
build-and-deploy:
name: Build & Deploy XBPS packages
runs-on: ubuntu-latest
needs: preflight
if: github.repository == 'AvengeMedia/DankMaterialShell' && needs.preflight.outputs.should_build == 'true'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: true
R2_BUCKET: danklinux-void
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
R2_PREFIX: dms
REPOSITORY_DIR: ${{ github.workspace }}/r2-repo
SOURCE_COMMIT: ${{ github.sha }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.release.tag_name || (github.event.inputs.version && (startsWith(github.event.inputs.version, 'v') && github.event.inputs.version || format('v{0}', github.event.inputs.version))) || github.ref }}
fetch-depth: 0
submodules: recursive
- name: Record checked-out source commit
run: echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
- name: Download current R2 repository
run: |
aws --version
bash .github/scripts/publish-void-r2.sh download
- name: Install XBPS Static Utilities
run: |
mkdir -p ${{ github.workspace }}/xbps
curl -s -L https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz | tar -xJ -C ${{ github.workspace }}/xbps
echo "${{ github.workspace }}/xbps/usr/bin" >> $GITHUB_PATH
- name: Clone void-packages
run: |
git clone --depth=1 https://github.com/void-linux/void-packages.git
- name: Inject templates
run: |
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
- name: Bootstrap xbps-src
run: |
cd void-packages
./xbps-src binary-bootstrap
- name: Configure signing keys and trust
run: |
# Write private key; extract and register the public key
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
chmod 600 /tmp/xbps_privkey.pem
# Extract public key in PEM format
openssl rsa -in /tmp/xbps_privkey.pem -pubout -out /tmp/dms-key.pub
rm -f /tmp/xbps_privkey.pem
# Compute MD5 fingerprint in colon-separated hex format
FINGERPRINT=$(openssl rsa -pubin -in /tmp/dms-key.pub -outform DER 2>/dev/null | openssl dgst -md5 -c | tr '[:upper:]' '[:lower:]' | awk '{print $NF}')
# Format key in XML property list (plist) format as expected by xbps
mkdir -p /tmp/keys
cat <<EOF > "/tmp/keys/${FINGERPRINT}.plist"
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>public-key</key>
<data>$(base64 -w0 /tmp/dms-key.pub)</data>
<key>public-key-size</key>
<integer>4096</integer>
<key>signature-by</key>
<string>AvengeMedia</string>
</dict>
</plist>
EOF
# Copy keys to all host and chroot trust stores
sudo mkdir -p /var/db/xbps/keys
sudo cp "/tmp/keys/${FINGERPRINT}.plist" "/var/db/xbps/keys/${FINGERPRINT}.plist"
mkdir -p void-packages/masterdir/var/db/xbps/keys
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/masterdir/var/db/xbps/keys/${FINGERPRINT}.plist"
mkdir -p void-packages/etc/xbps.d/keys
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/etc/xbps.d/keys/${FINGERPRINT}.plist"
mkdir -p void-packages/common/repo-keys
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/common/repo-keys/${FINGERPRINT}.plist"
rm -rf /tmp/keys /tmp/dms-key.pub
- name: Configure repositories
run: |
# Append the repository to repos-remote templates so xbps-src translates it automatically
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote.conf
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
# Add any existing compiled packages to the build cache directory to avoid rebuilds
if [ -d "r2-repo/current" ]; then
mkdir -p void-packages/hostdir/binpkgs
cp -L r2-repo/current/*.xbps void-packages/hostdir/binpkgs/ 2>/dev/null || true
xbps-rindex -a void-packages/hostdir/binpkgs/*.xbps 2>/dev/null || true
fi
- name: Build packages
run: |
mkdir -p r2-repo/current
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
if [ "${{ github.event_name }}" = "schedule" ]; then
BUILD_DMS="false"
BUILD_GIT="true"
fi
if [ "${{ github.event_name }}" = "release" ]; then
BUILD_DMS="true"
BUILD_GIT="false"
fi
RELEASE_TAG=""
if [ "${{ github.event_name }}" = "release" ]; then
RELEASE_TAG="${{ github.event.release.tag_name }}"
elif [ -n "${{ github.event.inputs.version || '' }}" ]; then
RELEASE_TAG="${{ github.event.inputs.version }}"
elif [ "${{ github.ref_type }}" = "tag" ]; then
RELEASE_TAG="${{ github.ref_name }}"
fi
if [ -n "$RELEASE_TAG" ]; then
RELEASE_VER="${RELEASE_TAG#v}"
ARCHIVE_TAG="v${RELEASE_VER}"
echo "Stable release tag: $ARCHIVE_TAG"
else
RELEASE_VER=""
ARCHIVE_TAG=""
echo "Stable release tag: (template version)"
fi
echo "=== Starting Builds ==="
echo "DMS stable build enabled: $BUILD_DMS"
echo "Git build enabled: $BUILD_GIT"
echo "Force rebuild: $FORCE_REBUILD"
cd void-packages
if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then
echo "🔧 Updating stable template for $ARCHIVE_TAG"
TARBALL="$(mktemp)"
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
rm -f "$TARBALL"
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
fi
# 1. Build dms-git (development package)
if [ "$BUILD_GIT" = "true" ]; then
# Calculate dynamic git version (tag.commits.hash)
GIT_VER=$(git -C .. describe --tags --always | sed 's/^v//; s/-/./g')
echo "🔨 Preparing dms-git version $GIT_VER"
# Stage source tarball in the xbps-src source cache.
# --transform adds a top-level directory so xbps-src can extract
# into $wrksrc (create_wrksrc=yes handles the rest).
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
mkdir -p "$SRC_CACHE"
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
--exclude=void-packages \
--exclude=r2-repo \
--exclude=.git \
--exclude=danklinux \
-C .. .
CHECKSUM=$(sha256sum "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" | cut -d' ' -f1)
# Dynamically patch template version, checksum, and distfiles
sed -i "s/^version=.*/version=${GIT_VER}/" srcpkgs/dms-git/template
sed -i "s/^checksum=.*/checksum=${CHECKSUM}/" srcpkgs/dms-git/template
sed -i "s|^distfiles=.*|distfiles=\"dms-git-${GIT_VER}.tar.gz\"|" srcpkgs/dms-git/template
EXPECTED_GIT_FILE="dms-git-${GIT_VER}_1.x86_64.xbps"
if [ -f "../r2-repo/current/$EXPECTED_GIT_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_GIT_FILE already exists, skipping build."
else
echo "🔨 Compiling dms-git..."
./xbps-src pkg dms-git
rm -f "../r2-repo/current/${EXPECTED_GIT_FILE}"
cp -L "hostdir/binpkgs/${EXPECTED_GIT_FILE}" ../r2-repo/current/
fi
fi
# 2. Build stable dms package
if [ "$BUILD_DMS" = "true" ]; then
STABLE_VER=$(grep -E '^version=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
STABLE_REV=$(grep -E '^revision=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
EXPECTED_DMS_FILE="dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps"
if [ -f "../r2-repo/current/$EXPECTED_DMS_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_DMS_FILE already exists, skipping build."
else
echo "🔨 Compiling dms ($STABLE_VER)..."
./xbps-src pkg dms
rm -f "../r2-repo/current/${EXPECTED_DMS_FILE}"
cp -L hostdir/binpkgs/dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps ../r2-repo/current/
fi
fi
- name: Index and sign repository
run: |
cd r2-repo/current
rm -f *.sig
# Guard: nothing to index if no .xbps files exist
if ! ls *.xbps 1>/dev/null 2>&1; then
echo "⚠️ No .xbps files found to index, skipping."
exit 0
fi
# Regenerate repo index
xbps-rindex -a $(pwd)/*.xbps
xbps-rindex --remove-obsoletes $(pwd)
# Remove signatures left behind by obsolete packages. Signatures for
# unchanged immutable packages are retained byte-for-byte.
for sig in *.sig2; do
[ -e "$sig" ] || break
[ -f "${sig%.sig2}" ] || rm -f "$sig"
done
# Sign repository
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
chmod 600 /tmp/xbps_privkey.pem
xbps-rindex --sign --signedby "AvengeMedia <[email protected]>" --privkey /tmp/xbps_privkey.pem $(pwd)
xbps-rindex --sign-pkg --privkey /tmp/xbps_privkey.pem $(pwd)/*.xbps
rm -f /tmp/xbps_privkey.pem
- name: Publish repository to R2
run: bash .github/scripts/publish-void-r2.sh publish
+2 -2
View File
@@ -18,13 +18,13 @@ jobs:
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
+20
View File
@@ -107,8 +107,28 @@ vim/
bin/
# Core dumps
core.*
# prek-installed local git hooks (generated from .pre-commit-config.yaml)
.githooks/
# direnv
.envrc
.direnv/
quickshell/dms-plugins
quickshell/dms-plugins-external
__pycache__
.vscode/
# Void (xbps) build artifacts
*.xbps
distro/void/temp/
distro/void/hostdir/
distro/void/masterdir*/
# Often gets built
core/dms
core/internal/shellembed/dist
+4
View File
@@ -0,0 +1,4 @@
[submodule "dank-qml-common"]
path = dank-qml-common
url = https://github.com/AvengeMedia/dank-qml-common.git
branch = stable-1.6
+32
View File
@@ -20,3 +20,35 @@ repos:
language: system
files: ^core/.*\.(go|mod|sum)$
pass_filenames: false
- repo: local
hooks:
- id: settings-search-index
name: settings search index is up to date
entry: bash -c 'python3 quickshell/translations/extract_settings_index.py >/dev/null || exit 1; if ! git diff --exit-code -- quickshell/translations/settings_search_index.json; then echo "settings_search_index.json is out of date; run quickshell/translations/extract_settings_index.py and stage the result" >&2; exit 1; fi'
language: system
files: ^quickshell/(Modules/Settings/.*\.qml|Common/SettingsTabs\.qml|Services/SettingsSearchService\.qml|translations/(extract_settings_index\.py|en\.json))$
pass_filenames: false
- repo: local
hooks:
- id: i18n-term-freeze
name: i18n term freeze (no new I18n.tr/qsTr terms)
entry: python3 quickshell/translations/check_term_freeze.py
language: system
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
pass_filenames: false
- repo: local
hooks:
- id: i18n-term-variants
name: i18n term variants (no case/punctuation duplicates)
entry: python3 quickshell/translations/check_term_variants.py
language: system
files: ^quickshell/(.*\.qml|translations/(check_term_variants\.py|extract_translations\.py))$
pass_filenames: false
- repo: local
hooks:
- id: no-console-in-qml
name: no console.* in QML (use Log service)
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
files: ^quickshell/.*\.qml$
exclude: ^quickshell/(Services/Log\.qml$|dms-plugins/|PLUGINS/)
+62
View File
@@ -0,0 +1,62 @@
## 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.
## 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.
There is a GO module (dankgo) that is used by core for certain common go functionality.
### 1- core:
Contains 2 apps:
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.
2. cmd/dankinstall - An entirely separate app, a TUI for quickly installing DMS
### 2- quickshell:
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.
Layout:
- Common/ - app-wide singletons and helpers (Theme, SettingsData, SessionData, I18n, PopoutManager)
- Services/ - headless singletons that talk to the system and to core
- 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 start with the part of the system they touched, followed by a short lowercase explanation of the work:
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.
+16
View File
@@ -1,5 +1,21 @@
This file is more of a quick reference so I know what to account for before next releases.
# Next
- Go core migrated to dankgo (shared log/paths/errdefs/ipc/shellapp)
- Quickshell UI embedded in the dms binary, -c / DMS_SHELL_DIR override - breaking for setups relying on ~/.config/quickshell/dms auto-discovery (XDG search removed)
- release binaries + all -git packages build with withshell; sockets/pidfiles/env names unchanged, IPC APIVersion 28 wire-compatible
- -git packages (AUR dms-shell-git, fedora, opensuse, void, debian, ubuntu) no longer install /usr/share/quickshell/dms; dms greeter install falls back to the embedded UI when the greeter package is present
- ping over the socket returns {"pong":true} instead of "pong"
- startup: embedded UI resolution is keyed by build-time .dankrev (no per-start content hashing)
# 1.5.0
- Overhauled shadows
- App ID changed to com.danklinux.dms - breaking for window rules
- Greeter stuff
- Terminal mux
- Locale overrides
- new neovim theming
# 1.4.0
- Overhauled system monitor, graphs, styling
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+77 -1
View File
@@ -6,6 +6,20 @@ To contribute fork this repository, make your changes, and open a pull request.
## Setup
Clone with submodules — the shared widget library ([dank-qml-common](https://github.com/AvengeMedia/dank-qml-common)) is vendored at `dank-qml-common/` and symlinked into `quickshell/DankCommon`:
```bash
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git
# or, in an existing clone:
git submodule update --init
```
To have `git pull` keep the submodule in sync automatically (moving it to the commit this repo points at, no separate `git submodule update` step), set:
```bash
git config submodule.recurse true
```
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
```bash
@@ -28,6 +42,47 @@ This will provide:
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
## Building and running
The Quickshell UI is embedded into the `dms` binary at build time. `make build` copies `quickshell/` into `core/internal/shellembed/dist/` (generated, never committed) and compiles with the `withshell` tag. `make dev` builds without the tag — that binary carries no UI and requires an explicit config dir.
```bash
make build # embedded binary at core/bin/dms
make dev # untagged development build
make run # dev build, then launch against the live quickshell/ tree
```
The UI config dir resolves in order: `-c <dir>`, `DMS_SHELL_DIR`, the dir a running instance is using, then the embedded UI. Each candidate must contain `shell.qml`. `make run` uses `-c $(pwd)/quickshell`, so QML edits hot-reload from the working tree.
The Go core depends on [dankgo](https://github.com/AvengeMedia/dankgo) for logging, XDG paths, the IPC transport, and the quickshell process lifecycle. To develop against a local dankgo checkout, create a gitignored `go.work` at the repo root:
```
go 1.26.1
use (
./core
../dankgo
)
```
## Shared widgets (dank-qml-common)
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
2. Commit and PR those changes in the `dank-qml-common` repo: `cd dank-qml-common && git switch -c my-change`, push, open the PR there.
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
The submodule URL in `.gitmodules` is HTTPS so CI and anonymous clones keep working. To push over SSH instead of being prompted for credentials, add a push rewrite to your git config — fetches stay HTTPS, pushes use SSH:
```bash
git config --global url."[email protected]:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/"
```
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
## VSCode Setup
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
@@ -86,7 +141,9 @@ touch .qmlls.ini
4. Restart dms to generate the `.qmlls.ini` file
5. Make your changes, test, and open a pull request.
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.
### I18n/Localization
@@ -102,6 +159,10 @@ Text {
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
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.
### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
@@ -110,6 +171,21 @@ Preferably, try to keep new terms to a minimum and re-use existing terms where p
4. Run `go mod tidy`
5. Open pull request
golangci-lint runs as a pre-commit hook and covers `go vet`, so there's nothing separate to run. If you run `go vet ./...` yourself you'll see complaints about the generated mocks, ignore them, `core/.golangci.yml` excludes that directory.
#### Mocks
Test mocks under `core/internal/mocks/` are generated with [mockery](https://vektra.github.io/mockery/) v3. Don't edit them by hand, regenerate after changing a mocked interface:
```bash
cd core
go run github.com/vektra/mockery/v3@latest
# or with it installed (go install github.com/vektra/mockery/v3@latest):
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.
## Pull request
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
+35 -19
View File
@@ -6,8 +6,8 @@ BINARY_NAME=dms
CORE_DIR=core
BUILD_DIR=$(CORE_DIR)/bin
PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin
DATA_DIR=$(PREFIX)/share
INSTALL_DIR=$(DESTDIR)$(PREFIX)/bin
DATA_DIR=$(DESTDIR)$(PREFIX)/share
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
.PHONY: all build dev run clean lint-qml install install-bin install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
all: build
@@ -27,24 +27,32 @@ build:
@$(MAKE) -C $(CORE_DIR) build
@echo "Build complete"
dev:
@$(MAKE) -C $(CORE_DIR) dev
run: dev
@$(BUILD_DIR)/$(BINARY_NAME) run -c $(CURDIR)/$(SHELL_DIR)
clean:
@echo "Cleaning build artifacts..."
@$(MAKE) -C $(CORE_DIR) clean
@echo "Clean complete"
lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh
# Pull the latest dank-qml-common and pin it everywhere it is consumed
# (submodule pointer + nix flake input). Commit both in one change.
update-common:
git submodule update --remote --merge dank-qml-common
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
# Installation targets
install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Binary installed"
install-shell:
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
@mkdir -p $(SHELL_INSTALL_DIR)
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@echo "Shell files installed"
install-completions:
@echo "Installing shell completions..."
@mkdir -p $(DATA_DIR)/bash-completion/completions
@@ -56,13 +64,17 @@ install-completions:
@echo "Shell completions installed"
install-systemd:
ifneq ($(shell uname),Linux)
@echo "Skipping systemd user service (non-Linux); start the shell from your compositor config with 'dms run'"
else
@echo "Installing systemd user service..."
@mkdir -p $(SYSTEMD_USER_DIR)
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
@sed 's|/usr/bin/dms|$(PREFIX)/bin/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
endif
install-icon:
@echo "Installing icon..."
@@ -71,12 +83,14 @@ install-icon:
@echo "Icon installed"
install-desktop:
@echo "Installing desktop entry..."
@echo "Installing desktop entries..."
@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)/com.danklinux.dms.notepad.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry installed"
@echo "Desktop entries installed"
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
install: install-bin install-completions install-systemd install-icon install-desktop
@echo ""
@echo "Installation complete!"
@echo ""
@@ -89,7 +103,7 @@ uninstall-bin:
@echo "Binary removed"
uninstall-shell:
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
@echo "Removing pre-1.6 shell files from $(SHELL_INSTALL_DIR)..."
@rm -rf $(SHELL_INSTALL_DIR)
@echo "Shell files removed"
@@ -113,10 +127,12 @@ uninstall-icon:
@echo "Icon removed"
uninstall-desktop:
@echo "Removing desktop entry..."
@echo "Removing desktop entries..."
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry removed"
@echo "Desktop entries removed"
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
@echo ""
@@ -130,11 +146,11 @@ help:
@echo " all (default) - Build the DMS binary"
@echo " build - Same as 'all'"
@echo " clean - Clean build artifacts"
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
@echo ""
@echo "Install:"
@echo " install - Build and install everything (requires sudo)"
@echo " install-bin - Install only the binary"
@echo " install-shell - Install only shell files"
@echo " install-completions - Install only shell completions"
@echo " install-systemd - Install only systemd service"
@echo " install-icon - Install only icon"
@@ -143,7 +159,7 @@ help:
@echo "Uninstall:"
@echo " uninstall - Remove everything (requires sudo)"
@echo " uninstall-bin - Remove only the binary"
@echo " uninstall-shell - Remove only shell files"
@echo " uninstall-shell - Remove shell files left by pre-1.6 installs"
@echo " uninstall-completions - Remove only shell completions"
@echo " uninstall-systemd - Remove only systemd service"
@echo " uninstall-icon - Remove only icon"
+21 -6
View File
@@ -13,7 +13,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
[![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 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 release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases)
[![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin)
[![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)
[![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)
@@ -47,7 +47,7 @@ DankMaterialShell/
<div align="center">
https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
https://github.com/user-attachments/assets/d310418b-cc44-4ace-ac0d-2fc0c851291d
</div>
@@ -83,7 +83,7 @@ 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.
**System Monitoring**
Real-time CPU, RAM, GPU metrics and temperatures with [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/AvengeMedia/dgop)). Process list with search and management.
**Powerful Launcher**
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
@@ -98,10 +98,12 @@ Notification center with grouping, rich text support, and keyboard navigation.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
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).
**Plugin System**
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
Extend functionality with the [plugin registry](https://plugins.danklinux.com). DMS keeps
`~/.config/DankMaterialShell/plugins.lock.json` synchronized with managed plugin installs and
their exact Git commits, so the same plugins can be reproduced on another machine.
## Supported Compositors
@@ -120,6 +122,8 @@ dms ipc call audio setvolume 50
dms ipc call wallpaper set /path/to/image.jpg
dms brightness list # List available displays
dms plugins search # Browse plugin registry
dms plugins lock # Refresh the portable plugin lockfile
dms plugins restore ~/plugins.lock.json
```
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
@@ -132,6 +136,17 @@ dms plugins search # Browse plugin registry
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
## Dank Projects
DMS is one piece of the suite. The rest lives in its own repos:
- [dank-greeter](https://github.com/AvengeMedia/dank-greeter) - greetd login screen with the Dank Material aesthetic. The Greeter tab in DMS settings is the front-end for it.
- [dankcalendar](https://github.com/AvengeMedia/dankcalendar) - Local, Google, Microsoft, and CalDAV calendars for the dank desktop.
- [dgop](https://github.com/AvengeMedia/dgop) - System monitoring 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.
- [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
- [dankgo](https://github.com/AvengeMedia/dankgo) - Common Go modules behind the single binary apps.
## Development
See component-specific documentation:
@@ -188,7 +203,7 @@ For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeM
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=AvengeMedia/DankMaterialShell&type=date&legend=top-left)](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
[![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)
## License
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=DankMaterialShell
GenericName=Desktop Shell
Comment=Material 3 inspired shell for Wayland compositors
Exec=dms run
Icon=danklogo
Terminal=false
NoDisplay=true
StartupNotify=false
Categories=Utility;
+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Name=DMS Notepad
GenericName=Text Editor
Comment=Quick notes and text files in the dank desktop shell
Exec=dms ipc call notepad openFile %f
Icon=danklogo
Terminal=false
NoDisplay=true
Categories=Utility;TextEditor;
MimeType=text/plain;text/markdown;
StartupNotify=false
+5 -1
View File
@@ -8,9 +8,13 @@ Requisite=graphical-session.target
Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms
ExecReload=/bin/kill -USR1 $MAINPID
LimitNOFILE=16384:infinity
Restart=on-failure
RestartForceExitStatus=TEMPFAIL
SuccessExitStatus=TEMPFAIL
RestartSec=1.23
TimeoutStartSec=90s
TimeoutStopSec=10
[Install]
+5 -6
View File
@@ -72,7 +72,6 @@ linters:
- path: _test\.go
linters:
- errcheck
- govet
- unused
- ineffassign
- staticcheck
@@ -93,15 +92,15 @@ linters:
- linters:
- errcheck
text: "Error return value of `.+\\.AddMatchSignal` is not checked"
# Exclude wayland pkg from errcheck and ineffassign (generated code patterns)
- linters:
- errcheck
- ineffassign
path: pkg/go-wayland/
# Exclude proto pkg from ineffassign (generated protocol code)
- linters:
- ineffassign
path: internal/proto/
# Exclude proto pkg SA4006 (generated protocol code has trailing dead offset increments)
- linters:
- staticcheck
text: "SA4006:"
path: internal/proto/
# binary.Write/Read to bytes.Buffer can't fail
- linters:
- errcheck
+66 -57
View File
@@ -1,76 +1,85 @@
with-expecter: true
dir: "internal/mocks/{{.InterfaceDirRelative}}"
mockname: "Mock{{.InterfaceName}}"
outpkg: "{{.PackageName}}"
dir: internal/mocks/{{.InterfaceDirRelative}}
structname: Mock{{.InterfaceName}}
pkgname: '{{.SrcPackageName}}'
filename: 'mock_{{.InterfaceName}}.go'
template: testify
template-data:
unroll-variadic: true
packages:
github.com/Wifx/gonetworkmanager/v2:
interfaces:
NetworkManager:
Device:
DeviceWireless:
AccessPoint:
Connection:
Settings:
ActiveConnection:
IP4Config:
net:
interfaces:
Conn:
github.com/AvengeMedia/danklinux/internal/plugins:
interfaces:
GitClient:
github.com/godbus/dbus/v5:
interfaces:
BusObject:
github.com/AvengeMedia/danklinux/internal/server/brightness:
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
config:
dir: "internal/mocks/brightness"
outpkg: mocks_brightness
dir: internal/mocks/geolocation
pkgname: mocks_geolocation
interfaces:
DBusConn:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config:
dir: "internal/mocks/network"
outpkg: mocks_network
interfaces:
Backend:
Client: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups:
config:
dir: "internal/mocks/cups"
outpkg: mocks_cups
dir: internal/mocks/cups
pkgname: mocks_cups
interfaces:
CUPSClientInterface:
CUPSClientInterface: {}
PkHelper:
config:
dir: "internal/mocks/cups_pkhelper"
outpkg: mocks_cups_pkhelper
dir: internal/mocks/cups_pkhelper
pkgname: mocks_cups_pkhelper
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
config:
dir: "internal/mocks/evdev"
outpkg: mocks_evdev
dir: internal/mocks/evdev
pkgname: mocks_evdev
interfaces:
EvdevDevice:
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
EvdevDevice: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config:
dir: "internal/mocks/version"
outpkg: mocks_version
dir: internal/mocks/network
pkgname: mocks_network
interfaces:
VersionFetcher:
Backend: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
config:
dir: "internal/mocks/wlcontext"
outpkg: mocks_wlcontext
dir: internal/mocks/wlcontext
pkgname: mocks_wlcontext
interfaces:
WaylandContext:
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
config:
dir: "internal/mocks/wlclient"
outpkg: mocks_wlclient
interfaces:
WaylandDisplay:
WaylandContext: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
config:
dir: "internal/mocks/utils"
outpkg: mocks_utils
dir: internal/mocks/utils
pkgname: mocks_utils
interfaces:
AppChecker:
AppChecker: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
config:
dir: internal/mocks/version
pkgname: mocks_version
interfaces:
VersionFetcher: {}
github.com/AvengeMedia/dankgo/wayland/client:
config:
dir: internal/mocks/wlclient
pkgname: mocks_wlclient
interfaces:
WaylandDisplay: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/plugins:
interfaces:
GitClient: {}
github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness:
config:
dir: internal/mocks/brightness
pkgname: mocks_brightness
interfaces:
DBusConn: {}
github.com/Wifx/gonetworkmanager/v2:
interfaces:
AccessPoint: {}
ActiveConnection: {}
Connection: {}
Device: {}
DeviceWireless: {}
IP4Config: {}
NetworkManager: {}
Settings: {}
github.com/godbus/dbus/v5:
interfaces:
BusObject: {}
net:
interfaces:
Conn: {}
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.9.0
rev: v2.13.1
hooks:
- id: golangci-lint-fmt
require_serial: true
+55 -30
View File
@@ -3,6 +3,8 @@ BINARY_NAME_INSTALL=dankinstall
SOURCE_DIR=cmd/dms
SOURCE_DIR_INSTALL=cmd/dankinstall
BUILD_DIR=bin
SHELL_SRC=../quickshell
EMBED_DIR=internal/shellembed/dist
PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin
@@ -21,17 +23,46 @@ BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUI
# Architecture to build for dist target (amd64, arm64, or all)
ARCH ?= all
# Target OSes for dist builds
DIST_OSES ?= linux freebsd
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
ifeq ($(ARCH),all)
DIST_ARCHS = amd64 arm64
else
DIST_ARCHS = $(ARCH)
endif
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
# Default target
all: build
# Copy the quickshell UI into the embed dir (gitignored) so tagged builds
# can bake it into the binary. Dev-only files are stripped; scripts/ is kept
# minus its dev entries, since Theme.qml and BluetoothService.qml run
# gtk.sh/qt.sh/bluez-card-profile.lua out of the resolved shell dir.
# tar -h dereferences the DankCommon submodule symlink; go:embed rejects
# symlinks. .qmlls.ini is excluded at copy time: it's a symlink into the
# quickshell runtime VFS and dereferencing it fails whenever the shell isn't
# running.
sync-shell:
@test -e $(SHELL_SRC)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
@rm -rf $(EMBED_DIR)
@mkdir -p $(EMBED_DIR)
@tar -C $(SHELL_SRC) --exclude=.qmlls.ini -chf - . | tar -C $(EMBED_DIR) -xf -
@rm -rf $(EMBED_DIR)/.git* $(EMBED_DIR)/.github
@find $(EMBED_DIR) -type d \( -name .claude -o -name .vscode \) -prune -exec rm -rf {} +
@rm -f $(EMBED_DIR)/AGENTS.md $(EMBED_DIR)/qmlformat-all.sh
@rm -f $(EMBED_DIR)/scripts/i18nsync.py $(EMBED_DIR)/scripts/build-vscode-vsix.sh $(EMBED_DIR)/scripts/qmllint-entrypoints.sh
@rm -f $(EMBED_DIR)/scripts/spam-notifications.sh $(EMBED_DIR)/scripts/verify-notifications.sh
@rm -f $(EMBED_DIR)/translations/*.py $(EMBED_DIR)/translations/WORKFLOW.md
@cd $(EMBED_DIR) && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum | sha256sum | cut -c1-16 > .dankrev
# Build the main binary (dms)
build:
build: sync-shell
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
CGO_ENABLED=0 $(GO) build -tags withshell $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
dankinstall:
@@ -40,42 +71,34 @@ dankinstall:
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
dist:
ifeq ($(ARCH),all)
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
# Build distro binaries (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair
dist: sync-shell
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..."
@mkdir -p $(BUILD_DIR)
@echo "Building for linux/amd64..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
@echo "Building for linux/arm64..."
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
@echo "Distribution builds complete:"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
else
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
@mkdir -p $(BUILD_DIR)
@echo "Building for linux/$(ARCH)..."
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
@echo "Distribution build complete:"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
endif
@for os in $(DIST_OSES); do \
for arch in $(DIST_ARCHS); do \
echo "Building for $$os/$$arch..."; \
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch ./$(SOURCE_DIR) || exit 1; \
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \
done; \
done
@echo "Distribution builds complete"
build-all: build dankinstall
install: build
install:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installation complete"
install-all: build-all
install-all:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete"
install-dankinstall: dankinstall
install-dankinstall:
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete"
@@ -99,7 +122,7 @@ uninstall-dankinstall:
clean:
@echo "Cleaning build artifacts..."
@rm -rf $(BUILD_DIR)
@rm -rf $(BUILD_DIR) $(EMBED_DIR)
@echo "Clean complete"
test:
@@ -112,7 +135,8 @@ fmt:
vet:
@echo "Running go vet..."
$(GO) vet ./...
$(GO) vet $$($(GO) list ./... | grep -v internal/highlight/lexers)
$(GO) vet -composites=false ./internal/highlight/lexers
deps:
@echo "Updating dependencies..."
@@ -141,10 +165,11 @@ print-version:
help:
@echo "Available targets:"
@echo " all - Build the main binary (dms) (default)"
@echo " build - Build the main binary (dms)"
@echo " build - Build the main binary (dms) with the embedded UI"
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
@echo " dankinstall - Build dankinstall binary"
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
@echo " dist - Build dms for linux/freebsd amd64/arm64 (no update/greeter)"
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow"
@echo " build-all - Build both binaries"
@echo " install - Install dms to $(INSTALL_DIR)"
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
+61 -8
View File
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
Command-line interface and daemon for shell management and system control.
**dankinstall**
Distribution-aware installer with TUI for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo.
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
## System Integration
@@ -74,11 +74,11 @@ Custom IPC via unix socket (JSON API) for shell communication.
- `dms run [-d]` - Start shell (optionally as daemon)
- `dms restart` / `dms kill` - Manage running processes
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
- `dms plugins [install|browse|search]` - Plugin management
- `dms plugins [install|browse|search|lock|restore]` - Plugin management and portable exact-revision lockfiles
- `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages)
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
- `dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
### Color Picker
@@ -109,7 +109,7 @@ make test # Run tests
**Distribution build:**
```bash
make dist # Build without update/greeter features
make dist # Build without update features
```
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
@@ -132,9 +132,11 @@ This runs gofmt, golangci-lint, tests, and builds before each commit when `core/
**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`:
```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
go run github.com/AvengeMedia/dankgo/cmd/go-wayland-scanner@latest \
-i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
-pkg wlr_gamma_control -o internal/proto/wlr_gamma_control/gamma_control.go
```
@@ -147,13 +149,61 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
## Installation via dankinstall
**Interactive (TUI):**
```bash
curl -fsSL https://install.danklinux.com | sh
```
**Headless (unattended):**
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
sudo -v && curl -fsSL https://install.danklinux.com | 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.danklinux.com | sh -s -- -c niri -t ghostty --git-deps niri,quickshell --all-features -y
```
| Flag | Short | Description |
|------|-------|-------------|
| `--compositor <niri\|hyprland\|mango>` | `-c` | Compositor/WM to install (required for headless) |
| `--term <ghostty\|kitty\|alacritty>` | `-t` | Terminal emulator (required for headless) |
| `--privesc <sudo\|doas\|run0>` | | Privilege escalation tool (default: autodetect) |
| `--git` | | Install the git version of every dependency that has one |
| `--git-deps <name,...>` | | Install the git version of specific dependencies (e.g. `niri,quickshell`) |
| `--all-features` | | Enable all optional dependencies (`dms-greeter`, `danksearch`, `dankcalendar`) |
| `--dms-greeter` | | Install dms-greeter |
| `--danksearch` | | Install danksearch and enable its user indexing service |
| `--dankcalendar` | | Install dankcalendar |
| `--include-deps <name,...>` | | Enable optional dependencies (e.g. `dms-greeter`) |
| `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
| `--yes` | `-y` | Required for headless mode - confirms installation without interactive prompts |
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
Optional packages (`dms-greeter`, `danksearch`, `dankcalendar`) are disabled by default; enable them with their dedicated flags, `--include-deps`, or `--all-features`.
When no flags are provided, `dankinstall` launches the interactive TUI.
### Headless mode validation rules
Headless mode activates when `--compositor` or `--term` is provided.
- Both `--compositor` and `--term` are required; providing only one results in an error.
- Headless-only flags (`--privesc`, `--git`, `--git-deps`, `--all-features`, `--include-deps`, `--exclude-deps`, `--replace-configs`, `--replace-configs-all`, `--yes`, `--danksearch`, `--dankcalendar`, `--dms-greeter`) are rejected in TUI mode.
- Positional arguments are not accepted.
### Log file location
`dankinstall` writes logs to `/tmp` by default.
Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory.
## Supported Distributions
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives)
**Arch Linux**
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
@@ -166,7 +216,7 @@ Uses COPR repositories (`avengemedia/danklinux`, `avengemedia/dms`).
Requires PPA support. Most packages built from source (slow first install).
**Debian**
Debian 13+ (Trixie). niri only, no Hyprland support. Builds from source.
Debian 13+ (Trixie), testing, and sid. Stable quickshell from Debian (trixie-backports on 13). Other companions via OBS.
**openSUSE**
Most packages available in standard repos. Minimal building required.
@@ -174,4 +224,7 @@ Most packages available in standard repos. Minimal building required.
**Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
**Void Linux**
Uses XBPS with the DMS and DankLinux self-hosted repositories.
See installer output for distribution-specific details during installation.
+182 -3
View File
@@ -3,20 +3,172 @@ package main
import (
"fmt"
"os"
"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"
"github.com/spf13/cobra"
)
var Version = "dev"
var (
compositor string
term string
privescTool string
gitAll bool
gitDeps []string
allFeatures bool
includeDeps []string
excludeDeps []string
replaceConfigs []string
replaceConfigsAll bool
yes bool
danksearch bool
dankcalendar bool
dmsGreeter bool
)
var rootCmd = &cobra.Command{
Use: "dankinstall",
Short: "Install DankMaterialShell and its dependencies",
Long: `dankinstall sets up DankMaterialShell with your chosen compositor and terminal.
Without flags, it launches an interactive TUI. Providing either --compositor
or --term activates headless (unattended) mode, which requires both flags.
Headless mode requires cached credentials or a passwordless rule for your
privilege escalation tool (sudo, doas, or run0).`,
Args: cobra.NoArgs,
RunE: runDankinstall,
SilenceErrors: true,
SilenceUsage: true,
}
func init() {
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)")
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
rootCmd.Flags().StringVar(&privescTool, "privesc", "", "Privilege escalation tool: sudo, doas, or run0 (default: autodetect)")
rootCmd.Flags().BoolVar(&gitAll, "git", false, "Install the git version of every dep that has one")
rootCmd.Flags().StringSliceVar(&gitDeps, "git-deps", []string{}, "Deps to install the git version of (e.g. niri,quickshell)")
rootCmd.Flags().BoolVar(&allFeatures, "all-features", false, "Enable all optional deps (dms-greeter, danksearch, dankcalendar)")
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
rootCmd.Flags().BoolVar(&danksearch, "danksearch", false, "Install danksearch and enable its user indexing service")
rootCmd.Flags().BoolVar(&dankcalendar, "dankcalendar", false, "Install dankcalendar")
rootCmd.Flags().BoolVar(&dmsGreeter, "dms-greeter", false, "Install dms-greeter")
}
func main() {
if os.Getuid() == 0 {
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
os.Exit(1)
}
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runDankinstall(cmd *cobra.Command, args []string) error {
headlessMode := compositor != "" || term != ""
if !headlessMode {
headlessOnly := []string{
"privesc",
"git",
"git-deps",
"all-features",
"include-deps",
"exclude-deps",
"replace-configs",
"replace-configs-all",
"yes",
"danksearch",
"dankcalendar",
"dms-greeter",
}
var set []string
for _, name := range headlessOnly {
if cmd.Flags().Changed(name) {
set = append(set, "--"+name)
}
}
if len(set) > 0 {
return fmt.Errorf("flags %s are only valid in headless mode (requires both --compositor and --term)", strings.Join(set, ", "))
}
}
if headlessMode {
return runHeadless()
}
return runTUI()
}
func runHeadless() error {
if compositor == "" {
return fmt.Errorf("--compositor is required for headless mode (niri, hyprland, or mango)")
}
if term == "" {
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
}
cfg := headless.Config{
Compositor: compositor,
Terminal: term,
PrivescTool: privescTool,
GitAll: gitAll,
GitDeps: gitDeps,
AllFeatures: allFeatures,
IncludeDeps: includeDeps,
ExcludeDeps: excludeDeps,
ReplaceConfigs: replaceConfigs,
ReplaceConfigsAll: replaceConfigsAll,
Yes: yes,
DankSearch: danksearch,
DankCalendar: dankcalendar,
DmsGreeter: dmsGreeter,
}
runner := headless.NewRunner(cfg)
fileLogger, err := log.NewFileLogger()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to create log file: %v\n", err)
}
if fileLogger != nil {
fmt.Printf("Logging to: %s\n", fileLogger.GetLogPath())
fileLogger.StartListening(runner.GetLogChan())
defer func() {
if err := fileLogger.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to close log file: %v\n", err)
}
}()
} else {
defer drainLogChan(runner.GetLogChan())()
}
if err := runner.Run(); err != nil {
if fileLogger != nil {
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", fileLogger.GetLogPath())
}
return err
}
if fileLogger != nil {
fmt.Printf("\nFull logs are available at: %s\n", fileLogger.GetLogPath())
}
return nil
}
func runTUI() error {
fileLogger, err := log.NewFileLogger()
if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err)
@@ -38,18 +190,45 @@ func main() {
if fileLogger != nil {
fileLogger.StartListening(model.GetLogChan())
} else {
defer drainLogChan(model.GetLogChan())()
}
p := tea.NewProgram(model, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Printf("Error running program: %v\n", err)
if logFilePath != "" {
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", logFilePath)
}
os.Exit(1)
return fmt.Errorf("error running program: %w", err)
}
if logFilePath != "" {
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
}
return nil
}
// drainLogChan discards messages from logCh so blocking sends in downstream
// components cannot deadlock when no file logger is attached; the returned
// cleanup stops the drain without assuming the channel will ever close.
func drainLogChan(logCh <-chan string) func() {
drainStop := make(chan struct{})
drainDone := make(chan struct{})
go func() {
defer close(drainDone)
for {
select {
case <-drainStop:
return
case _, ok := <-logCh:
if !ok {
return
}
}
}
}()
return func() {
close(drainStop)
<-drainDone
}
}
+242
View File
@@ -0,0 +1,242 @@
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
"github.com/spf13/cobra"
)
var authCmd = &cobra.Command{
Use: "auth",
Short: "Manage DMS authentication sync",
Long: "Manage PAM/authentication setup for the DMS lock screen",
}
var authSyncCmd = &cobra.Command{
Use: "sync",
Short: "Sync DMS authentication configuration",
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal")
if term {
if err := syncAuthInTerminal(yes); err != nil {
log.Fatalf("Error launching auth sync in terminal: %v", err)
}
return
}
if err := syncAuth(yes); err != nil {
log.Fatalf("Error syncing authentication: %v", err)
}
},
}
var authResolveLockCmd = &cobra.Command{
Use: "resolve-lock",
Short: "Generate the lock-screen PAM config from the system auth stack",
Long: "Resolve the distribution's PAM auth stack into a self-contained lock-screen config under the user state directory.\n" +
"Runs unprivileged (reads /etc/pam.d, writes to the user's state dir) and is used by the shell as a fallback when /etc/pam.d/dankshell is not managed.\n" +
"Prints the path of the generated file.",
Run: func(cmd *cobra.Command, args []string) {
quiet, _ := cmd.Flags().GetBool("quiet")
logFunc := func(msg string) {
if !quiet {
fmt.Println(msg)
}
}
path, err := sharedpam.WriteUserLockscreenPamConfig(logFunc)
if err != nil {
log.Fatalf("Error resolving lock-screen PAM config: %v", err)
}
fmt.Println(path)
},
}
var authListServicesCmd = &cobra.Command{
Use: "list-services",
Short: "List candidate lock-screen PAM services available on this system",
Long: "Enumerate the lock-screen PAM services that exist on this system and report their resolved auth stack (whether it has an auth directive and whether fingerprint/U2F modules appear inline).",
Run: func(cmd *cobra.Command, args []string) {
asJSON, _ := cmd.Flags().GetBool("json")
services := sharedpam.ListLockscreenPamServices()
if asJSON {
payload := struct {
Services []sharedpam.LockscreenPamServiceInfo `json:"services"`
}{Services: services}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("Error encoding services: %v", err)
}
fmt.Println(string(data))
return
}
if len(services) == 0 {
fmt.Println("No candidate lock-screen PAM services found.")
return
}
for _, s := range services {
fmt.Printf("%-20s %-30s auth=%-5t fingerprint=%-5t u2f=%t\n", s.Name, s.Path, s.HasAuth, s.InlineFingerprint, s.InlineU2f)
}
},
}
var authValidateCmd = &cobra.Command{
Use: "validate",
Short: "Validate a PAM service file for use by the DMS lock screen",
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password or dedicated U2F stack. Exits 1 when the file is not usable.",
Run: func(cmd *cobra.Command, args []string) {
path, _ := cmd.Flags().GetString("path")
service, _ := cmd.Flags().GetString("service")
purpose, _ := cmd.Flags().GetString("purpose")
asJSON, _ := cmd.Flags().GetBool("json")
if (path == "") == (service == "") {
log.Fatalf("Error: exactly one of --path or --service is required")
}
if purpose != "password" && purpose != "u2f" {
log.Fatalf("Error: --purpose must be password or u2f")
}
var result sharedpam.LockscreenPamValidation
switch {
case service != "":
if purpose == "u2f" {
result = sharedpam.ValidateLockscreenU2fPamService(service)
} else {
result = sharedpam.ValidateLockscreenPamService(service)
}
case !filepath.IsAbs(path):
result = sharedpam.LockscreenPamValidation{
Path: path,
MissingModules: []string{},
Warnings: []string{},
Errors: []string{"--path must be an absolute file path"},
}
default:
if purpose == "u2f" {
result = sharedpam.ValidateLockscreenU2fPamPath(path)
} else {
result = sharedpam.ValidateLockscreenPamPath(path)
}
}
if asJSON {
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("Error encoding validation: %v", err)
}
fmt.Println(string(data))
} else {
printLockscreenPamValidation(result)
}
if !result.Valid {
os.Exit(1)
}
},
}
func printLockscreenPamValidation(result sharedpam.LockscreenPamValidation) {
fmt.Printf("Path: %s\n", result.Path)
fmt.Printf("Valid: %t\n", result.Valid)
fmt.Printf("Has auth: %t\n", result.HasAuth)
fmt.Printf("Inline fingerprint: %t\n", result.InlineFingerprint)
fmt.Printf("Inline U2F: %t\n", result.InlineU2f)
if len(result.MissingModules) > 0 {
fmt.Printf("Missing modules: %s\n", strings.Join(result.MissingModules, ", "))
}
for _, w := range result.Warnings {
fmt.Println("⚠ " + w)
}
for _, e := range result.Errors {
fmt.Println("✗ " + e)
}
}
func init() {
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
authListServicesCmd.Flags().Bool("json", false, "Output as JSON")
authValidateCmd.Flags().String("path", "", "Absolute path to a PAM service file to validate")
authValidateCmd.Flags().String("service", "", "Name of a PAM service to resolve across the system PAM dirs")
authValidateCmd.Flags().String("purpose", "password", "Validation purpose: password or u2f")
authValidateCmd.Flags().Bool("json", false, "Output as JSON")
}
func syncAuth(nonInteractive bool) error {
if !nonInteractive {
fmt.Println("=== DMS Authentication Sync ===")
fmt.Println()
}
logFunc := func(msg string) {
fmt.Println(msg)
}
if err := sharedpam.SyncAuthConfig(logFunc, "", sharedpam.SyncAuthOptions{}); err != nil {
return err
}
if !nonInteractive {
fmt.Println("\n=== Authentication Sync Complete ===")
fmt.Println("\nAuthentication changes have been applied.")
}
return nil
}
func syncAuthInTerminal(nonInteractive bool) error {
syncFlags := make([]string, 0, 1)
if nonInteractive {
syncFlags = append(syncFlags, "--yes")
}
shellSyncCmd := "dms auth sync"
if len(syncFlags) > 0 {
shellSyncCmd += " " + strings.Join(syncFlags, " ")
}
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
return runCommandInTerminal(shellCmd)
}
func runCommandInTerminal(shellCmd string) error {
terminals := []struct {
name string
args []string
}{
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
{"kitty", []string{"bash", "-c", shellCmd}},
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
}
for _, t := range terminals {
if _, err := exec.LookPath(t.name); err != nil {
continue
}
cmd := exec.Command(t.name, t.args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
}
+62
View File
@@ -0,0 +1,62 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/backup"
"github.com/spf13/cobra"
)
var backupOutputPath string
var backupCmd = &cobra.Command{
Use: "backup",
Short: "Backup and restore DMS configuration",
}
var backupCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a tar.gz backup of ~/.config/DankMaterialShell (settings, plugins, themes)",
Args: cobra.NoArgs,
Run: runBackupCreate,
}
var backupRestoreCmd = &cobra.Command{
Use: "restore <archive>",
Short: "Restore a DMS configuration backup; the current configuration is moved aside first",
Args: cobra.ExactArgs(1),
Run: runBackupRestore,
}
func init() {
backupCreateCmd.Flags().StringVarP(&backupOutputPath, "output", "o", "", "Output archive path (default dms-backup-<timestamp>.tar.gz)")
backupCmd.AddCommand(backupCreateCmd, backupRestoreCmd)
rootCmd.AddCommand(backupCmd)
}
func runBackupCreate(cmd *cobra.Command, args []string) {
output := backupOutputPath
if output == "" {
output = backup.DefaultArchiveName()
}
if err := backup.Create(output); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(output)
}
func runBackupRestore(cmd *cobra.Command, args []string) {
previous, err := backup.Restore(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if previous != "" {
fmt.Printf("Previous configuration moved to %s\n", previous)
}
fmt.Println("Restore complete. Restart the shell to apply.")
}
+8 -10
View File
@@ -8,13 +8,13 @@ import (
"strings"
"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/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/spf13/cobra"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
ghtml "github.com/yuin/goldmark/renderer/html"
@@ -116,7 +116,7 @@ func getCachedStyle(name string) *chroma.Style {
style := styles.Get(name)
if style == nil {
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
style = styles.Fallback
style = styles.Fallback()
}
cacheMutex.Lock()
@@ -209,12 +209,10 @@ func runChroma(cmd *cobra.Command, args []string) {
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithStyle(chromaStyle),
highlighting.WithFormatOptions(
html.WithClasses(!chromaInline),
),
),
highlight.Markdown{
Style: styles.Get(chromaStyle),
Formatter: html.New(html.WithClasses(!chromaInline)),
},
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
+28 -10
View File
@@ -31,6 +31,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/dankgo/wlclipboard"
"github.com/spf13/cobra"
)
@@ -63,6 +64,15 @@ var clipPasteCmd = &cobra.Command{
Run: runClipPaste,
}
var clipSendPasteCmd = &cobra.Command{
Use: "send-paste",
Short: "Send a paste keystroke to the focused window",
Long: "Emulate ctrl+v (or ctrl+shift+v with --shift) via a virtual keyboard. Works without server.",
Run: runClipSendPaste,
}
var clipSendPasteShift bool
var clipWatchCmd = &cobra.Command{
Use: "watch [command]",
Short: "Watch clipboard for changes",
@@ -216,8 +226,10 @@ func init() {
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
clipSendPasteCmd.Flags().BoolVarP(&clipSendPasteShift, "shift", "s", false, "Send ctrl+shift+v (terminal paste)")
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipSendPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
}
func runClipCopy(cmd *cobra.Command, args []string) {
@@ -272,8 +284,8 @@ func runClipCopy(cmd *cobra.Command, args []string) {
}
}
func parseMultiOffers(data []byte) ([]clipboard.Offer, error) {
var offers []clipboard.Offer
func parseMultiOffers(data []byte) ([]wlclipboard.Offer, error) {
var offers []wlclipboard.Offer
pos := 0
for pos < len(data) {
@@ -300,20 +312,26 @@ func parseMultiOffers(data []byte) ([]clipboard.Offer, error) {
offerData := data[pos : pos+dataLen]
pos += dataLen
offers = append(offers, clipboard.Offer{MimeType: mimeType, Data: offerData})
offers = append(offers, wlclipboard.Offer{MimeType: mimeType, Data: offerData})
}
return offers, nil
}
func runClipPaste(cmd *cobra.Command, args []string) {
data, _, err := clipboard.Paste()
data, _, err := wlclipboard.Paste()
if err != nil {
log.Fatalf("paste: %v", err)
}
os.Stdout.Write(data)
}
func runClipSendPaste(cmd *cobra.Command, args []string) {
if err := clipboard.SendPasteKeystroke(clipSendPasteShift); err != nil {
log.Fatalf("send-paste: %v", err)
}
}
func runClipWatch(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -327,13 +345,13 @@ func runClipWatch(cmd *cobra.Command, args []string) {
switch {
case len(args) > 0:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) {
runCommand(args, data)
}); err != nil && err != context.Canceled {
log.Fatalf("Watch error: %v", err)
}
case clipWatchStore:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) {
if err := clipboard.Store(data, mimeType); err != nil {
log.Errorf("store: %v", err)
}
@@ -365,7 +383,7 @@ func runClipWatch(cmd *cobra.Command, args []string) {
log.Fatalf("Watch error: %v", err)
}
case clipJSONOutput:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) {
out := map[string]any{
"data": string(data),
"mimeType": mimeType,
@@ -378,7 +396,7 @@ func runClipWatch(cmd *cobra.Command, args []string) {
log.Fatalf("Watch error: %v", err)
}
default:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) {
os.Stdout.Write(data)
os.Stdout.WriteString("\n")
}); err != nil && err != context.Canceled {
@@ -914,7 +932,7 @@ func downloadToTempFile(rawURL string) (string, error) {
var contentType string
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
for attempt := range 3 {
if attempt > 0 {
time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
}
+41 -12
View File
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"runtime/debug"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
@@ -10,16 +11,22 @@ import (
)
var (
colorOutputFmt string
colorAutocopy bool
colorNotify bool
colorLowercase bool
colorOutputFmt string
colorAutocopy bool
colorNotify bool
colorLowercase bool
colorAllowMulti bool
)
var colorCmd = &cobra.Command{
Use: "color",
Short: "Color utilities",
Long: "Color utilities including picking colors from the screen",
Long: `Color utilities including picking colors from the screen.
This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle
See: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
}
var colorPickCmd = &cobra.Command{
@@ -29,6 +36,9 @@ var colorPickCmd = &cobra.Command{
Click on any pixel to capture its color, or press Escape to cancel.
This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle
Output format flags (mutually exclusive, default: --hex):
--hex - Hexadecimal (#RRGGBB)
--rgb - RGB values (R G B)
@@ -37,12 +47,16 @@ Output format flags (mutually exclusive, default: --hex):
--cmyk - CMYK values (C% M% Y% K%)
--json - JSON with all formats
Optional:
--raw - Removes ANSI escape codes and background colors. Use this when piping to other commands
Examples:
dms color pick # Pick color, output as hex
dms color pick --rgb # Output as RGB
dms color pick --json # Output all formats as JSON
dms color pick --hex -l # Output hex in lowercase
dms color pick -a # Auto-copy result to clipboard`,
dms color pick -a # Auto-copy result to clipboard
dms color pick --allow-multiple # Skip the one-picker-at-a-time guard`,
Run: runColorPick,
}
@@ -53,9 +67,11 @@ func init() {
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
colorPickCmd.Flags().Bool("raw", false, "Removes ANSI escape codes and background colors. Use this when piping to other commands")
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
colorPickCmd.Flags().BoolVar(&colorAllowMulti, "allow-multiple", false, "Open a picker even when another selection overlay is already open")
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
@@ -77,13 +93,18 @@ func runColorPick(cmd *cobra.Command, args []string) {
}
config := colorpicker.Config{
Format: format,
CustomFormat: colorOutputFmt,
Lowercase: colorLowercase,
Autocopy: colorAutocopy,
Notify: colorNotify,
Format: format,
CustomFormat: colorOutputFmt,
Lowercase: colorLowercase,
Autocopy: colorAutocopy,
Notify: colorNotify,
AllowMultiple: colorAllowMulti,
}
// One-shot process holding a few screen-sized buffers: skip GC cycles while it runs.
debug.SetGCPercent(-1)
debug.SetMemoryLimit(1 << 30)
picker := colorpicker.New(config)
color, err := picker.Run()
if err != nil {
@@ -113,7 +134,15 @@ func runColorPick(cmd *cobra.Command, args []string) {
if jsonOutput {
fmt.Println(output)
} else if color.IsDark() {
return
}
if raw, _ := cmd.Flags().GetBool("raw"); raw {
fmt.Printf("%s\n", output)
return
}
if color.IsDark() {
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
} else {
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
+341 -73
View File
@@ -9,6 +9,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
"github.com/spf13/cobra"
)
@@ -18,56 +19,16 @@ var versionCmd = &cobra.Command{
Run: runVersion,
}
var runCmd = &cobra.Command{
Use: "run",
Short: "Launch quickshell with DMS configuration",
Long: "Launch quickshell with DMS configuration (qs -c dms)",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
daemon, _ := cmd.Flags().GetBool("daemon")
session, _ := cmd.Flags().GetBool("session")
if daemon {
runShellDaemon(session)
} else {
runShellInteractive(session)
}
},
}
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Restart quickshell with DMS configuration",
Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
restartShell()
},
}
var restartDetachedCmd = &cobra.Command{
Use: "restart-detached <pid>",
Hidden: true,
Args: cobra.ExactArgs(1),
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
runDetachedRestart(args[0])
},
}
var killCmd = &cobra.Command{
Use: "kill",
Short: "Kill running DMS shell processes",
Long: "Kill all running quickshell processes with DMS configuration",
Run: func(cmd *cobra.Command, args []string) {
killShell()
},
}
var ipcCmd = &cobra.Command{
Use: "ipc [target] [function] [args...]",
Use: "ipc",
Short: "Send IPC commands to running DMS shell",
Long: `Send IPC commands to the running DMS shell.
dms ipc call <target> <function> [args...] invoke a command
dms ipc list list all targets and functions
Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
@@ -75,11 +36,23 @@ var ipcCmd = &cobra.Command{
},
}
var ipcListCmd = &cobra.Command{
Use: "list",
Short: "List all IPC targets and functions",
Run: func(cmd *cobra.Command, args []string) {
printIPCHelp()
},
}
func init() {
ipcCmd.AddCommand(ipcListCmd)
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
_ = findConfig(cmd, args)
printIPCHelp()
})
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
pluginsLockCmd.Flags().StringP("output", "o", "", "Also write the lockfile to this path")
pluginsRestoreCmd.Flags().Bool("prune", false, "Remove managed plugins that are not in the lockfile")
}
var debugSrvCmd = &cobra.Command{
@@ -158,10 +131,22 @@ var pluginsUninstallCmd = &cobra.Command{
}
var pluginsUpdateCmd = &cobra.Command{
Use: "update <plugin-id>",
Short: "Update a plugin by ID",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
Args: cobra.ExactArgs(1),
Use: "update [plugin-id]",
Short: "Update a plugin by ID, or all plugins",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). If --all or -a is specified, all installed plugins will be updated.",
Args: func(cmd *cobra.Command, args []string) error {
updateAll, _ := cmd.Flags().GetBool("all")
if updateAll {
if len(args) > 0 {
return fmt.Errorf("cannot specify plugin ID when using --all/-a")
}
return nil
}
if len(args) != 1 {
return fmt.Errorf("requires exactly 1 arg (plugin ID) or use --all/-a")
}
return nil
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
@@ -169,12 +154,62 @@ var pluginsUpdateCmd = &cobra.Command{
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
checkOnly, _ := cmd.Flags().GetBool("check")
updateAll, _ := cmd.Flags().GetBool("all")
if checkOnly {
if updateAll {
if err := checkAllPluginsCLI(); err != nil {
log.Fatalf("Error checking updates: %v", err)
}
return
}
if err := checkPluginCLI(args[0]); err != nil {
log.Fatalf("Error checking update: %v", err)
}
return
}
if updateAll {
if err := updateAllPluginsCLI(); err != nil {
log.Fatalf("Error updating plugins: %v", err)
}
return
}
if err := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err)
}
},
}
var pluginsLockCmd = &cobra.Command{
Use: "lock",
Short: "Record installed plugins and their exact revisions",
Long: "Write a portable plugins.lock.json containing every managed user plugin and its current Git commit.",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
if err := lockPluginsCLI(output); err != nil {
log.Fatalf("Error writing plugin lockfile: %v", err)
}
},
}
var pluginsRestoreCmd = &cobra.Command{
Use: "restore [lockfile]",
Short: "Restore plugins from exact revisions in a lockfile",
Long: "Install or reset all plugins in a portable plugins.lock.json. Existing managed plugins are retained unless --prune is specified.",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
path := ""
if len(args) == 1 {
path = args[0]
}
prune, _ := cmd.Flags().GetBool("prune")
if err := restorePluginsCLI(path, prune); err != nil {
log.Fatalf("Error restoring plugins: %v", err)
}
},
}
func runVersion(cmd *cobra.Command, args []string) {
fmt.Printf("%s\n", formatVersion(Version))
}
@@ -204,29 +239,41 @@ func formatVersion(version string) string {
return fmt.Sprintf("dms %s", version)
}
var baseVersionRe = regexp.MustCompile(`^([\d.]+)`)
// Installed UI trees, for builds without an embedded UI.
var shellVersionPaths = []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
}
func getBaseVersion() string {
paths := []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
if ver := parseBaseVersion(shellembed.Version()); ver != "" {
return ver
}
for _, path := range paths {
if content, err := os.ReadFile(path); err == nil {
ver := strings.TrimSpace(string(content))
ver = strings.TrimPrefix(ver, "v")
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
if matches := re.FindStringSubmatch(ver); matches != nil {
return matches[1]
}
}
for _, path := range shellVersionPaths {
content, err := os.ReadFile(path)
if err != nil {
continue
}
if ver := parseBaseVersion(string(content)); ver != "" {
return ver
}
}
// Fallback
return "1.0.2"
}
func parseBaseVersion(raw string) string {
matches := baseVersionRe.FindStringSubmatch(strings.TrimPrefix(strings.TrimSpace(raw), "v"))
if matches == nil {
return ""
}
return matches[1]
}
func startDebugServer() error {
server.CLIVersion = Version
return server.Start(true)
@@ -254,6 +301,13 @@ func browsePlugins() error {
return nil
}
feedback := plugins.FetchFeedback()
nameByID := make(map[string]string, len(pluginList))
for _, plugin := range pluginList {
nameByID[plugin.ID] = plugin.Name
}
fmt.Printf("\nAvailable Plugins (%d):\n\n", len(pluginList))
for _, plugin := range pluginList {
installed, _ := manager.IsInstalled(plugin)
@@ -277,6 +331,26 @@ func browsePlugins() error {
if len(plugin.Dependencies) > 0 {
fmt.Printf(" Dependencies: %s\n", strings.Join(plugin.Dependencies, ", "))
}
if fb, ok := feedback[plugin.ID]; ok {
fmt.Printf(" Upvotes: %d\n", fb.Upvotes)
if len(fb.Status) > 0 {
fmt.Printf(" Status: %s\n", strings.Join(fb.Status, ", "))
}
if fb.IssueURL != "" {
fmt.Printf(" Discuss: %s\n", fb.IssueURL)
}
if len(fb.Similar) > 0 {
names := make([]string, len(fb.Similar))
for i, id := range fb.Similar {
if name, ok := nameByID[id]; ok {
names[i] = name
} else {
names[i] = id
}
}
fmt.Printf(" Related: %s\n", strings.Join(names, ", "))
}
}
fmt.Println()
}
@@ -317,7 +391,11 @@ func listInstalledPlugins() error {
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok {
fmt.Printf(" %s\n", plugin.Name)
hasUpdateStr := ""
if hasUpdates, _, err := manager.HasUpdates(id, plugin); err == nil && hasUpdates {
hasUpdateStr = " (update available)"
}
fmt.Printf(" %s%s\n", plugin.Name, hasUpdateStr)
fmt.Printf(" ID: %s\n", plugin.ID)
fmt.Printf(" Category: %s\n", plugin.Category)
fmt.Printf(" Author: %s\n", plugin.Author)
@@ -387,6 +465,40 @@ func installPluginCLI(idOrName string) error {
return nil
}
func lockPluginsCLI(outputPath string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
warnings, err := manager.WriteCurrentLockfile(outputPath)
if err != nil {
return err
}
for _, warning := range warnings {
fmt.Fprintf(os.Stderr, "Warning: %s\n", warning)
}
fmt.Printf("Plugin lockfile written: %s\n", manager.GetLockfilePath())
if outputPath != "" && outputPath != manager.GetLockfilePath() {
fmt.Printf("Plugin lockfile exported: %s\n", outputPath)
}
return nil
}
func restorePluginsCLI(path string, prune bool) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
if err := manager.RestoreFromLockfile(path, prune); err != nil {
return err
}
if path == "" {
path = manager.GetLockfilePath()
}
fmt.Printf("Plugins restored from lockfile: %s\n", path)
return nil
}
func getAvailablePluginIDs() []string {
registry, err := plugins.NewRegistry()
if err != nil {
@@ -497,16 +609,168 @@ func updatePluginCLI(idOrName string) error {
return nil
}
func updateAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var errs []error
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
if plugin != nil {
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Update(*plugin); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", plugin.Name)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", plugin.Name, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
}
} else {
fmt.Printf("Updating plugin: %s\n", pluginID)
if err := manager.UpdateByIDOrName(pluginID); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", pluginID)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", pluginID, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", pluginID)
}
}
}
if len(errs) > 0 {
for _, err := range errs {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
return fmt.Errorf("failed to update some plugins")
}
return nil
}
func checkPluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
hasUpdates, _, err := manager.HasUpdates(plugin.ID, *plugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
} else {
fmt.Printf("Plugin is up to date: %s\n", plugin.Name)
}
return nil
}
dummyPlugin := plugins.Plugin{ID: idOrName}
hasUpdates, _, err := manager.HasUpdates(idOrName, dummyPlugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s\n", idOrName)
} else {
fmt.Printf("Plugin is up to date: %s\n", idOrName)
}
return nil
}
func checkAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var count int
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
var hasUpdates bool
var name string
if plugin != nil {
name = plugin.Name
hasUpdates, _, _ = manager.HasUpdates(pluginID, *plugin)
} else {
name = pluginID
dummyPlugin := plugins.Plugin{ID: pluginID}
hasUpdates, _, _ = manager.HasUpdates(pluginID, dummyPlugin)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", name, pluginID)
count++
}
}
if count > 0 {
fmt.Printf("\nFound %d plugin(s) with available updates.\n", count)
} else {
fmt.Println("All plugins are up to date.")
}
return nil
}
func getCommonCommands() []*cobra.Command {
return []*cobra.Command{
commands := shellApp.Commands()
return append(commands, []*cobra.Command{
versionCmd,
runCmd,
restartCmd,
restartDetachedCmd,
killCmd,
ipcCmd,
debugSrvCmd,
pluginsCmd,
registryCmd,
dank16Cmd,
brightnessCmd,
dpmsCmd,
@@ -514,6 +778,7 @@ func getCommonCommands() []*cobra.Command {
greeterCmd,
setupCmd,
colorCmd,
qrCmd,
screenshotCmd,
notifyActionCmd,
notifyCmd,
@@ -526,5 +791,8 @@ func getCommonCommands() []*cobra.Command {
dlCmd,
randrCmd,
blurCmd,
}
trashCmd,
systemCmd,
switchUserCmd,
}...)
}
+63 -28
View File
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/luaconfig"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra"
)
@@ -27,7 +28,24 @@ var resolveIncludeCmd = &cobra.Command{
case 0:
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
case 1:
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
return []string{
"binds.lua",
"binds-user.lua",
"colors.lua",
"layout.lua",
"outputs.lua",
"cursor.lua",
"windowrules.lua",
"cursor.kdl",
"layout.kdl",
"outputs.kdl",
"binds.kdl",
"input.kdl",
"cursor.conf",
"layout.conf",
"outputs.conf",
"binds.conf",
}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
@@ -39,8 +57,10 @@ func init() {
}
type IncludeResult struct {
Exists bool `json:"exists"`
Included bool `json:"included"`
Exists bool `json:"exists"`
Included bool `json:"included"`
ConfigFormat string `json:"configFormat,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
}
func runResolveInclude(cmd *cobra.Command, args []string) {
@@ -55,7 +75,7 @@ func runResolveInclude(cmd *cobra.Command, args []string) {
result, err = checkHyprlandInclude(filename)
case "niri":
result, err = checkNiriInclude(filename)
case "mangowc", "dwl", "mango":
case "mangowc", "mango":
result, err = checkMangoWCInclude(filename)
default:
log.Fatalf("Unknown compositor: %s", compositor)
@@ -70,10 +90,7 @@ func runResolveInclude(cmd *cobra.Command, args []string) {
}
func checkHyprlandInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
if err != nil {
return IncludeResult{}, err
}
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
@@ -82,17 +99,41 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
result.Exists = true
}
mainConfig := filepath.Join(configDir, "hyprland.conf")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
return result, nil
targetAbs, err := filepath.Abs(targetPath)
if err != nil {
return result, err
}
targetRel := filepath.ToSlash(filepath.Join("dms", filename))
mainLua := filepath.Join(configDir, "hyprland.lua")
if _, err := os.Stat(mainLua); err == nil {
result.ConfigFormat = "lua"
result.ReadOnly = false
processedLua := make(map[string]bool)
if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) {
result.Included = true
return result, nil
}
}
mainConf := filepath.Join(configDir, "hyprland.conf")
if _, err := os.Stat(mainConf); err == nil {
if result.ConfigFormat == "" {
result.ConfigFormat = "hyprlang"
result.ReadOnly = true
}
processed := make(map[string]bool)
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
result.Included = true
return result, nil
}
}
processed := make(map[string]bool)
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
return result, nil
}
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]bool) bool {
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
@@ -109,9 +150,9 @@ func hyprlandFindInclude(filePath, target string, processed map[string]bool) boo
}
baseDir := filepath.Dir(absPath)
lines := strings.Split(string(data), "\n")
lines := strings.SplitSeq(string(data), "\n")
for _, line := range lines {
for line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
@@ -141,7 +182,7 @@ func hyprlandFindInclude(filePath, target string, processed map[string]bool) boo
continue
}
if hyprlandFindInclude(expanded, target, processed) {
if hyprlandFindIncludeHyprlang(expanded, target, processed) {
return true
}
}
@@ -150,10 +191,7 @@ func hyprlandFindInclude(filePath, target string, processed map[string]bool) boo
}
func checkNiriInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/niri")
if err != nil {
return IncludeResult{}, err
}
configDir := filepath.Join(utils.XDGConfigHome(), "niri")
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
@@ -191,7 +229,7 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
baseDir := filepath.Dir(absPath)
content := string(data)
for _, line := range strings.Split(content, "\n") {
for line := range strings.SplitSeq(content, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
continue
@@ -229,10 +267,7 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
}
func checkMangoWCInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/mango")
if err != nil {
return IncludeResult{}, err
}
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
@@ -271,9 +306,9 @@ func mangowcFindInclude(filePath, target string, processed map[string]bool) bool
}
baseDir := filepath.Dir(absPath)
lines := strings.Split(string(data), "\n")
lines := strings.SplitSeq(string(data), "\n")
for _, line := range lines {
for line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
+5 -5
View File
@@ -79,11 +79,11 @@ func runDank16(cmd *cobra.Command, args []string) {
primaryLight = primaryColor
}
variantOpts := dank16.VariantOptions{
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
Background: background,
UseDPS: contrastAlgo == "dps",
IsLightMode: isLight,
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
BackgroundDark: background,
UseDPS: contrastAlgo == "dps",
IsLightMode: isLight,
}
variantColors := dank16.GenerateVariantPalette(variantOpts)
fmt.Print(dank16.GenerateVariantJSON(variantColors))
+578 -37
View File
@@ -2,7 +2,9 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"os/exec"
"path/filepath"
@@ -15,6 +17,7 @@ import (
"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"
@@ -92,6 +95,8 @@ var (
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
mangowcVersionRegex = regexp.MustCompile(`mango (\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+)?)`)
)
var doctorCmd = &cobra.Command{
@@ -125,6 +130,7 @@ const (
catConfigFiles
catServices
catEnvironment
catFonts
)
func (c category) String() string {
@@ -147,6 +153,8 @@ func (c category) String() string {
return "Services"
case catEnvironment:
return "Environment"
case catFonts:
return "Fonts"
default:
return "Unknown"
}
@@ -213,6 +221,7 @@ func runDoctor(cmd *cobra.Command, args []string) {
checkConfigurationFiles(),
checkSystemdServices(),
checkEnvironmentVars(),
checkFonts(),
)
switch {
@@ -315,7 +324,16 @@ func checkSystemInfo() []checkResult {
func checkEnvironmentVars() []checkResult {
var results []checkResult
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")...)
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")...)
}
results = append(results, checkEnvVar("QS_ICON_THEME")...)
results = append(results, checkXDGMenuPrefix()...)
if matugen.QtengineActive() {
results = append(results, checkQtenginePlugin()...)
}
return results
}
@@ -330,6 +348,251 @@ func checkEnvVar(name string) []checkResult {
return nil
}
func checkXDGMenuPrefix() []checkResult {
menuPrefix := os.Getenv("XDG_MENU_PREFIX")
if menuPrefix != "" {
if checkXDGMenuFile(menuPrefix) {
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"}}
}
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"}}
}
if doctorVerbose {
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, "Not set", "", doctorDocsURL + "#xdg-menu-prefix"}}
}
return nil
}
// qtQueryBinaries are the Qt build tools that can be asked where Qt looks for
// plugins, in probe order. 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 is
// asked for QT_VERSION and the major is taken from the answer.
var qtQueryBinaries = []struct{ bin, flag string }{
{"qmake6", "-query"},
{"qmake-qt6", "-query"},
{"qtpaths6", "--query"},
{"qtpaths-qt6", "--query"},
{"qmake-qt5", "-query"},
{"qtpaths-qt5", "--query"},
{"qmake", "-query"},
{"qtpaths", "--query"},
}
// checkQtenginePlugin verifies the qtengine platform theme plugin is installed
// where Qt actually searches, for every Qt major that can be interrogated.
//
// 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"
pluginRoots := qtPluginRoots()
// 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.
if len(pluginRoots) == 0 {
return []checkResult{{
catEnvironment, "qtengine plugin", statusInfo, "Cannot verify (no qmake/qtpaths)",
"Install a Qt development package to let dms doctor check the plugin against the path Qt actually searches.",
url,
}}
}
// Qt also searches QT_PLUGIN_PATH, so a plugin found there is loadable.
envRoots := filepath.SplitList(os.Getenv("QT_PLUGIN_PATH"))
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})
}
return results
}
// checkQtPlatformThemePlugin validates one of the QT_QPA_PLATFORMTHEME
// variables beyond mere presence: the value must name a platform theme whose
// plugin Qt can actually load. A wrong value fails silently at run time — Qt
// apps fall back to a built-in theme, icon lookups return nothing, and
// Quickshell tray menus draw a placeholder for every icon an app publishes.
func checkQtPlatformThemePlugin(varName string) []checkResult {
value := os.Getenv(varName)
// Unset is reported by checkEnvVar; qtengine has its own plugin check.
if value == "" || value == "qtengine" {
return nil
}
url := doctorDocsURL + "#environment-variables"
// qt6ct-kde is the AUR package name, not the theme value: with it set,
// Qt finds no platform theme at all and every Qt app falls back silently.
if value == "qt6ct-kde" {
return []checkResult{{
catEnvironment, value, statusWarn, "Package name, not a theme",
fmt.Sprintf("qt6ct-kde is the AUR package name; the theme value Qt expects is qt6ct. Qt apps get no platform theme, so palettes stay unstyled and icon lookups fail. Set %s=qt6ct, then restart the shell.", varName),
url,
}}
}
// DMS themes Qt6 through these variables; qt5ct is the Qt5 counterpart.
major := "6"
if value == "qt5ct" {
major = "5"
}
files := qtPlatformThemePluginFiles(value)
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
// that could not be interrogated, and never claim it is fine either.
if root == "" {
return []checkResult{{
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.",
url,
}}
}
// 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...)
if plugin == "" {
// plasma-integration ships the kde theme under a file name that has
// changed across versions; a miss says nothing reliable about the
// install, so degrade to info instead of warning at a working setup.
if value == "kde" {
return []checkResult{{
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.",
url,
}}
}
expected := filepath.Join(root, "platformthemes", files[0])
return []checkResult{{
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),
url,
}}
}
return []checkResult{{catEnvironment, name, statusOK, "Installed", plugin, url}}
}
// qtPlatformThemePluginFiles lists the plugin file names that can provide a
// QT_QPA_PLATFORMTHEME value, most conventional first. Qt resolves the value
// 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
// libq<value> naming convention.
func qtPlatformThemePluginFiles(value string) []string {
if files, ok := map[string][]string{
"qt6ct": {"libqt6ct.so"},
"qt5ct": {"libqt5ct.so"},
"gtk3": {"libqgtk3.so"},
"kde": {"libqkdeplatformtheme.so"},
}[value]; ok {
return files
}
return []string{"libq" + value + ".so", "lib" + value + ".so"}
}
// 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
// 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.
func qtPluginRoots() map[string]string {
pluginRoots := make(map[string]string)
for _, q := range qtQueryBinaries {
if _, err := exec.LookPath(q.bin); err != nil {
continue
}
major, _, _ := strings.Cut(qtQuery(q.bin, q.flag, "QT_VERSION"), ".")
if major == "" || pluginRoots[major] != "" {
continue
}
if root := qtQuery(q.bin, q.flag, "QT_INSTALL_PLUGINS"); root != "" {
pluginRoots[major] = root
}
}
return pluginRoots
}
// qtPluginPath returns the first of roots that holds any of files under
// platformthemes/, or "" if none does.
func qtPluginPath(roots []string, files ...string) string {
for _, root := range roots {
if root == "" {
continue
}
for _, file := range files {
path := filepath.Join(root, "platformthemes", file)
if _, err := os.Stat(path); err == nil {
return path
}
}
}
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.
func qtQuery(bin, flag, key string) string {
output, err := exec.Command(bin, flag, key).Output()
if err != nil {
return ""
}
return qtQueryValue(string(output), key)
}
// qtQueryValue extracts one property from a Qt query. Most tools print the bare
// value, but some ignore the requested property and dump every one as
// "KEY:value" lines; taking the whole output there would yield a bogus path.
func qtQueryValue(output, key string) string {
lines := strings.Split(strings.TrimSpace(output), "\n")
for _, line := range lines {
if value, found := strings.CutPrefix(strings.TrimSpace(line), key+":"); found {
return strings.TrimSpace(value)
}
}
if len(lines) == 1 {
return strings.TrimSpace(lines[0])
}
return ""
}
func checkXDGMenuFile(prefix string) bool {
menuPath := fmt.Sprintf("/etc/xdg/menus/%sapplications.menu", prefix)
_, err := os.Stat(menuPath)
return err == nil
}
func readOSRelease() map[string]string {
result := make(map[string]string)
data, err := os.ReadFile("/etc/os-release")
@@ -355,9 +618,8 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
}
qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures)
qsDetails := ""
if doctorVerbose && qsPath != "" {
qsVersion, qsStatus, qsPath, qsDetails := getQuickshellVersionInfo(qsMissingFeatures)
if doctorVerbose && qsPath != "" && qsDetails == "" {
qsDetails = qsPath
}
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
@@ -373,12 +635,12 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
}
func getDMSShellVersion() (version, path string) {
if err := findConfig(nil, nil); err == nil && configPath != "" {
versionFile := filepath.Join(configPath, "VERSION")
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
versionFile := filepath.Join(shellApp.ConfigPath(), "VERSION")
if data, err := os.ReadFile(versionFile); err == nil {
return strings.TrimSpace(string(data)), configPath
return strings.TrimSpace(string(data)), shellApp.ConfigPath()
}
return "installed", configPath
return "installed", shellApp.ConfigPath()
}
if dmsPath, err := config.LocateDMSConfig(); err == nil {
@@ -392,41 +654,49 @@ func getDMSShellVersion() (version, path string) {
return "", ""
}
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string, string) {
if !utils.CommandExists("qs") {
return "Not installed", statusError, ""
return "Not installed", statusError, "", ""
}
qsPath, _ := exec.LookPath("qs")
output, err := exec.Command("qs", "--version").Output()
if err != nil {
return "Installed (version check failed)", statusWarn, qsPath
details := "Run 'qs --version' to inspect the failure."
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
details = quickshellVersionFailureDetails(string(exitErr.Stderr))
}
return "Installed (version check failed)", statusWarn, qsPath, details
}
fullVersion := strings.TrimSpace(string(output))
if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 {
if version.CompareVersions(matches[1], "0.2.0") < 0 {
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath, ""
}
if missingFeatures {
return fullVersion, statusWarn, qsPath
return fullVersion, statusWarn, qsPath, ""
}
return fullVersion, statusOK, qsPath
return fullVersion, statusOK, qsPath, ""
}
return fullVersion, statusWarn, qsPath
return fullVersion, statusWarn, qsPath, ""
}
func quickshellVersionFailureDetails(stderr string) string {
if strings.Contains(stderr, "undefined symbol:") && strings.Contains(stderr, "Qt_6_PRIVATE_API") {
return "Quickshell is incompatible with the installed Qt libraries. Rebuild or reinstall Quickshell against the current Qt version."
}
return "Run 'qs --version' to inspect the failure."
}
func checkDMSInstallation() []checkResult {
var results []checkResult
dmsPath := ""
if err := findConfig(nil, nil); err == nil && configPath != "" {
dmsPath = configPath
} else if path, err := config.LocateDMSConfig(); err == nil {
dmsPath = path
}
dmsPath := resolveDoctorShellPath()
if dmsPath == "" {
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
@@ -471,6 +741,8 @@ func checkWindowManagers() []checkResult {
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
{"Scroll", "scroll", "--version", scrollVersionRegex, []string{"scroll"}},
{"Aqueous", "aqueous", "-version", aqueousVersionRegex, []string{"aqueous"}},
}
var results []checkResult
@@ -503,7 +775,7 @@ func checkWindowManagers() []checkResult {
results = append(results, checkResult{
catCompositor, "Compositor", statusError,
"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, or Aqueous",
doctorDocsURL + "#compositor-checks",
})
}
@@ -512,11 +784,28 @@ func checkWindowManagers() []checkResult {
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
}
if os.Getenv("AQUEOUS_SOCKET") != "" {
results = append(results, checkAqueousConfigHelper())
}
results = append(results, checkCompositorBlurSupport())
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 {
supported, err := blur.ProbeSupport()
if err != nil {
@@ -549,10 +838,14 @@ func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
func detectRunningWM() string {
switch {
case os.Getenv("AQUEOUS_SOCKET") != "":
return "Aqueous"
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
return "Hyprland"
case os.Getenv("NIRI_SOCKET") != "":
return "niri"
case os.Getenv("MANGO_INSTANCE_SIGNATURE") != "":
return "MangoWC"
case os.Getenv("MIRACLESOCK") != "":
return "Miracle WM"
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
@@ -641,7 +934,7 @@ ShellRoot {
{"Polkit", "Authentication prompts"},
{"IdleMonitor", "Idle detection"},
{"IdleInhibitor", "Prevent idle/sleep"},
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
{"ShortcutInhibitor", "Allow shortcut management (niri, Aqueous)"},
{"BackgroundBlur", "Background blur API support in Quickshell"},
}
@@ -764,17 +1057,15 @@ func findQtPluginDirs() []string {
}
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
for dir := range strings.SplitSeq(envPath, ":") {
addDir(dir)
}
for _, dir := range filepath.SplitList(os.Getenv("QT_PLUGIN_PATH")) {
addDir(dir)
}
// Try qtpaths
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
addDir(strings.TrimSpace(string(output)))
for _, q := range qtQueryBinaries {
if _, err := exec.LookPath(q.bin); err != nil {
continue
}
addDir(qtQuery(q.bin, q.flag, "QT_INSTALL_PLUGINS"))
}
// Fallback: common distro paths
@@ -803,6 +1094,8 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
return "systemd-networkd"
case network.BackendConnMan:
return "ConnMan"
case network.BackendWpaSupplicant:
return "wpa_supplicant"
default:
return ""
}
@@ -876,10 +1169,10 @@ func checkOptionalDependencies() []checkResult {
important bool
}{
{"matugen", "matugen", "Dynamic theming", true},
{"dgop", "dgop", "System monitoring", true},
{"cava", "cava", "Audio visualizer", true},
{"khal", "khal", "Calendar events", false},
{"danksearch", "dsearch", "File search", false},
{"dankcalendar", "dcal", "Calendar app", false},
{"fprintd", "fprintd-list", "Fingerprint auth", false},
}
@@ -896,9 +1189,62 @@ func checkOptionalDependencies() []checkResult {
}
}
results = append(results, checkAdwGtk3())
return results
}
// Mirrors the search in quickshell/scripts/gtk.sh: user theme dirs, then XDG_DATA_DIRS.
func checkAdwGtk3() checkResult {
optionalFeaturesURL := doctorDocsURL + "#optional-features"
home, _ := os.UserHomeDir()
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = filepath.Join(home, ".local", "share")
}
userRoots := []string{filepath.Join(dataHome, "themes"), filepath.Join(home, ".themes")}
dataDirs := os.Getenv("XDG_DATA_DIRS")
if dataDirs == "" {
dataDirs = "/usr/local/share:/usr/share"
}
var systemRoots []string
for _, d := range strings.Split(dataDirs, ":") {
if d != "" {
systemRoots = append(systemRoots, filepath.Join(d, "themes"))
}
}
locate := func(roots []string, variant string) bool {
for _, root := range roots {
if info, err := os.Stat(filepath.Join(root, "adw-gtk3"+variant, "gtk-3.0")); err == nil && info.IsDir() {
return true
}
}
return false
}
userLight := locate(userRoots, "")
userDark := locate(userRoots, "-dark")
sysLight := locate(systemRoots, "")
sysDark := locate(systemRoots, "-dark")
switch {
case userLight && userDark:
return checkResult{catOptionalFeatures, "adw-gtk3", statusOK, "Installed", "GTK3 dynamic theming (user copy)", optionalFeaturesURL}
case sysLight && sysDark:
return checkResult{catOptionalFeatures, "adw-gtk3", statusOK, "Installed", "GTK3 dynamic theming (system copy, applied on next theme apply)", optionalFeaturesURL}
case userLight || userDark || sysLight || sysDark:
missing := "adw-gtk3-dark"
if userDark || sysDark {
missing = "adw-gtk3"
}
return checkResult{catOptionalFeatures, "adw-gtk3", statusWarn, "Missing " + missing, "GTK3 dynamic theming needs both variants", optionalFeaturesURL}
default:
return checkResult{catOptionalFeatures, "adw-gtk3", statusInfo, "Not installed", "GTK3 dynamic theming; without it the global CSS override applies", optionalFeaturesURL}
}
}
func checkConfigurationFiles() []checkResult {
configDir, _ := os.UserConfigDir()
cacheDir, _ := os.UserCacheDir()
@@ -926,6 +1272,11 @@ func checkConfigurationFiles() []checkResult {
status = statusWarn
message += " (read-only)"
}
// The shell falls back to defaults on a parse failure, so a broken file must not report ok.
if data, readErr := os.ReadFile(cf.path); readErr == nil && len(data) > 0 && !json.Valid(data) {
status = statusError
message = "Invalid JSON - the shell ignores this file and runs on defaults"
}
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
}
return results
@@ -947,14 +1298,24 @@ func checkSystemdServices() []checkResult {
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
}
switch {
case dmsState.active == "failed":
status = statusError
case dmsState.active == "active":
case dmsState.enabled == "disabled":
status, message = statusWarn, "Disabled"
case dmsState.active == "failed" || dmsState.active == "inactive":
case dmsState.active == "inactive":
status = statusError
}
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
}
if dmsState.exists && dmsState.enabled == "enabled" {
gsState := getServiceState("graphical-session.target", true)
if gsState.exists && gsState.active != "active" {
results = append(results, checkResult{catServices, "graphical-session.target", statusWarn, "Inactive", "Compositor session never activated it, so dms.service cannot autostart. On Hyprland, hyprland-session.target must exist and be started by the compositor.", doctorDocsURL + "#services"})
}
}
greetdState := getServiceState("greetd", false)
switch {
case greetdState.exists:
@@ -1110,14 +1471,14 @@ func formatResultsPlain(results []checkResult) string {
if currentCategory != -1 {
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
currentCategory = r.category
}
sb.WriteString(fmt.Sprintf("- [%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 != "" {
sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
fmt.Fprintf(&sb, " - %s\n", r.details)
}
}
@@ -1127,8 +1488,188 @@ func formatResultsPlain(results []checkResult) string {
}
sb.WriteString("\n---\n")
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
return sb.String()
}
const (
defaultDoctorFontFamily = "Inter Variable"
defaultDoctorMonoFontFamily = "Fira Code"
)
// 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{
"inter variable": {
"DankCommon/assets/fonts/inter/InterVariable.ttf",
"assets/fonts/inter/InterVariable.ttf",
},
"fira code": {
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
},
"firacode nerd font": {
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
},
}
func resolveDoctorShellPath() string {
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
return shellApp.ConfigPath()
}
if path, err := config.LocateDMSConfig(); err == nil {
return path
}
return ""
}
func findBundledFontFile(shellPath, family string) string {
if shellPath == "" {
return ""
}
relPaths, ok := bundledFontRelPaths[strings.ToLower(strings.TrimSpace(family))]
if !ok {
return ""
}
for _, rel := range relPaths {
path := filepath.Join(shellPath, rel)
if info, err := os.Stat(path); err == nil && !info.IsDir() {
return path
}
}
return ""
}
func isBundledDefaultFont(family string) bool {
_, ok := bundledFontRelPaths[strings.ToLower(strings.TrimSpace(family))]
return ok
}
func fontInFcList(name, cacheLower string) bool {
target := strings.ToLower(strings.TrimSpace(name))
if target == "" {
return false
}
for line := range strings.SplitSeq(cacheLower, "\n") {
for fam := range strings.SplitSeq(strings.TrimSpace(line), ",") {
if strings.TrimSpace(fam) == target {
return true
}
}
}
return false
}
func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailable bool, url string) checkResult {
if bundled := findBundledFontFile(shellPath, family); bundled != "" {
details := "Bundled (Qt FontLoader)"
if doctorVerbose {
details = bundled
}
return checkResult{catFonts, label, statusOK, family, details, url}
}
if isBundledDefaultFont(family) {
if shellPath == "" {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not verified", family),
"Could not locate shell config to verify bundled font files.",
url,
}
}
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' bundled file missing", family),
"Expected font file missing from shell install. Reinstall DMS or check DankCommon assets.",
url,
}
}
if !fcListAvailable {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not verified", family),
"fc-list not installed; cannot verify custom fonts in fontconfig.",
url,
}
}
if fcCache == "" {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not found", family),
"Fontconfig cache is empty or unreadable. Try running 'fc-cache -fv'.",
url,
}
}
if fontInFcList(family, fcCache) {
return checkResult{catFonts, label, statusOK, family, "Available via fontconfig", url}
}
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not found", family),
"Font is not registered with fontconfig. Try running 'fc-cache -fv' or install the font.",
url,
}
}
func checkFonts() []checkResult {
var results []checkResult
url := doctorDocsURL + "#fonts"
fontFamily := defaultDoctorFontFamily
monoFontFamily := defaultDoctorMonoFontFamily
if configDir, err := os.UserConfigDir(); err == nil {
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
if data, err := os.ReadFile(settingsPath); err == nil {
var settings struct {
FontFamily string `json:"fontFamily"`
MonoFontFamily string `json:"monoFontFamily"`
}
if err := json.Unmarshal(data, &settings); err == nil {
if settings.FontFamily != "" {
fontFamily = settings.FontFamily
}
if settings.MonoFontFamily != "" {
monoFontFamily = settings.MonoFontFamily
}
}
}
}
shellPath := resolveDoctorShellPath()
needFontconfig := !isBundledDefaultFont(fontFamily) || !isBundledDefaultFont(monoFontFamily)
fcListAvailable := utils.CommandExists("fc-list")
fcCache := ""
if needFontconfig {
if !fcListAvailable {
results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify custom fonts in fontconfig cache.", url})
} else {
output, err := exec.Command("fc-list", ":", "family").Output()
if err != nil {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url})
} else {
fcCache = strings.ToLower(string(output))
if len(strings.TrimSpace(fcCache)) == 0 {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url})
}
}
}
}
results = append(results,
checkConfiguredFont("Normal Font", fontFamily, shellPath, fcCache, fcListAvailable, url),
checkConfiguredFont("Monospace Font", monoFontFamily, shellPath, fcCache, fcListAvailable, url),
)
return results
}
+237
View File
@@ -0,0 +1,237 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestQuickshellVersionFailureDetails(t *testing.T) {
tests := []struct {
name string
stderr string
want string
}{
{
name: "Qt private API symbol mismatch",
stderr: "qs: symbol lookup error: qs: undefined symbol: _ZN23QUntypedPropertyBindingC1EP23QPropertyBindingPrivate, version Qt_6_PRIVATE_API",
want: "Quickshell is incompatible with the installed Qt libraries. Rebuild or reinstall Quickshell against the current Qt version.",
},
{
name: "other failure",
stderr: "qs: unsupported option --version",
want: "Run 'qs --version' to inspect the failure.",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := quickshellVersionFailureDetails(tc.stderr); got != tc.want {
t.Fatalf("quickshellVersionFailureDetails(%q) = %q, want %q", tc.stderr, got, tc.want)
}
})
}
}
// Parsing a full property dump as a bare value would yield a bogus plugin root,
// making the qtengine check warn at a user whose install is fine.
func TestQtQueryValue(t *testing.T) {
const dumpAll = `QT_INSTALL_PREFIX:/usr
QT_INSTALL_PLUGINS:/usr/lib/qt/plugins
QT_VERSION:5.15.19`
tests := []struct {
name string
output string
key string
want string
}{
{
name: "bare value as qmake prints it",
output: "/usr/lib/qt6/plugins\n",
key: "QT_INSTALL_PLUGINS",
want: "/usr/lib/qt6/plugins",
},
{
name: "key:value line among a full property dump",
output: dumpAll,
key: "QT_INSTALL_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",
output: dumpAll,
key: "QT_INSTALL_LIBEXECS",
want: "",
},
{
name: "empty output yields nothing",
output: "",
key: "QT_INSTALL_PLUGINS",
want: "",
},
{
name: "surrounding whitespace is trimmed",
output: " QT_INSTALL_PLUGINS: /usr/lib/qt/plugins \n",
key: "QT_INSTALL_PLUGINS",
want: "/usr/lib/qt/plugins",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := qtQueryValue(tc.output, tc.key); got != tc.want {
t.Fatalf("qtQueryValue(%q, %q) = %q, want %q", tc.output, tc.key, got, tc.want)
}
})
}
}
// stubQtQueryTool writes a fake Qt build tool answering QT_VERSION and
// QT_INSTALL_PLUGINS from the given values, and points PATH at its directory
// alone so the real toolchain cannot interfere.
func stubQtQueryTool(t *testing.T, bin, version, plugins string) string {
t.Helper()
dir := t.TempDir()
script := "#!/bin/sh\ncase \"$2\" in\n QT_VERSION) echo " + version +
" ;;\n QT_INSTALL_PLUGINS) echo " + plugins + " ;;\nesac\n"
if err := os.WriteFile(filepath.Join(dir, bin), []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir)
return dir
}
func writePlugin(t *testing.T, pluginsDir, file string) {
t.Helper()
if err := os.MkdirAll(filepath.Join(pluginsDir, "platformthemes"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(pluginsDir, "platformthemes", file), nil, 0o644); err != nil {
t.Fatal(err)
}
}
// qtenginePluginFile("", major) is a path with a directory component, and
// qtPluginPath joins platformthemes/ again — a regression here silently warns
// at every correctly installed system, so pin the full path down.
func TestQtenginePluginPath(t *testing.T) {
root := t.TempDir()
if got := qtenginePluginPath([]string{root}, "6"); got != "" {
t.Fatalf("missing plugin should yield an empty path, got %q", got)
}
want := filepath.Join(root, "platformthemes", "libqt6engine-plugin.so")
if err := os.MkdirAll(filepath.Join(root, "platformthemes"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(want, nil, 0o644); err != nil {
t.Fatal(err)
}
if got := qtenginePluginPath([]string{root}, "6"); got != want {
t.Fatalf("qtenginePluginPath = %q, want %q", got, want)
}
}
func TestCheckQtPlatformThemePlugin(t *testing.T) {
t.Run("qt6ct-kde is flagged as a package name, not a theme", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct-kde")
t.Setenv("PATH", t.TempDir()) // no Qt tools reachable
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 {
t.Fatalf("got %d results, want 1: %+v", len(results), results)
}
if results[0].status != statusWarn {
t.Fatalf("status = %q, want warn", results[0].status)
}
if !strings.Contains(results[0].details, "package name") || !strings.Contains(results[0].details, "qt6ct") {
t.Fatalf("details should name the package/value mixup, got %q", results[0].details)
}
})
t.Run("installed plugin reports OK", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
plugins := t.TempDir()
writePlugin(t, plugins, "libqt6ct.so")
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusOK {
t.Fatalf("got %+v, want one OK result", results)
}
if !strings.Contains(results[0].details, "libqt6ct.so") {
t.Fatalf("details should point at the plugin, got %q", results[0].details)
}
})
t.Run("missing plugin warns with the expected path", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
plugins := t.TempDir()
stubQtQueryTool(t, "qtpaths6", "6.11.2", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusWarn {
t.Fatalf("got %+v, want one warn result", results)
}
want := filepath.Join(plugins, "platformthemes", "libqt6ct.so")
if !strings.Contains(results[0].details, want) {
t.Fatalf("details should contain expected path %s, got %q", want, results[0].details)
}
})
t.Run("no Qt build tools degrades to info", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
t.Setenv("PATH", t.TempDir())
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusInfo {
t.Fatalf("got %+v, want one info result", results)
}
})
t.Run("unset and qtengine are left to their own checks", func(t *testing.T) {
t.Setenv("PATH", t.TempDir())
t.Setenv("QT_QPA_PLATFORMTHEME", "")
if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil {
t.Fatalf("unset should yield no results, got %+v", results)
}
t.Setenv("QT_QPA_PLATFORMTHEME", "qtengine")
if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil {
t.Fatalf("qtengine should yield no results, got %+v", results)
}
})
t.Run("qt6 variable is not satisfied by a Qt5 tool alone", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME_QT6", "qt6ct")
plugins := t.TempDir()
stubQtQueryTool(t, "qmake-qt5", "5.15.19", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME_QT6")
if len(results) != 1 || results[0].status != statusInfo {
t.Fatalf("got %+v, want one info result", results)
}
})
t.Run("kde plugin miss degrades to info", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "kde")
plugins := t.TempDir()
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusInfo {
t.Fatalf("got %+v, want one info result", results)
}
})
t.Run("gtk3 resolves through the libq convention", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "gtk3")
plugins := t.TempDir()
writePlugin(t, plugins, "libqgtk3.so")
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusOK {
t.Fatalf("got %+v, want one OK result", results)
}
})
}
+34 -47
View File
@@ -3,19 +3,19 @@ package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
"github.com/spf13/cobra"
)
var dlOutput string
var dlUserAgent string
var dlHeaders []string
var dlTimeout int
var dlConnectTimeout int
var dlIPv4Only bool
var dlCmd = &cobra.Command{
@@ -33,65 +33,52 @@ var dlCmd = &cobra.Command{
func init() {
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
dlCmd.Flags().StringArrayVarP(&dlHeaders, "header", "H", nil, "Extra request header as 'Name: value' (repeatable)")
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Total request timeout in seconds")
dlCmd.Flags().IntVar(&dlConnectTimeout, "connect-timeout", 5, "Connection timeout in seconds")
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
}
func runDownload(url string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("invalid request: %w", err)
func parseHeaders(raw []string) (map[string]string, error) {
if len(raw) == 0 {
return nil, nil
}
switch {
case dlUserAgent != "":
req.Header.Set("User-Agent", dlUserAgent)
default:
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
}
dialer := &net.Dialer{Timeout: 5 * time.Second}
transport := &http.Transport{DialContext: dialer.DialContext}
if dlIPv4Only {
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp4", addr)
headers := make(map[string]string, len(raw))
for _, entry := range raw {
name, value, found := strings.Cut(entry, ":")
if !found {
return nil, fmt.Errorf("invalid header %q: expected 'Name: value'", entry)
}
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("invalid header %q: empty name", entry)
}
headers[name] = strings.TrimSpace(value)
}
client := &http.Client{Transport: transport}
return headers, nil
}
resp, err := client.Do(req)
func runDownload(url string) error {
headers, err := parseHeaders(dlHeaders)
if err != nil {
return fmt.Errorf("download failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
if dlOutput == "" {
_, err = io.Copy(os.Stdout, resp.Body)
return err
}
if dir := filepath.Dir(dlOutput); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("mkdir failed: %w", err)
}
opts := netfetch.Options{
Headers: headers,
UserAgent: dlUserAgent,
Timeout: time.Duration(dlTimeout) * time.Second,
ConnectTimeout: time.Duration(dlConnectTimeout) * time.Second,
IPv4Only: dlIPv4Only,
}
f, err := os.Create(dlOutput)
if err != nil {
return fmt.Errorf("create failed: %w", err)
if dlOutput == "" {
return netfetch.ToWriter(context.Background(), url, opts, os.Stdout)
}
defer f.Close()
if _, err := io.Copy(f, resp.Body); err != nil {
os.Remove(dlOutput)
return fmt.Errorf("write failed: %w", err)
if err := netfetch.ToFile(context.Background(), url, opts, dlOutput); err != nil {
return err
}
fmt.Println(dlOutput)
+26 -24
View File
@@ -16,6 +16,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
@@ -26,7 +27,7 @@ var updateCmd = &cobra.Command{
Use: "update",
Short: "Update DankMaterialShell to the latest version",
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
PreRunE: findConfig,
PreRunE: shellApp.ResolveConfig,
Run: func(cmd *cobra.Command, args []string) {
runUpdate()
},
@@ -98,7 +99,7 @@ func runUpdate() {
}
log.Info("Update complete! Restarting DMS...")
restartShell()
shellApp.Restart()
}
func updateArchLinux() error {
@@ -250,6 +251,10 @@ func updateOtherDistros() error {
return fmt.Errorf("failed to fetch changes: %w", err)
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
if currentTag != "" {
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
latestTagOutput, err := latestTagCmd.Output()
@@ -291,6 +296,10 @@ func updateOtherDistros() error {
return fmt.Errorf("update cancelled")
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
return nil
}
@@ -320,10 +329,21 @@ func updateOtherDistros() error {
return fmt.Errorf("update cancelled")
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
fmt.Println("\nUpdate complete!")
return nil
}
func updateSubmodules() error {
submoduleCmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
submoduleCmd.Stdout = os.Stdout
submoduleCmd.Stderr = os.Stderr
return submoduleCmd.Run()
}
func offerReclone(dmsPath string) bool {
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
reader := bufio.NewReader(os.Stdin)
@@ -342,7 +362,7 @@ func offerReclone(dmsPath string) bool {
}
fmt.Println("Cloning fresh copy...")
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
if err := cloneCmd.Run(); err != nil {
@@ -393,27 +413,11 @@ func updateDMSBinary() error {
}
fmt.Println("Fetching latest release version...")
cmd := exec.Command("curl", "-s", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest")
output, err := cmd.Output()
version, err := netfetch.LatestReleaseTag(context.Background())
if err != nil {
return fmt.Errorf("failed to fetch latest release: %w", err)
}
version := ""
for line := range strings.SplitSeq(string(output), "\n") {
if strings.Contains(line, "\"tag_name\"") {
parts := strings.Split(line, "\"")
if len(parts) >= 4 {
version = parts[3]
break
}
}
}
if version == "" {
return fmt.Errorf("could not determine latest version")
}
fmt.Printf("Latest version: %s\n", version)
tempDir, err := os.MkdirTemp("", "dms-update-*")
@@ -429,14 +433,12 @@ func updateDMSBinary() error {
checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
fmt.Println("Downloading dms binary...")
downloadCmd := exec.Command("curl", "-L", binaryURL, "-o", binaryPath)
if err := downloadCmd.Run(); err != nil {
if err := netfetch.ToFile(context.Background(), binaryURL, netfetch.Options{Timeout: 5 * time.Minute}, binaryPath); err != nil {
return fmt.Errorf("failed to download binary: %w", err)
}
fmt.Println("Downloading checksum...")
downloadCmd = exec.Command("curl", "-L", checksumURL, "-o", checksumPath)
if err := downloadCmd.Run(); err != nil {
if err := netfetch.ToFile(context.Background(), checksumURL, netfetch.Options{Timeout: 30 * time.Second}, checksumPath); err != nil {
return fmt.Errorf("failed to download checksum: %w", err)
}
File diff suppressed because it is too large. Load diff
+104 -64
View File
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -46,18 +47,30 @@ var keybindsSetCmd = &cobra.Command{
Short: "Set a keybind override",
Long: "Create or update a keybind override for the specified provider",
Args: cobra.ExactArgs(3),
Run: runKeybindsSet,
Run: runKeybindsEdit,
}
var keybindsRemoveCmd = &cobra.Command{
Use: "remove <provider> <key>",
Short: "Remove a keybind override",
Long: "Remove a keybind override from the specified provider",
Short: "Remove a keybind",
Long: "Remove a keybind. For Hyprland this writes a negative override to dms/binds-user.lua so the key stays unbound across DMS updates. For other providers it deletes the entry from the managed file.",
Args: cobra.ExactArgs(2),
Run: runKeybindsRemove,
Run: runKeybindsEdit,
}
var keybindsResetCmd = &cobra.Command{
Use: "reset <provider> <key>",
Short: "Reset a keybind override to its DMS default",
Long: "Drop the user override for the given key so the DMS default re-applies. For providers without a separate default file (Niri, MangoWC) this is equivalent to remove.",
Args: cobra.ExactArgs(2),
Run: runKeybindsEdit,
}
func init() {
keybindsCmd.PersistentFlags().String("expected-generation", "", "Configuration generation observed when editing Aqueous bindings")
for _, command := range []*cobra.Command{keybindsSetCmd, keybindsRemoveCmd, keybindsResetCmd} {
command.Flags().Bool("json", false, "Return structured mutation results, including errors")
}
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
@@ -72,6 +85,7 @@ func init() {
keybindsCmd.AddCommand(keybindsShowCmd)
keybindsCmd.AddCommand(keybindsSetCmd)
keybindsCmd.AddCommand(keybindsRemoveCmd)
keybindsCmd.AddCommand(keybindsResetCmd)
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
return providers.NewJSONFileProvider(filePath)
@@ -84,6 +98,9 @@ func initializeProviders() {
registry := keybinds.GetDefaultRegistry()
hyprlandProvider := providers.NewHyprlandProvider("")
if err := registry.Register(providers.NewAqueousProvider()); err != nil {
log.Warnf("Failed to register Aqueous provider: %v", err)
}
if err := registry.Register(hyprlandProvider); err != nil {
log.Warnf("Failed to register Hyprland provider: %v", err)
}
@@ -197,69 +214,92 @@ func runKeybindsShow(cmd *cobra.Command, args []string) {
printCheatSheet(provider)
}
func getWritableProvider(name string) keybinds.WritableProvider {
provider, err := keybinds.GetDefaultRegistry().Get(name)
func keybindEditFailure(err error) map[string]any {
code := "command_failed"
var helperError *providers.AqueousError
if errors.As(err, &helperError) {
code = helperError.Code
}
return map[string]any{"success": false, "code": code, "message": err.Error()}
}
func runKeybindsEdit(cmd *cobra.Command, args []string) {
result, err := editKeybind(cmd, args)
if err == nil {
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(result)
return
}
structured, _ := cmd.Flags().GetBool("json")
if !structured {
log.Fatalf("Failed to save keybind: %v", err)
return
}
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(keybindEditFailure(err))
os.Exit(1)
}
func editKeybind(cmd *cobra.Command, args []string) (map[string]any, error) {
provider, err := keybinds.GetDefaultRegistry().Get(args[0])
if err != nil {
log.Fatalf("Error: %v", err)
return nil, err
}
key := args[1]
if aqueous, ok := provider.(*providers.AqueousProvider); ok {
generation, _ := cmd.Flags().GetString("expected-generation")
edit := providers.AqueousBindEdit{Generation: generation, Key: key, Remove: cmd.Name() != "set"}
if !edit.Remove {
for _, flag := range []string{"desc", "allow-when-locked", "cooldown-ms", "no-repeat", "no-inhibiting", "flags"} {
if cmd.Flags().Changed(flag) {
return nil, fmt.Errorf("aqueous does not support --%s in this provider", flag)
}
}
edit.Action = args[2]
edit.OriginalKey, _ = cmd.Flags().GetString("replace-key")
}
result, err := aqueous.Edit(cmd.Context(), edit)
if err != nil {
return nil, err
}
return map[string]any{"success": true, "code": "applied", "generation": result.String("generation"), "key": key}, nil
}
writable, ok := provider.(keybinds.WritableProvider)
if !ok {
log.Fatalf("Provider %s does not support writing keybinds", name)
return nil, fmt.Errorf("provider %s does not support writing keybinds", args[0])
}
return writable
}
func runKeybindsSet(cmd *cobra.Command, args []string) {
providerName, key, action := args[0], args[1], args[2]
writable := getWritableProvider(providerName)
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
_ = writable.RemoveBind(replaceKey)
}
options := make(map[string]any)
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
options["allow-when-locked"] = true
}
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
options["cooldown-ms"] = v
}
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false
}
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
options["allow-inhibiting"] = false
}
if v, _ := cmd.Flags().GetString("flags"); v != "" {
options["flags"] = v
}
desc, _ := cmd.Flags().GetString("desc")
if err := writable.SetBind(key, action, desc, options); err != nil {
log.Fatalf("Error setting keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"action": action,
"path": writable.GetOverridePath(),
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsRemove(_ *cobra.Command, args []string) {
providerName, key := args[0], args[1]
writable := getWritableProvider(providerName)
if err := writable.RemoveBind(key); err != nil {
log.Fatalf("Error removing keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"removed": true,
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
result := map[string]any{"success": true, "key": key}
switch cmd.Name() {
case "set":
options := make(map[string]any)
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
options["allow-when-locked"] = true
}
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
options["cooldown-ms"] = v
}
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false
}
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
options["allow-inhibiting"] = false
}
if v, _ := cmd.Flags().GetString("flags"); v != "" {
options["flags"] = v
}
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
_ = writable.RemoveBind(replaceKey)
}
desc, _ := cmd.Flags().GetString("desc")
err = writable.SetBind(key, args[2], desc, options)
result["action"] = args[2]
result["path"] = writable.GetOverridePath()
case "remove":
err = writable.RemoveBind(key)
result["removed"] = true
case "reset":
err = writable.ResetBind(key)
result["reset"] = true
default:
return nil, fmt.Errorf("unsupported keybind operation: %s", cmd.Name())
}
return result, err
}
+25
View File
@@ -0,0 +1,25 @@
package main
import (
"errors"
"fmt"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
)
func TestKeybindEditFailure(t *testing.T) {
for _, tc := range []struct {
err error
code string
}{
{&providers.AqueousError{Code: "external_change", Message: "changed"}, "external_change"},
{fmt.Errorf("helper: %w", &providers.AqueousError{Code: "read_only", Message: "denied"}), "read_only"},
{errors.New("transport failed"), "command_failed"},
} {
result := keybindEditFailure(tc.err)
if result["success"] != false || result["code"] != tc.code || result["message"] != tc.err.Error() {
t.Fatalf("unexpected failure result: %#v", result)
}
}
}
+54 -3
View File
@@ -38,10 +38,24 @@ var matugenCheckCmd = &cobra.Command{
Run: runMatugenCheck,
}
var matugenPreviewCmd = &cobra.Command{
Use: "preview",
Short: "Preview Matugen scheme colors without applying them",
Run: runMatugenPreview,
}
var matugenQtengineCmd = &cobra.Command{
Use: "qtengine",
Short: "Sync qtengine's config with the current DMS theme",
Run: runMatugenQtengine,
}
func init() {
matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd)
matugenCmd.AddCommand(matugenCheckCmd)
matugenCmd.AddCommand(matugenPreviewCmd)
matugenCmd.AddCommand(matugenQtengineCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files")
@@ -49,18 +63,25 @@ func init() {
cmd.Flags().String("config-dir", "", "User config directory")
cmd.Flags().String("kind", "image", "Source type: image or hex")
cmd.Flags().String("value", "", "Wallpaper path or hex color")
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
cmd.Flags().String("mode", "dark", "Color mode: dark, light, or smart (smart requires matugen 4.2+ and an image source)")
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type (supports scheme-smart on matugen 4.2+)")
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
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("terminals-always-dark", false, "Force terminal themes to dark variant")
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().String("source-mode", "", "Source color selection: dominant, colorful, darkness, lightness, saturation, less-saturation, value")
}
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
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().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
matugenQtengineCmd.Flags().String("config-dir", "", "User config directory")
matugenQtengineCmd.Flags().String("icon-theme", "", "Icon theme name")
}
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
@@ -77,6 +98,8 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
contrast, _ := cmd.Flags().GetFloat64("contrast")
sourceMode, _ := cmd.Flags().GetString("source-mode")
return matugen.Options{
StateDir: stateDir,
@@ -87,11 +110,13 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
Mode: matugen.ColorMode(mode),
IconTheme: iconTheme,
MatugenType: matugenType,
Contrast: contrast,
RunUserTemplates: runUserTemplates,
StockColors: stockColors,
SyncModeWithPortal: syncModeWithPortal,
TerminalsAlwaysDark: terminalsAlwaysDark,
SkipTemplates: skipTemplates,
SourceMode: sourceMode,
}
}
@@ -128,6 +153,8 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
"syncModeWithPortal": opts.SyncModeWithPortal,
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"skipTemplates": opts.SkipTemplates,
"contrast": opts.Contrast,
"sourceMode": opts.SourceMode,
"wait": wait,
},
}
@@ -196,3 +223,27 @@ func runMatugenCheck(cmd *cobra.Command, args []string) {
}
fmt.Println(string(data))
}
func runMatugenPreview(cmd *cobra.Command, args []string) {
sourceColor, _ := cmd.Flags().GetString("source-color")
imagePath, _ := cmd.Flags().GetString("image")
contrast, _ := cmd.Flags().GetFloat64("contrast")
previews, err := matugen.PreviewSchemes(sourceColor, contrast, imagePath)
if err != nil {
log.Fatalf("Failed to generate Matugen previews: %v", err)
}
data, err := json.Marshal(previews)
if err != nil {
log.Fatalf("Failed to marshal Matugen previews: %v", err)
}
fmt.Println(string(data))
}
func runMatugenQtengine(cmd *cobra.Command, args []string) {
configDir, _ := cmd.Flags().GetString("config-dir")
iconTheme, _ := cmd.Flags().GetString("icon-theme")
if err := matugen.SyncQtengineConfigAt(configDir, iconTheme); err != nil {
log.Fatalf("Failed to sync qtengine config: %v", err)
}
}
+14
View File
@@ -0,0 +1,14 @@
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)
}
+21 -1
View File
@@ -5,6 +5,7 @@ import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
@@ -61,8 +62,27 @@ func runNotify(cmd *cobra.Command, args []string) {
Timeout: int32(notifyTimeout),
}
if err := notify.Send(n); err != nil {
id, err := notify.Send(n)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if n.FilePath == "" {
return
}
watchNotificationAction(id, n.FilePath)
}
func watchNotificationAction(id uint32, path string) {
if id == 0 {
return
}
resp, ok := tryServerRequest(models.Request{
Method: "notify.watchAction",
Params: map[string]any{"id": id, "path": path},
})
if ok && resp.Error == "" {
return
}
notify.SpawnActionListener(id, path)
}
+36 -97
View File
@@ -28,9 +28,9 @@ with flags to handle different MIME types or application categories.
Examples:
dms open https://example.com # Open URL with browser picker
dms open file.pdf --mime application/pdf # Open PDF with compatible apps
dms open document.odt --category Office # Open with office applications
dms open --mime image/png image.png # Open image with image viewers`,
dms open file.pdf # Open file (MIME auto-detected)
dms open file.pdf --mime application/pdf # Override MIME detection
dms open document.odt --category Office # Open with office applications`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runOpen(args[0])
@@ -47,123 +47,58 @@ func init() {
})
}
// mimeTypeToCategories maps MIME types to desktop file categories
func mimeTypeToCategories(mimeType string) []string {
// Split MIME type to get the main type
parts := strings.Split(mimeType, "/")
if len(parts) < 1 {
return nil
func detectMimeFromPath(path string) string {
ext := filepath.Ext(path)
if ext == "" {
return ""
}
mainType := parts[0]
switch mainType {
case "image":
return []string{"Graphics", "Viewer"}
case "video":
return []string{"Video", "AudioVideo"}
case "audio":
return []string{"Audio", "AudioVideo"}
case "text":
if strings.Contains(mimeType, "html") {
return []string{"WebBrowser"}
}
return []string{"TextEditor", "Office"}
case "application":
if strings.Contains(mimeType, "pdf") {
return []string{"Office", "Viewer"}
}
if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "spreadsheet") ||
strings.Contains(mimeType, "presentation") || strings.Contains(mimeType, "msword") ||
strings.Contains(mimeType, "ms-excel") || strings.Contains(mimeType, "ms-powerpoint") ||
strings.Contains(mimeType, "opendocument") {
return []string{"Office"}
}
if strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") ||
strings.Contains(mimeType, "gzip") || strings.Contains(mimeType, "compress") {
return []string{"Archiving", "Utility"}
}
return []string{"Office", "Viewer"}
}
return nil
return mime.TypeByExtension(ext)
}
func runOpen(target string) {
// Parse file:// URIs to extract the actual file path
actualTarget := target
detectedMimeType := openMimeType
detectedCategories := openCategories
detectedRequestType := openRequestType
log.Infof("Processing target: %s", target)
if parsedURL, err := url.Parse(target); err == nil && parsedURL.Scheme == "file" {
// Extract file path from file:// URI and convert to absolute path
actualTarget = parsedURL.Path
if absPath, err := filepath.Abs(actualTarget); err == nil {
actualTarget = absPath
switch {
case isScheme(target, "file://"):
parsedURL, err := url.Parse(target)
if err == nil {
actualTarget = parsedURL.Path
}
if abs, err := filepath.Abs(actualTarget); err == nil {
actualTarget = abs
}
if detectedRequestType == "url" || detectedRequestType == "" {
detectedRequestType = "file"
}
log.Infof("Detected file:// URI, extracted absolute path: %s", actualTarget)
// Auto-detect MIME type if not provided
if detectedMimeType == "" {
ext := filepath.Ext(actualTarget)
if ext != "" {
detectedMimeType = mime.TypeByExtension(ext)
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
}
detectedMimeType = detectMimeFromPath(actualTarget)
}
log.Infof("Detected file:// URI, absolute path: %s", actualTarget)
// Auto-detect categories based on MIME type if not provided
if len(detectedCategories) == 0 && detectedMimeType != "" {
detectedCategories = mimeTypeToCategories(detectedMimeType)
log.Infof("Detected categories from MIME type: %v", detectedCategories)
}
} else if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
// Handle HTTP(S) URLs
case isScheme(target, "http://"), isScheme(target, "https://"), isScheme(target, "dms://"):
if detectedRequestType == "" {
detectedRequestType = "url"
}
log.Infof("Detected HTTP(S) URL")
} else if strings.HasPrefix(target, "dms://") {
// Handle DMS internal URLs (theme/plugin install, etc.)
if detectedRequestType == "" {
detectedRequestType = "url"
}
log.Infof("Detected DMS internal URL")
} else if _, err := os.Stat(target); err == nil {
// Handle local file paths directly (not file:// URIs)
// Convert to absolute path
if absPath, err := filepath.Abs(target); err == nil {
actualTarget = absPath
}
log.Infof("Detected URL: %s", target)
default:
if _, err := os.Stat(target); err != nil {
break
}
if abs, err := filepath.Abs(target); err == nil {
actualTarget = abs
}
if detectedRequestType == "url" || detectedRequestType == "" {
detectedRequestType = "file"
}
log.Infof("Detected local file path, converted to absolute: %s", actualTarget)
// Auto-detect MIME type if not provided
if detectedMimeType == "" {
ext := filepath.Ext(actualTarget)
if ext != "" {
detectedMimeType = mime.TypeByExtension(ext)
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
}
}
// Auto-detect categories based on MIME type if not provided
if len(detectedCategories) == 0 && detectedMimeType != "" {
detectedCategories = mimeTypeToCategories(detectedMimeType)
log.Infof("Detected categories from MIME type: %v", detectedCategories)
detectedMimeType = detectMimeFromPath(actualTarget)
}
log.Infof("Detected local file path: %s", actualTarget)
}
params := map[string]any{
@@ -174,8 +109,8 @@ func runOpen(target string) {
params["mimeType"] = detectedMimeType
}
if len(detectedCategories) > 0 {
params["categories"] = detectedCategories
if len(openCategories) > 0 {
params["categories"] = openCategories
}
if detectedRequestType != "" {
@@ -183,7 +118,7 @@ func runOpen(target string) {
}
method := "apppicker.open"
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "dms://")) {
if detectedMimeType == "" && len(openCategories) == 0 && (isScheme(target, "http://") || isScheme(target, "https://") || isScheme(target, "dms://")) {
method = "browser.open"
params["url"] = target
}
@@ -203,3 +138,7 @@ func runOpen(target string) {
log.Infof("Request sent successfully")
}
func isScheme(target, prefix string) bool {
return strings.HasPrefix(target, prefix)
}
+265
View File
@@ -0,0 +1,265 @@
package main
import (
"fmt"
"io"
"os"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/qrcode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
)
var (
qrEcc string
qrVersion int
qrOutput string
qrStdout bool
qrClipboard bool
qrCopyText bool
qrRender bool
qrNoRender bool
qrInvert bool
qrQuietZone int
qrModuleSize int
qrFg string
qrBg string
qrTransparent bool
qrBorder int
qrShape string
qrLogo string
qrLogoScale int
qrWifiPassword string
qrWifiSecurity string
qrWifiHidden bool
)
var qrCmd = &cobra.Command{
Use: "qr [text]",
Short: "Generate QR codes",
Long: `Generate a QR code from text, stdin, or a WiFi network.
By default the code is rendered to the terminal when stdout is a TTY, or
written as PNG bytes to stdout when piped. Use flags to also copy to the
clipboard, save a PNG, or tune encoding and colors.
Input:
dms qr "https://example.com" # encode an argument
echo -n "data" | dms qr # encode stdin
dms qr - # encode stdin explicitly
Output (combine freely):
dms qr "text" --clipboard # copy PNG image to clipboard
dms qr "text" --copy-text # copy the source text to clipboard
dms qr "text" -o code.png # save a PNG file
dms qr "text" > code.png # PNG to stdout (piped)
dms qr "text" --render # force terminal render
Encoding & style:
--ecc L|M|Q|H # error correction (default M)
--qr-version 10 # force symbol version (1-40)
--module-size 12 --fg '#000' ... # PNG sizing and colors
--shape circle # round modules
--logo icon.png # center logo (bumps --ecc to H)
--invert # flip colors for light terminals
WiFi:
dms qr wifi MySSID -p secret # build from an explicit password
dms qr wifi MySSID # pull the saved secret from the shell`,
Args: cobra.ArbitraryArgs,
Run: runQR,
}
var qrWifiCmd = &cobra.Command{
Use: "wifi <ssid>",
Short: "Generate a WiFi QR code",
Long: `Generate a QR code that joins a WiFi network when scanned.
With --password the code is built entirely offline. Without it, the saved
credentials are fetched from the running DMS shell (like the network panel).`,
Args: cobra.ExactArgs(1),
Run: runQRWifi,
}
func init() {
qrCmd.PersistentFlags().StringVar(&qrEcc, "ecc", "", "Error correction level: L, M, Q, H (default M, or H with --logo)")
qrCmd.PersistentFlags().IntVar(&qrVersion, "qr-version", 0, "Force QR symbol version 1-40 (0 = auto)")
qrCmd.PersistentFlags().StringVarP(&qrOutput, "output", "o", "", "Write a PNG to this file")
qrCmd.PersistentFlags().BoolVar(&qrStdout, "stdout", false, "Write PNG bytes to stdout")
qrCmd.PersistentFlags().BoolVar(&qrClipboard, "clipboard", false, "Copy the PNG image to the clipboard")
qrCmd.PersistentFlags().BoolVar(&qrCopyText, "copy-text", false, "Copy the source text to the clipboard")
qrCmd.PersistentFlags().BoolVar(&qrRender, "render", false, "Force terminal rendering")
qrCmd.PersistentFlags().BoolVar(&qrNoRender, "no-render", false, "Never render to the terminal")
qrCmd.PersistentFlags().BoolVar(&qrInvert, "invert", false, "Swap colors (for light terminals)")
qrCmd.PersistentFlags().IntVar(&qrQuietZone, "quiet-zone", 2, "Terminal margin in modules")
qrCmd.PersistentFlags().IntVar(&qrModuleSize, "module-size", 0, "PNG pixels per module (0 = auto)")
qrCmd.PersistentFlags().StringVar(&qrFg, "fg", "", "Dark module color (#RGB or #RRGGBB)")
qrCmd.PersistentFlags().StringVar(&qrBg, "bg", "", "Light module color (#RGB or #RRGGBB)")
qrCmd.PersistentFlags().BoolVar(&qrTransparent, "transparent", false, "Transparent PNG background")
qrCmd.PersistentFlags().IntVar(&qrBorder, "border", -1, "PNG border in pixels (-1 = auto)")
qrCmd.PersistentFlags().StringVar(&qrShape, "shape", "square", "PNG module shape (square, circle)")
qrCmd.PersistentFlags().StringVar(&qrLogo, "logo", "", "Center a PNG/JPEG logo on the PNG output")
qrCmd.PersistentFlags().IntVar(&qrLogoScale, "logo-scale", 0, "Max logo size as 1/N of the code (0 = library default of 5)")
qrWifiCmd.Flags().StringVarP(&qrWifiPassword, "password", "p", "", "WiFi password (offline build)")
qrWifiCmd.Flags().StringVar(&qrWifiSecurity, "security", "WPA", "Security type (WPA, WEP, nopass)")
qrWifiCmd.Flags().BoolVar(&qrWifiHidden, "hidden", false, "Mark the network as hidden")
qrCmd.AddCommand(qrWifiCmd)
}
func runQR(cmd *cobra.Command, args []string) {
text := strings.Join(args, " ")
if text == "" || text == "-" {
data, err := io.ReadAll(os.Stdin)
if err != nil {
fatalf("Error reading stdin: %v", err)
}
text = strings.TrimRight(string(data), "\n")
}
if text == "" {
fatalf("Error: no input (provide text, pipe stdin, or use a subcommand)")
}
emitQR(text)
}
func runQRWifi(cmd *cobra.Command, args []string) {
ssid := args[0]
if qrWifiPassword != "" || strings.EqualFold(qrWifiSecurity, "nopass") {
emitQR(qrcode.WiFiString(qrWifiSecurity, ssid, qrWifiPassword, qrWifiHidden))
return
}
content, err := fetchWifiQRContent(ssid)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
fmt.Fprintln(os.Stderr, "Hint: pass --password to build the code without the shell.")
os.Exit(1)
}
if qrWifiHidden {
content = strings.TrimSuffix(content, ";") + "H:true;;"
}
emitQR(content)
}
func emitQR(text string) {
renderTerm := shouldRenderTerminal()
pngToStdout := qrStdout || (!renderTerm && qrOutput == "" && !qrClipboard && !qrCopyText)
if pngToStdout || qrOutput != "" || qrClipboard {
png, err := qrcode.RenderPNG(text, qrcode.ImageOptions{
ECC: effectiveEcc(),
Version: qrVersion,
ModuleSize: qrModuleSize,
Fg: qrFg,
Bg: qrBg,
Transparent: qrTransparent,
Border: qrBorder,
Shape: qrShape,
Logo: qrLogo,
LogoScale: qrLogoScale,
})
if err != nil {
fatalf("Error encoding QR: %v", err)
}
emitPNG(png, pngToStdout)
}
if qrCopyText {
if err := clipboard.CopyText(text); err != nil {
fatalf("Error copying text: %v", err)
}
}
if !renderTerm {
return
}
out, err := qrcode.RenderTerminal(text, qrcode.TermOptions{
ECC: effectiveEcc(),
Version: qrVersion,
QuietZone: qrQuietZone,
Invert: qrInvert,
Fg: qrFg,
Bg: qrBg,
})
if err != nil {
fatalf("Error rendering QR: %v", err)
}
dst := os.Stdout
if pngToStdout {
dst = os.Stderr
}
fmt.Fprint(dst, out)
}
func emitPNG(png []byte, toStdout bool) {
if qrOutput != "" {
if err := os.WriteFile(qrOutput, png, 0o644); err != nil {
fatalf("Error writing file: %v", err)
}
fmt.Fprintln(os.Stderr, qrOutput)
}
if toStdout {
os.Stdout.Write(png)
}
if qrClipboard {
if err := clipboard.Copy(png, "image/png"); err != nil {
fatalf("Error copying image: %v", err)
}
}
}
func shouldRenderTerminal() bool {
switch {
case qrNoRender:
return false
case qrRender:
return true
case qrStdout, qrOutput != "", qrClipboard, qrCopyText:
return false
default:
return isatty.IsTerminal(os.Stdout.Fd())
}
}
func effectiveEcc() string {
switch {
case qrEcc != "":
return qrEcc
case qrLogo != "":
return "H"
default:
return "M"
}
}
func fatalf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
func fetchWifiQRContent(ssid string) (string, error) {
resp, err := sendServerRequest(models.Request{
ID: 1,
Method: "network.qrcode-content",
Params: map[string]any{"ssid": ssid},
})
if err != nil {
return "", err
}
if resp.Error != "" {
return "", fmt.Errorf("%s", resp.Error)
}
if resp.Result == nil {
return "", fmt.Errorf("empty response")
}
content, ok := (*resp.Result).(string)
if !ok {
return "", fmt.Errorf("unexpected response format")
}
return content, nil
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/registries"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
var registryCmd = &cobra.Command{
Use: "registry",
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.",
}
var registryListCmd = &cobra.Command{
Use: "list",
Short: "List configured registries",
Run: func(cmd *cobra.Command, args []string) {
for _, s := range registries.Load(afero.NewOsFs()) {
suffix := ""
if s.Official() {
suffix = " (official)"
}
fmt.Printf("%s%s\n %s\n", s.Name, suffix, s.URL)
}
},
}
var registryAddCmd = &cobra.Command{
Use: "add <name> <url>",
Short: "Add a registry",
Long: "Add a registry by name and git URL. The repository must contain a plugins/ or themes/ directory in the registry format.",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
if err := registries.Add(afero.NewOsFs(), args[0], args[1]); err != nil {
log.Fatalf("Error adding registry: %v", err)
}
fmt.Printf("Registry added: %s\n", args[0])
},
}
var registryRemoveCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove a registry",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var names []string
for _, s := range registries.Load(afero.NewOsFs()) {
if !s.Official() {
names = append(names, s.Name)
}
}
return names, cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := registries.Remove(afero.NewOsFs(), args[0]); err != nil {
log.Fatalf("Error removing registry: %v", err)
}
fmt.Printf("Registry removed: %s\n", args[0])
},
}
+1 -60
View File
@@ -1,19 +1,9 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var customConfigPath string
var configPath string
var rootCmd = &cobra.Command{
Use: "dms",
Short: "dms CLI",
@@ -21,54 +11,5 @@ var rootCmd = &cobra.Command{
}
func init() {
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
}
func findConfig(cmd *cobra.Command, args []string) error {
if customConfigPath != "" {
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
shellPath := filepath.Join(customConfigPath, "shell.qml")
info, statErr := os.Stat(shellPath)
if statErr == nil && !info.IsDir() {
configPath = customConfigPath
log.Debug("Using config from: %s", configPath)
return nil
}
if statErr != nil {
return fmt.Errorf("custom config path error: %w", statErr)
}
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
}
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
if len(getAllDMSPIDs()) == 0 {
os.Remove(configStateFile)
} else {
statePath := strings.TrimSpace(string(data))
shellPath := filepath.Join(statePath, "shell.qml")
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
log.Debug("Using config from active session state file: %s", statePath)
configPath = statePath
log.Debug("Using config from: %s", configPath)
return nil
}
os.Remove(configStateFile)
}
}
log.Debug("No custom path or active session, searching default XDG locations...")
var err error
configPath, err = config.LocateDMSConfig()
if err != nil {
return err
}
log.Debug("Using config from: %s", configPath)
return nil
rootCmd.PersistentFlags().StringVarP(shellApp.CustomConfigVar(), "config", "c", "", "Path to a UI config dir (containing shell.qml) to use instead of the embedded UI (env: DMS_SHELL_DIR)")
}
+235 -38
View File
@@ -2,18 +2,25 @@ package main
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
"github.com/spf13/cobra"
)
var (
ssOutputName string
ssSeat string
ssCursor string
ssFormat string
ssQuality int
@@ -22,9 +29,27 @@ var (
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssNoConfirm bool
ssReset bool
ssStdout bool
ssJSON bool
ssGeometry bool
ssAllowMulti bool
ssHUD string
)
type screenshotMetadata struct {
Status string `json:"status"`
Path string `json:"path,omitempty"`
X *int `json:"x,omitempty"`
Y *int `json:"y,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Scale float64 `json:"scale,omitempty"`
Mime string `json:"mime,omitempty"`
Error string `json:"error,omitempty"`
}
var screenshotCmd = &cobra.Command{
Use: "screenshot",
Short: "Capture screenshots",
@@ -35,8 +60,9 @@ Modes:
full - Capture the focused output
all - Capture all outputs combined
output - Capture a specific output by name
window - Capture the focused window (Hyprland/DWL)
window - Capture the focused window (Hyprland/Mango/niri/Aqueous)
last - Capture the last selected region
scroll - Select a region, then scroll to capture a stitched tall image
Output format (--format):
png - PNG format (default)
@@ -48,12 +74,19 @@ Examples:
dms screenshot full # Full screen of focused output
dms screenshot all # All screens combined
dms screenshot output -o DP-1 # Specific output
dms screenshot window # Focused window (Hyprland)
dms screenshot window # Focused window
dms screenshot last # Last region (pre-selected)
dms screenshot --reset # Reset last region pre-selection
dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only
dms screenshot --no-confirm # Region capture on mouse release
dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
dms screenshot -f jpg -q 85 # JPEG with quality 85
dms screenshot --json # Print capture metadata as JSON
dms screenshot --allow-multiple # Skip the one-selector-at-a-time guard
dms screenshot -g # Print selected region geometry (X,Y WxH) to stdout
dms screenshot scroll # Scroll capture, Enter finishes / Esc cancels
dms screenshot scroll --interval 250`,
}
var ssRegionCmd = &cobra.Command{
@@ -91,10 +124,33 @@ If no previous region exists, falls back to interactive selection.`,
var ssWindowCmd = &cobra.Command{
Use: "window",
Short: "Capture the focused window",
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
Long: `Capture the currently focused window. Supported on Hyprland, Mango, niri, and Aqueous. Aqueous requires a running DMS shell and crops the output including borders and overlapping windows.`,
Run: runScreenshotWindow,
}
var ssScrollInterval int
var ssScrollCmd = &cobra.Command{
Use: "scroll",
Short: "Capture a scrolling region stitched into one tall image",
Long: `Select a region, then scroll the content beneath with the mouse wheel or
touchpad while frames are captured and stitched vertically. Finish with the
on-screen done button; cancel with the cancel button. Enter and Esc work
everywhere: most compositors hold the keyboard on the overlay (keyboard
scrolling does not reach the app there), while Hyprland leaves the keyboard
with the application — keyboard scrolling works, and Enter/Esc act through
temporary global binds for the session. The cursor is never included in
frames.
Frames are stitched continuously while scrolling, and revisited content is
never duplicated — scrolling up past the starting point extends the image
upward. Content jumped past faster than capture can follow is skipped rather
than stitched incorrectly.
Rotated outputs are not supported.`,
Run: runScreenshotScroll,
}
var ssListCmd = &cobra.Command{
Use: "list",
Short: "List available outputs",
@@ -105,12 +161,13 @@ var notifyActionCmd = &cobra.Command{
Use: "notify-action",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
screenshot.RunNotifyActionListener(args)
notify.RunActionListener(args)
},
}
func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().StringVar(&ssSeat, "seat", "", "Seat for Aqueous capture (required with multiple seats)")
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
@@ -119,9 +176,18 @@ func init() {
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
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().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)")
screenshotCmd.AddCommand(ssRegionCmd)
screenshotCmd.AddCommand(ssScrollCmd)
screenshotCmd.AddCommand(ssFullCmd)
screenshotCmd.AddCommand(ssAllCmd)
screenshotCmd.AddCommand(ssOutputCmd)
@@ -136,13 +202,25 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig()
config.Mode = mode
config.OutputName = ssOutputName
config.Seat = ssSeat
if strings.EqualFold(ssCursor, "on") {
config.Cursor = screenshot.CursorOn
}
config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify
config.NoConfirm = ssNoConfirm
config.Reset = ssReset
config.Stdout = ssStdout
config.Geometry = ssGeometry
config.AllowMultiple = ssAllowMulti
config.HUD = ssHUD
if ssGeometry {
config.Clipboard = false
config.SaveFile = false
config.Notify = false
}
if ssOutputDir != "" {
config.OutputDir = ssOutputDir
@@ -171,28 +249,118 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
return config
}
func runScreenshot(config screenshot.Config) {
sc := screenshot.New(config)
result, err := sc.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
// 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.
func setPopoutScreenshotMode(begin bool) {
fn := "end"
if begin {
fn = "begin"
}
cmdArgs := []string{"ipc"}
if pid, ok := shellApp.SessionPID(); ok {
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
} else {
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)
_ = exec.Command("qs", cmdArgs...).Start()
}
func writeScreenshotJSON(meta screenshotMetadata) {
_ = json.NewEncoder(os.Stdout).Encode(meta)
}
func exitScreenshotError(context string, err error) {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "error", Error: err.Error()})
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Error%s: %v\n", context, err)
os.Exit(1)
}
func formatMime(format screenshot.Format) string {
switch format {
case screenshot.FormatJPEG:
return "image/jpeg"
case screenshot.FormatPPM:
return "image/x-portable-pixmap"
default:
return "image/png"
}
}
func runScreenshot(config screenshot.Config) {
if ssJSON && config.Stdout {
fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout")
os.Exit(1)
}
if config.Geometry {
if config.Stdout {
fmt.Fprintln(os.Stderr, "Error: --geometry cannot be combined with --stdout")
os.Exit(1)
}
if config.Mode == screenshot.ModeScroll {
fmt.Fprintln(os.Stderr, "Error: --geometry cannot be combined with scroll mode")
os.Exit(1)
}
}
// Short-lived process over a few tens of MB: let the heap grow instead of paying GC cycles mid-capture.
debug.SetGCPercent(-1)
debug.SetMemoryLimit(1 << 30)
// Region select needs the keyboard; drop popout grabs for its duration.
config.SelectorHook = setPopoutScreenshotMode
result, err := screenshot.New(config).Run()
if err != nil {
exitScreenshotError("", err)
}
if result == nil {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"})
}
if config.Geometry {
os.Exit(1)
}
os.Exit(0)
}
defer result.Buffer.Close()
if config.Geometry {
if ssJSON {
x := int(result.Region.X)
y := int(result.Region.Y)
writeScreenshotJSON(screenshotMetadata{
Status: "success",
X: &x,
Y: &y,
Width: int(result.Region.Width),
Height: int(result.Region.Height),
})
} else {
fmt.Println(result.Region.GeometryString())
}
os.Exit(0)
}
if result.YInverted {
result.Buffer.FlipVertical()
if result.Buffer != nil {
defer result.Buffer.Close()
if result.YInverted {
result.Buffer.FlipVertical()
}
}
if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
os.Exit(1)
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil {
exitScreenshotError(" writing to stdout", err)
}
return
}
@@ -211,50 +379,64 @@ func runScreenshot(config screenshot.Config) {
}
filePath = filepath.Join(outputDir, filename)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
os.Exit(1)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format, result.CICP); err != nil {
exitScreenshotError(" writing file", err)
}
if !ssJSON {
fmt.Println(filePath)
}
fmt.Println(filePath)
}
if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
os.Exit(1)
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil {
exitScreenshotError(" copying to clipboard", err)
}
if !config.SaveFile {
if !ssJSON && !config.SaveFile {
fmt.Println("Copied to clipboard")
}
}
if ssJSON {
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),
})
}
if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
screenshot.SendNotification(screenshot.NotifyResult{
id := screenshot.SendNotification(screenshot.NotifyResult{
FilePath: filePath,
Clipboard: config.Clipboard,
ImageData: thumbData,
Width: thumbW,
Height: thumbH,
})
watchNotificationAction(id, filePath)
}
}
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32, cicp *screenshot.CICP) error {
var mimeType string
var data bytes.Buffer
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
switch format {
case screenshot.FormatJPEG:
mimeType = "image/jpeg"
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
if err := screenshot.EncodeBufferJPEG(&data, buf, pixelFormat, quality); err != nil {
return err
}
default:
mimeType = "image/png"
if err := screenshot.EncodePNG(&data, img); err != nil {
if err := screenshot.EncodeBufferPNG(&data, buf, pixelFormat, cicp); err != nil {
return err
}
}
@@ -262,14 +444,12 @@ func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, q
return clipboard.Copy(data.Bytes(), mimeType)
}
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32, cicp *screenshot.CICP) error {
switch format {
case screenshot.FormatJPEG:
return screenshot.EncodeJPEG(os.Stdout, img, quality)
return screenshot.EncodeBufferJPEG(os.Stdout, buf, pixelFormat, quality)
default:
return screenshot.EncodePNG(os.Stdout, img)
return screenshot.EncodeBufferPNG(os.Stdout, buf, pixelFormat, cicp)
}
}
@@ -296,9 +476,12 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
data := buf.Data()
rgb := make([]byte, dstW*dstH*3)
is10Bit := screenshot.PixelFormat(pixelFormat).Is10Bit()
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888),
uint32(screenshot.FormatABGR2101010), uint32(screenshot.FormatXBGR2101010):
swapRB = false
default:
swapRB = true
@@ -319,11 +502,19 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
if si+3 >= len(data) {
continue
}
if swapRB {
switch {
case 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
}
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]
} else {
default:
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
@@ -338,6 +529,12 @@ func runScreenshotRegion(cmd *cobra.Command, args []string) {
runScreenshot(config)
}
func runScreenshotScroll(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeScroll)
config.IntervalMs = min(max(ssScrollInterval, 30), 1000)
runScreenshot(config)
}
func runScreenshotFull(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeFullScreen)
runScreenshot(config)
+187
View File
@@ -0,0 +1,187 @@
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"sort"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var switchUserCmd = &cobra.Command{
Use: "switch-user [target]",
Short: "Switch to another active session on this seat",
Long: `Switch the active VT to another running session.
With no target, prints the list of switchable sessions. Pass a username or a
numeric session ID to switch directly. Requires the target to already be a
running session on the same seat (use the greeter for a fresh login).`,
Args: cobra.MaximumNArgs(1),
Run: runSwitchUser,
}
type sessionInfo struct {
ID string
Name string
Seat string
TTY string
Type string
Class string
Active bool
State string
Current bool
}
func runSwitchUser(cmd *cobra.Command, args []string) {
currentID := os.Getenv("XDG_SESSION_ID")
sessions, err := listSessions(currentID)
if err != nil {
log.Fatalf("%v", err)
}
switchable := make([]sessionInfo, 0, len(sessions))
for _, s := range sessions {
if s.Class != "user" || s.State == "closing" || s.Current {
continue
}
switchable = append(switchable, s)
}
if len(args) == 0 {
if len(switchable) == 0 {
fmt.Println("No other active sessions on this seat.")
return
}
printSessions(switchable)
return
}
target := args[0]
picked, err := pickSession(switchable, target)
if err != nil {
fmt.Fprintln(os.Stderr, err)
if len(switchable) == 0 {
fmt.Fprintln(os.Stderr, "No other active sessions on this seat. Only already-running sessions can be switched to.")
} else {
fmt.Fprintln(os.Stderr, "\nSwitchable sessions:")
printSessions(switchable)
}
os.Exit(1)
}
if err := activateSession(picked.ID); err != nil {
log.Fatalf("loginctl activate %s: %v", picked.ID, err)
}
}
func listSessions(currentID string) ([]sessionInfo, error) {
listOut, err := exec.Command("loginctl", "list-sessions", "--no-legend").Output()
if err != nil {
return nil, fmt.Errorf("loginctl list-sessions: %w", err)
}
var ids []string
scanner := bufio.NewScanner(strings.NewReader(string(listOut)))
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) == 0 {
continue
}
ids = append(ids, fields[0])
}
out := make([]sessionInfo, 0, len(ids))
for _, id := range ids {
s, err := showSession(id)
if err != nil {
continue
}
s.Current = currentID != "" && s.ID == currentID
out = append(out, s)
}
sort.SliceStable(out, func(i, j int) bool {
if out[i].Name != out[j].Name {
return out[i].Name < out[j].Name
}
return out[i].ID < out[j].ID
})
return out, nil
}
func showSession(id string) (sessionInfo, error) {
out, err := exec.Command("loginctl", "show-session", id,
"-p", "Id", "-p", "Name", "-p", "Seat", "-p", "TTY",
"-p", "Type", "-p", "Class", "-p", "Active", "-p", "State").Output()
if err != nil {
return sessionInfo{}, err
}
fields := map[string]string{}
for line := range strings.SplitSeq(string(out), "\n") {
idx := strings.IndexByte(line, '=')
if idx <= 0 {
continue
}
fields[line[:idx]] = line[idx+1:]
}
if fields["Id"] == "" {
return sessionInfo{}, fmt.Errorf("session %s: no Id", id)
}
return sessionInfo{
ID: fields["Id"],
Name: fields["Name"],
Seat: fields["Seat"],
TTY: fields["TTY"],
Type: fields["Type"],
Class: fields["Class"],
Active: fields["Active"] == "yes",
State: fields["State"],
}, nil
}
func pickSession(sessions []sessionInfo, target string) (sessionInfo, error) {
for _, s := range sessions {
if s.ID == target {
return s, nil
}
}
matches := make([]sessionInfo, 0, 2)
for _, s := range sessions {
if s.Name == target {
matches = append(matches, s)
}
}
if len(matches) == 1 {
return matches[0], nil
}
if len(matches) > 1 {
ids := make([]string, len(matches))
for i, m := range matches {
ids[i] = m.ID
}
return sessionInfo{}, fmt.Errorf("%s has multiple active sessions (%s); pass a session ID instead", target, strings.Join(ids, ", "))
}
return sessionInfo{}, fmt.Errorf("no switchable session matches %q", target)
}
func activateSession(id string) error {
return exec.Command("loginctl", "activate", id).Run()
}
func printSessions(sessions []sessionInfo) {
fmt.Printf("%-6s %-12s %-8s %-8s %-8s\n", "ID", "USER", "TYPE", "SEAT", "TTY")
for _, s := range sessions {
tty := s.TTY
if tty == "" {
tty = "-"
}
seat := s.Seat
if seat == "" {
seat = "-"
}
fmt.Printf("%-6s %-12s %-8s %-8s %-8s\n", s.ID, s.Name, s.Type, seat, tty)
}
}
+153 -80
View File
@@ -10,7 +10,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
@@ -100,56 +100,76 @@ var setupWindowrulesCmd = &cobra.Command{
}
type dmsConfigSpec struct {
niriFile string
hyprFile string
niriContent func(terminal string) string
hyprContent func(terminal string) string
niriFile string
hyprFile string
mangoFile string
niriContent func(terminal string) string
hyprContent func(terminal string) string
mangoContent func(terminal string) string
}
var dmsConfigSpecs = map[string]dmsConfigSpec{
"binds": {
niriFile: "binds.kdl",
hyprFile: "binds.conf",
niriFile: "binds.kdl",
hyprFile: "binds.lua",
mangoFile: "binds.conf",
niriContent: func(t string) string {
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
},
hyprContent: func(t string) string {
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
return strings.ReplaceAll(config.DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", t)
},
mangoContent: func(t string) string {
return strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", t)
},
},
"layout": {
niriFile: "layout.kdl",
hyprFile: "layout.conf",
niriContent: func(_ string) string { return config.NiriLayoutConfig },
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
niriFile: "layout.kdl",
hyprFile: "layout.lua",
mangoFile: "layout.conf",
niriContent: func(_ string) string { return config.NiriLayoutConfig },
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
mangoContent: func(_ string) string { return config.MangoLayoutConfig },
},
"colors": {
niriFile: "colors.kdl",
hyprFile: "colors.conf",
niriContent: func(_ string) string { return config.NiriColorsConfig },
hyprContent: func(_ string) string { return config.HyprColorsConfig },
niriFile: "colors.kdl",
hyprFile: "colors.lua",
mangoFile: "colors.conf",
niriContent: func(_ string) string { return config.NiriColorsConfig },
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
mangoContent: func(_ string) string { return config.MangoColorsConfig },
},
"alttab": {
niriFile: "alttab.kdl",
niriContent: func(_ string) string { return config.NiriAlttabConfig },
},
"input": {
niriFile: "input.kdl",
niriContent: func(_ string) string { return config.NiriInputConfig },
},
"outputs": {
niriFile: "outputs.kdl",
hyprFile: "outputs.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
niriFile: "outputs.kdl",
hyprFile: "outputs.lua",
mangoFile: "outputs.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return config.DMSOutputsLuaConfig },
mangoContent: func(_ string) string { return "" },
},
"cursor": {
niriFile: "cursor.kdl",
hyprFile: "cursor.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
niriFile: "cursor.kdl",
hyprFile: "cursor.lua",
mangoFile: "cursor.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return config.DMSCursorLuaConfig },
mangoContent: func(_ string) string { return "" },
},
"windowrules": {
niriFile: "windowrules.kdl",
hyprFile: "windowrules.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
niriFile: "windowrules.kdl",
hyprFile: "windowrules.lua",
mangoFile: "windowrules.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return config.DMSWindowRulesLuaConfig },
mangoContent: func(_ string) string { return "" },
},
}
@@ -188,20 +208,34 @@ func detectTerminal() (string, error) {
}
func detectCompositorForSetup() (string, error) {
compositors := greeter.DetectCompositors()
var compositors []string
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
if utils.CommandExists(candidate) {
compositors = append(compositors, candidate)
}
}
switch len(compositors) {
case 0:
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
return "", fmt.Errorf("no supported compositors found (niri, Hyprland, or mango required)")
case 1:
return strings.ToLower(compositors[0]), nil
}
selected, err := greeter.PromptCompositorChoice(compositors)
if err != nil {
return "", err
fmt.Println("Multiple compositors detected:")
for i, compositor := range compositors {
fmt.Printf("%d) %s\n", i+1, compositor)
}
return strings.ToLower(selected), nil
fmt.Printf("\nChoice (1-%d): ", len(compositors))
var response string
fmt.Scanln(&response)
choice := 0
fmt.Sscanf(strings.TrimSpace(response), "%d", &choice)
if choice < 1 || choice > len(compositors) {
return "", fmt.Errorf("invalid choice")
}
return strings.ToLower(compositors[choice-1]), nil
}
func runSetupDmsConfig(name string) error {
@@ -224,6 +258,9 @@ func runSetupDmsConfig(name string) error {
case "hyprland":
filename = spec.hyprFile
contentFn = spec.hyprContent
case "mango", "mangowc":
filename = spec.mangoFile
contentFn = spec.mangoContent
default:
return fmt.Errorf("unsupported compositor: %s", compositor)
}
@@ -235,9 +272,11 @@ func runSetupDmsConfig(name string) error {
var dmsDir string
switch compositor {
case "niri":
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "niri", "dms")
dmsDir = filepath.Join(utils.XDGConfigHome(), "niri", "dms")
case "hyprland":
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "hypr", "dms")
dmsDir = filepath.Join(utils.XDGConfigHome(), "hypr", "dms")
case "mango", "mangowc":
dmsDir = filepath.Join(utils.XDGConfigHome(), "mango", "dms")
}
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
@@ -273,7 +312,17 @@ func runSetup() error {
wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
useSystemd := true
if wmSelected {
if wm == deps.WindowManagerMango {
useSystemd = false
} else if isVoidSetup() {
useSystemd = false
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
} else {
useSystemd = promptSystemd()
}
}
if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.")
@@ -307,22 +356,18 @@ func runSetup() error {
}
}()
ctx := context.Background()
var results []config.DeploymentResult
var err error
if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
} else if wmSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
if len(results) > 1 {
results = results[:1]
}
} else if terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:]
}
if wmSelected {
var result config.DeploymentResult
result, err = deployer.DeployCompositor(wm, terminal, useSystemd)
results = append(results, result)
}
if err == nil && terminalSelected {
var terminalResults []config.DeploymentResult
terminalResults, err = deployer.DeployTerminal(terminal)
results = append(results, terminalResults...)
}
close(logChan)
@@ -344,6 +389,15 @@ func runSetup() error {
return nil
}
func isVoidSetup() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
// Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() {
@@ -379,10 +433,11 @@ func promptCompositor() (deps.WindowManager, bool) {
fmt.Println("Select compositor:")
fmt.Println("1) Niri")
fmt.Println("2) Hyprland")
fmt.Println("3) None")
fmt.Println("3) Mango")
fmt.Println("4) None")
var response string
fmt.Print("\nChoice (1-3): ")
fmt.Print("\nChoice (1-4): ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
@@ -391,6 +446,8 @@ func promptCompositor() (deps.WindowManager, bool) {
return deps.WindowManagerNiri, true
case "2":
return deps.WindowManagerHyprland, true
case "3":
return deps.WindowManagerMango, true
default:
return deps.WindowManagerNiri, false
}
@@ -434,38 +491,54 @@ func promptSystemd() bool {
}
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
homeDir := os.Getenv("HOME")
willBackup := false
if wmSelected {
var configPath string
switch wm {
case deps.WindowManagerNiri:
configPath = filepath.Join(homeDir, ".config", "niri", "config.kdl")
case deps.WindowManagerHyprland:
configPath = filepath.Join(homeDir, ".config", "hypr", "hyprland.conf")
}
if _, err := os.Stat(configPath); err == nil {
willBackup = true
}
if wmSelected && firstExistingPath(compositorPrimaryPaths(wm)) != "" {
return true
}
return terminalSelected && firstExistingPath([]string{terminalPrimaryPath(terminal)}) != ""
}
if terminalSelected {
var configPath string
switch terminal {
case deps.TerminalGhostty:
configPath = filepath.Join(homeDir, ".config", "ghostty", "config")
case deps.TerminalKitty:
configPath = filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
case deps.TerminalAlacritty:
configPath = filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
func compositorPrimaryPaths(wm deps.WindowManager) []string {
homeDir := os.Getenv("HOME")
switch wm {
case deps.WindowManagerNiri:
return []string{filepath.Join(homeDir, ".config", "niri", "config.kdl")}
case deps.WindowManagerHyprland:
return []string{
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
}
case deps.WindowManagerMango:
return []string{
filepath.Join(homeDir, ".config", "mango", "config.conf"),
filepath.Join(homeDir, ".config", "mango", "mango.conf"),
}
default:
return nil
}
}
func terminalPrimaryPath(terminal deps.Terminal) string {
homeDir := os.Getenv("HOME")
switch terminal {
case deps.TerminalGhostty:
return filepath.Join(homeDir, ".config", "ghostty", "config")
case deps.TerminalKitty:
return filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
case deps.TerminalAlacritty:
return filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
default:
return ""
}
}
if _, err := os.Stat(configPath); err == nil {
willBackup = true
func firstExistingPath(paths []string) string {
for _, path := range paths {
if path == "" {
continue
}
if _, err := os.Stat(path); err == nil {
return path
}
}
return willBackup
return ""
}
+148
View File
@@ -0,0 +1,148 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/spf13/cobra"
)
var setupHeadlessCmd = &cobra.Command{
Use: "headless",
Short: "Deploy DMS configurations without prompts",
Long: "Deploy compositor and terminal configurations non-interactively, for installers and scripts",
PersistentPreRunE: requireMutableSystemCommand,
RunE: func(cmd *cobra.Command, args []string) error {
compositor, _ := cmd.Flags().GetString("compositor")
terminal, _ := cmd.Flags().GetString("terminal")
noSystemd, _ := cmd.Flags().GetBool("no-systemd")
force, _ := cmd.Flags().GetBool("force")
skipExisting, _ := cmd.Flags().GetBool("skip-existing")
cmd.SilenceUsage = true
return runSetupHeadless(compositor, terminal, noSystemd, force, skipExisting)
},
}
func init() {
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().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("skip-existing", false, "Warn and skip instead of failing when a config already exists")
_ = setupHeadlessCmd.MarkFlagRequired("compositor")
setupHeadlessCmd.MarkFlagsMutuallyExclusive("force", "skip-existing")
}
func runSetupHeadless(compositor, terminal string, noSystemd, force, skipExisting bool) error {
wm, err := parseHeadlessCompositor(compositor)
if err != nil {
return err
}
term, termSelected, err := parseHeadlessTerminal(terminal)
if err != nil {
return err
}
deployCompositor := true
deployTerminal := termSelected
if !force {
if existing := firstExistingPath(compositorPrimaryPaths(wm)); existing != "" {
if !skipExisting {
return fmt.Errorf("%s config already exists: %s (use --force to overwrite with backup, or --skip-existing)", compositor, existing)
}
fmt.Printf("⚠ Skipping %s config, already exists: %s\n", compositor, existing)
deployCompositor = false
}
if deployTerminal {
if existing := firstExistingPath([]string{terminalPrimaryPath(term)}); existing != "" {
if !skipExisting {
return fmt.Errorf("%s config already exists: %s (use --force to overwrite with backup, or --skip-existing)", terminal, existing)
}
fmt.Printf("⚠ Skipping %s config, already exists: %s\n", terminal, existing)
deployTerminal = false
}
}
}
if !deployCompositor && !deployTerminal {
return nil
}
logChan := make(chan string, 100)
logDone := make(chan struct{})
go func() {
for msg := range logChan {
fmt.Println(" " + msg)
}
close(logDone)
}()
deployer := config.NewConfigDeployer(logChan)
var results []config.DeploymentResult
var deployErr error
if deployCompositor {
result, err := deployer.DeployCompositor(wm, term, headlessUseSystemd(wm, noSystemd))
results = append(results, result)
deployErr = err
}
if deployErr == nil && deployTerminal {
terminalResults, err := deployer.DeployTerminal(term)
results = append(results, terminalResults...)
deployErr = err
}
close(logChan)
<-logDone
for _, result := range results {
if !result.Deployed {
continue
}
fmt.Printf("✓ %s: %s\n", result.ConfigType, result.Path)
if result.BackupPath != "" {
fmt.Printf(" Backup: %s\n", result.BackupPath)
}
}
if deployErr != nil {
return fmt.Errorf("deployment failed: %w", deployErr)
}
return nil
}
func parseHeadlessCompositor(name string) (deps.WindowManager, error) {
switch name {
case "niri":
return deps.WindowManagerNiri, nil
case "hyprland":
return deps.WindowManagerHyprland, nil
case "mango":
return deps.WindowManagerMango, nil
default:
return 0, fmt.Errorf("unknown compositor %q (expected niri, hyprland, or mango)", name)
}
}
func parseHeadlessTerminal(name string) (deps.Terminal, bool, error) {
switch name {
case "":
return deps.TerminalGhostty, false, nil
case "ghostty":
return deps.TerminalGhostty, true, nil
case "kitty":
return deps.TerminalKitty, true, nil
case "alacritty":
return deps.TerminalAlacritty, true, nil
default:
return 0, false, fmt.Errorf("unknown terminal %q (expected ghostty, kitty, or alacritty)", name)
}
}
func headlessUseSystemd(wm deps.WindowManager, noSystemd bool) bool {
if noSystemd || wm == deps.WindowManagerMango {
return false
}
return !isVoidSetup()
}
+375
View File
@@ -0,0 +1,375 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"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/spf13/cobra"
)
var systemCmd = &cobra.Command{
Use: "system",
Short: "System operations",
Long: "System-level operations (updates, etc.). Runs against installed package managers directly; does not require the DMS server.",
}
var systemUpdateCmd = &cobra.Command{
Use: "update",
Short: "Apply or list system updates",
Long: `Apply or list system updates across detected package managers.
Default behavior is to apply available updates after prompting for confirmation.
Use --check to list updates without applying.
Examples:
dms system update --check # list available updates
dms system update # apply updates (interactive prompt)
dms system update --noconfirm # apply updates without prompting
dms system update --dry # simulate without changing anything
dms system update --no-flatpak --noconfirm # apply system updates only
dms system update --interval 3600 # set the server poll interval to 1h`,
Run: runSystemUpdate,
}
var (
sysUpdateCheck bool
sysUpdateNoConfirm bool
sysUpdateDry bool
sysUpdateJSON bool
sysUpdateNoFlatpak bool
sysUpdateNoAUR bool
sysUpdateIgnore []string
sysUpdateIntervalS int
sysUpdateListPmTime = 5 * time.Minute
)
func init() {
systemUpdateCmd.Flags().BoolVar(&sysUpdateCheck, "check", false, "List available updates without applying")
systemUpdateCmd.Flags().BoolVarP(&sysUpdateNoConfirm, "noconfirm", "y", false, "Apply updates without prompting")
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(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
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().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
systemCmd.AddCommand(systemUpdateCmd)
}
func runSystemUpdate(cmd *cobra.Command, args []string) {
switch {
case sysUpdateIntervalS >= 0:
runSystemUpdateSetInterval(sysUpdateIntervalS)
case sysUpdateCheck:
runSystemUpdateCheck()
default:
runSystemUpdateApply()
}
}
func selectBackends(ctx context.Context) []sysupdate.Backend {
sel := sysupdate.Select(ctx)
backends := sel.All()
if !sysUpdateNoFlatpak {
return backends
}
out := backends[:0]
for _, b := range backends {
if b.Repo() == sysupdate.RepoFlatpak {
continue
}
out = append(out, b)
}
return out
}
func runSystemUpdateCheck() {
ctx, cancel := context.WithTimeout(context.Background(), sysUpdateListPmTime)
defer cancel()
backends := selectBackends(ctx)
if len(backends) == 0 {
log.Fatal("No supported package manager found")
}
stopSpin := startSpinner("Checking for updates… ")
allPkgs, firstErr := collectUpdates(ctx, backends)
stopSpin()
allPkgs = filterUpdateTargets(allPkgs)
if sysUpdateJSON {
out, _ := json.MarshalIndent(map[string]any{
"backends": backendResults(backends, allPkgs),
"packages": allPkgs,
"error": errOrEmpty(firstErr),
"count": len(allPkgs),
}, "", " ")
fmt.Println(string(out))
return
}
printBackends(backends)
fmt.Printf("Updates: %d\n", len(allPkgs))
if firstErr != nil {
fmt.Printf("Error: %v\n", firstErr)
}
if len(allPkgs) == 0 {
return
}
fmt.Println()
for _, p := range allPkgs {
printPackage(p)
}
}
type backendResult struct {
ID string `json:"id"`
Display string `json:"displayName"`
Packages []sysupdate.Package `json:"packages"`
}
func backendResults(backends []sysupdate.Backend, pkgs []sysupdate.Package) []backendResult {
results := make([]backendResult, 0, len(backends))
for _, b := range backends {
var backendPkgs []sysupdate.Package
for _, p := range pkgs {
if sysupdate.BackendHasTargets(b, []sysupdate.Package{p}, true, true) {
backendPkgs = append(backendPkgs, p)
}
}
results = append(results, backendResult{ID: b.ID(), Display: b.DisplayName(), Packages: backendPkgs})
}
return results
}
func runSystemUpdateApply() {
checkCtx, checkCancel := context.WithTimeout(context.Background(), sysUpdateListPmTime)
defer checkCancel()
backends := selectBackends(checkCtx)
if len(backends) == 0 {
log.Fatal("No supported package manager found")
}
stopSpin := startSpinner("Checking for updates…")
pkgs, firstErr := collectUpdates(checkCtx, backends)
stopSpin()
pkgs = filterUpdateTargets(pkgs)
if firstErr != nil {
fmt.Printf("Warning: %v\n\n", firstErr)
}
printBackends(backends)
fmt.Printf("Updates: %d\n", len(pkgs))
if len(pkgs) == 0 {
fmt.Println("Nothing to upgrade.")
return
}
fmt.Println()
for _, p := range pkgs {
printPackage(p)
}
fmt.Println()
if !sysUpdateNoConfirm && !sysUpdateDry {
if !promptYesNo("Proceed with upgrade? [Y/n]: ") {
fmt.Println("Aborted.")
return
}
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
opts := sysupdate.UpgradeOptions{
Targets: pkgs,
IncludeFlatpak: !sysUpdateNoFlatpak,
IncludeAUR: !sysUpdateNoAUR,
Ignored: sysUpdateIgnore,
DryRun: sysUpdateDry,
UseSudo: true,
}
opts.AttachStdio = sysupdate.UpgradeNeedsPrivilege(backends, pkgs, opts)
onLine := func(line string) { fmt.Println(line) }
ran := false
for _, b := range backends {
if !sysupdate.BackendHasTargets(b, pkgs, opts.IncludeAUR, opts.IncludeFlatpak) {
continue
}
ran = true
fmt.Printf("\n== %s ==\n", b.DisplayName())
if err := b.Upgrade(ctx, opts, onLine); err != nil {
log.Fatalf("%s upgrade failed: %v", b.ID(), err)
}
}
if !ran {
fmt.Println("Nothing to upgrade.")
return
}
if sysUpdateDry {
fmt.Println("\nDry run complete (no changes applied).")
return
}
fmt.Println("\nUpgrade complete.")
}
func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupdate.Package, error) {
var all []sysupdate.Package
var firstErr error
for _, b := range backends {
pkgs, err := b.CheckUpdates(ctx)
if err != nil && firstErr == nil {
firstErr = fmt.Errorf("%s: %w", b.ID(), err)
}
all = append(all, pkgs...)
}
return all, firstErr
}
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 {
return pkgs
}
ignored := make(map[string]bool, len(sysUpdateIgnore))
for _, name := range sysUpdateIgnore {
ignored[name] = true
}
out := pkgs[:0]
for _, p := range pkgs {
if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR {
continue
}
if ignored[p.Name] {
continue
}
out = append(out, p)
}
return out
}
func runSystemUpdateSetInterval(seconds int) {
resp, err := sendServerRequest(models.Request{
ID: 1,
Method: "sysupdate.setInterval",
Params: map[string]any{"seconds": float64(seconds)},
})
if err != nil {
log.Fatalf("Failed: %v (is dms server running?)", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
fmt.Printf("Interval set to %d seconds.\n", seconds)
}
func promptYesNo(prompt string) bool {
if !stdinIsTTY() {
log.Fatal("Refusing to apply updates non-interactively. Re-run with --noconfirm or --check.")
}
fmt.Print(prompt)
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return false
}
switch strings.ToLower(strings.TrimSpace(line)) {
case "n", "no":
return false
default:
return true
}
}
func printBackends(backends []sysupdate.Backend) {
if len(backends) == 0 {
return
}
names := make([]string, 0, len(backends))
for _, b := range backends {
names = append(names, b.DisplayName())
}
fmt.Printf("Backends: %s\n", strings.Join(names, ", "))
}
func stdinIsTTY() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
return (fi.Mode() & os.ModeCharDevice) != 0
}
func stdoutIsTTY() bool {
fi, err := os.Stdout.Stat()
if err != nil {
return false
}
return (fi.Mode() & os.ModeCharDevice) != 0
}
// startSpinner prints an animated spinner to stdout for progress indication
func startSpinner(msg string) func() {
if !stdoutIsTTY() {
return func() {}
}
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
done := make(chan struct{})
go func() {
for i := 0; ; i++ {
select {
case <-done:
fmt.Print("\r\033[K")
return
case <-time.After(80 * time.Millisecond):
fmt.Printf("\r%s %s", frames[i%len(frames)], msg)
}
}
}()
return func() { close(done) }
}
var (
styleRepo = lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Bold(false)
styleName = lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
styleFrom = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
styleArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
styleTo = lipgloss.NewStyle().Foreground(lipgloss.Color("76")).Bold(true)
)
func printPackage(p sysupdate.Package) {
if !stdoutIsTTY() {
fmt.Printf(" [%s] %s %s -> %s\n", p.Repo, p.Name, defaultIfEmpty(p.FromVersion, "?"), defaultIfEmpty(p.ToVersion, "?"))
return
}
fmt.Printf(" %s %s %s %s %s\n",
styleRepo.Render("["+string(p.Repo)+"]"),
styleName.Render(p.Name),
styleFrom.Render(defaultIfEmpty(p.FromVersion, "?")),
styleArrow.Render("->"),
styleTo.Render(defaultIfEmpty(p.ToVersion, "?")),
)
}
func errOrEmpty(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func defaultIfEmpty(s, def string) string {
if s == "" {
return def
}
return s
}
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/trash"
"github.com/spf13/cobra"
)
var trashCmd = &cobra.Command{
Use: "trash",
Short: "Manage the user's trash (XDG Trash spec 1.0)",
}
var trashPutCmd = &cobra.Command{
Use: "put <path...>",
Short: "Move files or directories into the trash",
Args: cobra.MinimumNArgs(1),
Run: runTrashPut,
}
var trashListCmd = &cobra.Command{
Use: "list",
Short: "List trashed items across all known trash directories",
Run: runTrashList,
}
var trashCountCmd = &cobra.Command{
Use: "count",
Short: "Print the total number of trashed items",
Run: runTrashCount,
}
var trashEmptyCmd = &cobra.Command{
Use: "empty",
Short: "Permanently delete every trashed item",
Run: runTrashEmpty,
}
var trashRestoreCmd = &cobra.Command{
Use: "restore <name>",
Short: "Restore a trashed item to its original location",
Args: cobra.ExactArgs(1),
Run: runTrashRestore,
}
var (
trashJSONOutput bool
trashRestoreDir string
)
func init() {
trashListCmd.Flags().BoolVar(&trashJSONOutput, "json", false, "Output as JSON")
trashRestoreCmd.Flags().StringVar(&trashRestoreDir, "trash-dir", "", "Trash directory containing the item (default: home trash)")
trashCmd.AddCommand(trashPutCmd, trashListCmd, trashCountCmd, trashEmptyCmd, trashRestoreCmd)
}
func runTrashPut(cmd *cobra.Command, args []string) {
var failed int
for _, p := range args {
if _, err := trash.Put(p); err != nil {
log.Errorf("trash %s: %v", p, err)
failed++
continue
}
fmt.Println(p)
}
if failed > 0 {
os.Exit(1)
}
}
func runTrashList(cmd *cobra.Command, args []string) {
entries, err := trash.List()
if err != nil {
log.Fatalf("list trash: %v", err)
}
if trashJSONOutput {
if entries == nil {
entries = []trash.Entry{}
}
out, _ := json.MarshalIndent(entries, "", " ")
fmt.Println(string(out))
return
}
if len(entries) == 0 {
fmt.Println("Trash is empty")
return
}
for _, e := range entries {
marker := "F"
if e.IsDir {
marker = "D"
}
fmt.Printf("%s %s %s %s\n", marker, e.DeletionDate, e.Name, e.OriginalPath)
}
}
func runTrashCount(cmd *cobra.Command, args []string) {
n, err := trash.Count()
if err != nil {
log.Fatalf("count trash: %v", err)
}
fmt.Println(n)
}
func runTrashEmpty(cmd *cobra.Command, args []string) {
if err := trash.Empty(); err != nil {
log.Fatalf("empty trash: %v", err)
}
}
func runTrashRestore(cmd *cobra.Command, args []string) {
if err := trash.Restore(args[0], trashRestoreDir); err != nil {
log.Fatalf("restore: %v", err)
}
}
+52 -27
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
@@ -26,7 +27,7 @@ var windowrulesListCmd = &cobra.Command{
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
@@ -40,8 +41,7 @@ var windowrulesAddCmd = &cobra.Command{
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
// ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
@@ -55,7 +55,7 @@ var windowrulesUpdateCmd = &cobra.Command{
Args: cobra.ExactArgs(3),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
@@ -69,7 +69,7 @@ var windowrulesRemoveCmd = &cobra.Command{
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
@@ -83,7 +83,7 @@ var windowrulesReorderCmd = &cobra.Command{
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
return []string{"hyprland", "niri", "mango"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
@@ -118,9 +118,12 @@ func getCompositor(args []string) string {
if os.Getenv("NIRI_SOCKET") != "" {
return "niri"
}
// if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
// return "hyprland"
// }
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
return "hyprland"
}
if os.Getenv("MANGO_INSTANCE_SIGNATURE") != "" {
return "mango"
}
return ""
}
@@ -140,17 +143,14 @@ func writeRuleSuccess(id, path string) {
func runWindowrulesList(cmd *cobra.Command, args []string) {
compositor := getCompositor(args)
if compositor == "" {
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
log.Fatalf("Could not detect compositor. Please specify: hyprland, niri, or mango")
}
var result WindowRulesListResult
switch compositor {
case "niri":
configDir, err := utils.ExpandPath("$HOME/.config/niri")
if err != nil {
log.Fatalf("Failed to expand niri config path: %v", err)
}
configDir := filepath.Join(utils.XDGConfigHome(), "niri")
parseResult, err := providers.ParseNiriWindowRules(configDir)
if err != nil {
@@ -183,11 +183,7 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
result.DMSStatus = parseResult.DMSStatus
case "hyprland":
log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
if err != nil {
log.Fatalf("Failed to expand hyprland config path: %v", err)
}
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
if err != nil {
@@ -219,6 +215,38 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
result.Rules = allRules
result.DMSStatus = parseResult.DMSStatus
case "mango", "mangowc":
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
parseResult, err := providers.ParseMangoWindowRules(configDir)
if err != nil {
log.Fatalf("Failed to parse mango window rules: %v", err)
}
allRules := providers.ConvertMangoRulesToWindowRules(parseResult.Rules)
provider := providers.NewMangoWritableProvider(configDir)
dmsRules, _ := provider.LoadDMSRules()
dmsRuleMap := make(map[int]windowrules.WindowRule)
for i, dr := range dmsRules {
dmsRuleMap[i] = dr
}
dmsIdx := 0
for i, r := range allRules {
if r.Source == "dms/windowrules.conf" {
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
allRules[i].ID = dmr.ID
allRules[i].Name = dmr.Name
}
dmsIdx++
}
}
result.Rules = allRules
result.DMSStatus = parseResult.DMSStatus
default:
log.Fatalf("Unknown compositor: %s", compositor)
}
@@ -317,17 +345,14 @@ func runWindowrulesReorder(cmd *cobra.Command, args []string) {
func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
switch compositor {
case "niri":
configDir, err := utils.ExpandPath("$HOME/.config/niri")
if err != nil {
return nil
}
configDir := filepath.Join(utils.XDGConfigHome(), "niri")
return providers.NewNiriWritableProvider(configDir)
case "hyprland":
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
if err != nil {
return nil
}
configDir := filepath.Join(utils.XDGConfigHome(), "hypr")
return providers.NewHyprlandWritableProvider(configDir)
case "mango", "mangowc":
configDir := filepath.Join(utils.XDGConfigHome(), "mango")
return providers.NewMangoWritableProvider(configDir)
default:
return nil
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
)
type cmd struct {
+24
View File
@@ -0,0 +1,24 @@
package main
import (
"encoding/json"
"testing"
)
func TestDefaultImmutablePolicyAllowsSyncButBlocksEnable(t *testing.T) {
var policyFile cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &policyFile); err != nil {
t.Fatalf("failed to parse embedded CLI policy: %v", err)
}
if policyFile.BlockedCommands == nil {
t.Fatal("embedded CLI policy has no blocked_commands")
}
blocked := normalizeBlockedCommands(*policyFile.BlockedCommands)
if !commandBlockedByPolicy("greeter enable", blocked) {
t.Fatal("expected greeter enable to remain blocked on immutable/image-based systems")
}
if commandBlockedByPolicy("greeter sync", blocked) {
t.Fatal("expected greeter sync to remain available on immutable/image-based systems")
}
}
+6 -8
View File
@@ -12,23 +12,21 @@ import (
var Version = "dev"
func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupHeadlessCmd, setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd, pluginsLockCmd, pluginsRestoreCmd)
registryCmd.AddCommand(registryListCmd, registryAddCmd, registryRemoveCmd)
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(authCmd)
rootCmd.AddCommand(updateCmd)
rootCmd.SetHelpTemplate(getHelpTemplate())
}
func main() {
disableMemProfilingUnlessRequested()
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
+5 -8
View File
@@ -12,20 +12,17 @@ import (
var Version = "dev"
func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupHeadlessCmd, setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd, pluginsLockCmd, pluginsRestoreCmd)
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(authCmd)
rootCmd.SetHelpTemplate(getHelpTemplate())
}
func main() {
disableMemProfilingUnlessRequested()
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
)
type randrOutput struct {
+11 -3
View File
@@ -10,8 +10,13 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
// maxIPCMessageSize allows room for a 50 MB clipboard entry plus JSON/base64
// overhead in the line-delimited IPC response.
const maxIPCMessageSize = 96 * 1024 * 1024
func sendServerRequest(req models.Request) (*models.Response[any], error) {
socketPath := getServerSocketPath()
@@ -22,6 +27,7 @@ func sendServerRequest(req models.Request) (*models.Response[any], error) {
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), maxIPCMessageSize)
scanner.Scan() // discard initial capabilities message
reqData, err := json.Marshal(req)
@@ -61,6 +67,7 @@ func sendServerRequestFireAndForget(req models.Request) error {
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), maxIPCMessageSize)
scanner.Scan() // discard initial capabilities message
reqData, err := json.Marshal(req)
@@ -90,9 +97,10 @@ func tryServerRequest(req models.Request) (*models.Response[any], bool) {
}
func getServerSocketPath() string {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
runtimeDir = os.TempDir()
runtimeDir := utils.RuntimeDir()
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
return sessionSock
}
entries, err := os.ReadDir(runtimeDir)
+185 -580
View File
@@ -2,537 +2,29 @@ package main
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/qsipc"
)
type ipcTargets map[string]map[string][]string
// getProcessExitCode returns the exit code from a ProcessState.
// For normal exits, returns the exit code directly.
// For signal termination, returns 128 + signal number (Unix convention).
func getProcessExitCode(state *os.ProcessState) int {
if state == nil {
return 1
}
if code := state.ExitCode(); code != -1 {
return code
}
// Process was killed by signal - extract signal number
if status, ok := state.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
return 128 + int(status.Signal())
}
}
return 1
}
var isSessionManaged bool
func execDetachedRestart(targetPID int) {
selfPath, err := os.Executable()
if err != nil {
return
}
cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
cmd.Start()
}
func runDetachedRestart(targetPIDStr string) {
targetPID, err := strconv.Atoi(targetPIDStr)
if err != nil {
return
}
time.Sleep(200 * time.Millisecond)
proc, err := os.FindProcess(targetPID)
if err == nil {
proc.Signal(syscall.SIGTERM)
}
time.Sleep(500 * time.Millisecond)
killShell()
runShellDaemon(false)
}
func getRuntimeDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
return runtime
}
return os.TempDir()
}
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
}
func writePIDFile(childPID int) error {
pidFile := getPIDFilePath()
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
}
func removePIDFile() {
pidFile := getPIDFilePath()
os.Remove(pidFile)
}
func getAllDMSPIDs() []int {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var pids []int
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
pidFile := filepath.Join(dir, entry.Name())
data, err := os.ReadFile(pidFile)
if err != nil {
continue
}
childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
os.Remove(pidFile)
continue
}
proc, err := os.FindProcess(childPID)
if err != nil {
os.Remove(pidFile)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
os.Remove(pidFile)
continue
}
pids = append(pids, childPID)
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
if parentProc, err := os.FindProcess(parentPID); err == nil {
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
pids = append(pids, parentPID)
}
}
}
}
return pids
}
func runShellInteractive(session bool) {
isSessionManaged = session
go printASCII()
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
errChan := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
}()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting quickshell: %v", err)
}
// Write PID file for the quickshell child process
if err := writePIDFile(cmd.Process.Pid); err != nil {
log.Warnf("Failed to write PID file: %v", err)
}
defer removePIDFile()
defer func() {
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
if err := cmd.Wait(); err != nil {
errChan <- fmt.Errorf("quickshell exited: %w", err)
} else {
errChan <- fmt.Errorf("quickshell exited")
}
}()
for {
select {
case sig := <-sigChan:
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
return
}
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
case <-time.After(500 * time.Millisecond):
}
log.Infof("\nReceived signal %v, shutting down...", sig)
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
return
case err := <-errChan:
log.Error(err)
cancel()
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
}
}
}
func restartShell() {
pids := getAllDMSPIDs()
if len(pids) == 0 {
log.Info("No running DMS shell instances found. Starting daemon...")
runShellDaemon(false)
return
}
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("Error finding process %d: %v", pid, err)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
if err := proc.Signal(syscall.SIGUSR1); err != nil {
log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
} else {
log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
}
}
}
func killShell() {
pids := getAllDMSPIDs()
if len(pids) == 0 {
log.Info("No running DMS shell instances found.")
return
}
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("Error finding process %d: %v", pid, err)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
if err := proc.Kill(); err != nil {
log.Errorf("Error killing process %d: %v", pid, err)
} else {
log.Infof("Killed DMS process with PID %d", pid)
}
}
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), "danklinux-") && strings.HasSuffix(entry.Name(), ".pid") {
pidFile := filepath.Join(dir, entry.Name())
os.Remove(pidFile)
}
}
}
func runShellDaemon(session bool) {
isSessionManaged = session
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
if !isDaemonChild {
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
cmd.Env = os.Environ()
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
return
}
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
errChan := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
}()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
log.Fatalf("Error opening /dev/null: %v", err)
}
defer devNull.Close()
cmd.Stdin = devNull
cmd.Stdout = devNull
cmd.Stderr = devNull
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
// Write PID file for the quickshell child process
if err := writePIDFile(cmd.Process.Pid); err != nil {
log.Warnf("Failed to write PID file: %v", err)
}
defer removePIDFile()
defer func() {
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
if err := cmd.Wait(); err != nil {
errChan <- fmt.Errorf("quickshell exited: %w", err)
} else {
errChan <- fmt.Errorf("quickshell exited")
}
}()
for {
select {
case sig := <-sigChan:
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
return
}
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
case <-time.After(500 * time.Millisecond):
}
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
return
case <-errChan:
cancel()
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
}
}
}
var qsHasAnyDisplay = sync.OnceValue(func() bool {
out, err := exec.Command("qs", "ipc", "--help").Output()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "qs", "ipc", "--help")
cmd.WaitDelay = 500 * time.Millisecond
out, err := cmd.Output()
if err != nil {
return false
}
@@ -568,13 +60,34 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
return targets
}
func getShellIPCCompletions(args []string, _ string) []string {
func buildQsIPCBaseArgs() ([]string, error) {
cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
switch pid, ok := shellApp.SessionPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := shellApp.ResolveConfig(nil, nil); err != nil {
return nil, err
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
}
cmdArgs = append(cmdArgs, "-p", configPath, "show")
cmd := exec.Command("qs", cmdArgs...)
return cmdArgs, nil
}
func getShellIPCCompletions(args []string, _ string) []string {
baseArgs, err := buildQsIPCBaseArgs()
if err != nil {
log.Debugf("Error building IPC args for completions: %v", err)
return nil
}
cmdArgs := append(baseArgs, "show")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "qs", cmdArgs...)
cmd.WaitDelay = 500 * time.Millisecond
var targets ipcTargets
if output, err := cmd.Output(); err == nil {
@@ -590,7 +103,7 @@ func getShellIPCCompletions(args []string, _ string) []string {
if len(args) == 0 {
targetNames := make([]string, 0)
targetNames = append(targetNames, "call")
targetNames = append(targetNames, "call", "list")
for k := range targets {
targetNames = append(targetNames, k)
}
@@ -616,43 +129,6 @@ func getShellIPCCompletions(args []string, _ string) []string {
return nil
}
func getFirstDMSPID() (int, bool) {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
continue
}
proc, err := os.FindProcess(pid)
if err != nil {
continue
}
if proc.Signal(syscall.Signal(0)) != nil {
continue
}
return pid, true
}
return 0, false
}
func runShellIPCCommand(args []string) {
if len(args) == 0 {
printIPCHelp()
@@ -662,24 +138,15 @@ func runShellIPCCommand(args []string) {
if args[0] != "call" {
args = append([]string{"call"}, args...)
}
cmdArgs := []string{"ipc"}
switch pid, ok := getFirstDMSPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
log.Fatalf("Error finding config: %v", err)
}
// ! TODO - remove check when QS 0.3 is released
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
if tryDirectIPCCall(args) {
return
}
cmdArgs = append(cmdArgs, args...)
baseArgs, err := buildQsIPCBaseArgs()
if err != nil {
log.Fatalf("Error finding config: %v", err)
}
cmdArgs := append(baseArgs, args...)
cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
@@ -690,20 +157,42 @@ func runShellIPCCommand(args []string) {
}
}
// tryDirectIPCCall invokes the shell over its IPC socket without spawning qs.
// Any failure falls back to the qs child process path, which owns error output.
func tryDirectIPCCall(args []string) bool {
if len(args) < 3 {
return false
}
pid, ok := shellApp.SessionPID()
if !ok {
return false
}
result, isVoid, err := qsipc.Call(qsipc.SocketPathForPID(pid), args[1], args[2], args[3:])
if err != nil {
log.Debugf("Direct Quickshell IPC failed, falling back to qs: %v", err)
return false
}
if !isVoid {
fmt.Println(result)
}
return true
}
func printIPCHelp() {
fmt.Println("Usage: dms ipc <target> <function> [args...]")
fmt.Println("Usage: dms ipc call <target> <function> [args...]")
fmt.Println()
cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
baseArgs, err := buildQsIPCBaseArgs()
if err != nil {
printIPCHelpFailure(err)
return
}
cmdArgs = append(cmdArgs, "-p", configPath, "show")
cmdArgs := append(baseArgs, "show")
cmd := exec.Command("qs", cmdArgs...)
output, err := cmd.Output()
if err != nil {
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
printIPCHelpFailure(err)
return
}
@@ -731,3 +220,119 @@ func printIPCHelp() {
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
}
}
func printIPCHelpFailure(err error) {
fmt.Println("Could not retrieve IPC targets.")
if err != nil {
fmt.Printf(" %v\n", err)
}
fmt.Println()
fmt.Println(" Full docs: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc")
fmt.Println(" Try: dms ipc call <target> <function>")
}
// ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults
func ensureFontCache() {
if _, err := exec.LookPath("fc-list"); err != nil {
return
}
if _, err := exec.LookPath("fc-cache"); err != nil {
return
}
var fontsToCheck []string
if configDir, err := os.UserConfigDir(); err == nil {
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
if data, err := os.ReadFile(settingsPath); err == nil {
var settings struct {
FontFamily string `json:"fontFamily"`
MonoFontFamily string `json:"monoFontFamily"`
}
if err := json.Unmarshal(data, &settings); err == nil {
if settings.FontFamily != "" && settings.FontFamily != "Inter Variable" {
fontsToCheck = append(fontsToCheck, settings.FontFamily)
}
if settings.MonoFontFamily != "" && settings.MonoFontFamily != "Fira Code" {
fontsToCheck = append(fontsToCheck, settings.MonoFontFamily)
}
}
}
}
if len(fontsToCheck) == 0 {
return
}
output, err := exec.Command("fc-list", ":", "family").Output()
if err != nil || len(strings.TrimSpace(string(output))) == 0 {
log.Warnf("Font cache appears empty or corrupt, rebuilding...")
rebuildFontCache()
return
}
cacheFonts := strings.ToLower(string(output))
var missing []string
for _, font := range fontsToCheck {
if !fontInCache(strings.ToLower(font), cacheFonts) {
missing = append(missing, font)
}
}
if len(missing) > 0 {
log.Warnf("Font(s) not found in cache: %s — rebuilding...", strings.Join(missing, ", "))
rebuildFontCache()
}
}
func fontInCache(target, cache string) bool {
for line := range strings.SplitSeq(cache, "\n") {
for fam := range strings.SplitSeq(strings.TrimSpace(line), ",") {
if strings.TrimSpace(fam) == target {
return true
}
}
}
return false
}
func rebuildFontCache() {
cmd := exec.Command("fc-cache", "-f")
if output, err := cmd.CombinedOutput(); err != nil {
log.Warnf("Failed to rebuild font cache: %v\n%s", err, string(output))
} else {
log.Infof("Font cache rebuilt successfully")
}
}
// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
func logStartupFailure(exitCode int, uptime time.Duration, stderrTail string) {
if uptime >= 5*time.Second || exitCode == 0 || exitCode > 128 {
return
}
if containsFontCrashSignature(stderrTail) {
log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
} else {
log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
}
}
func containsFontCrashSignature(logStr string) bool {
logStr = strings.ToLower(logStr)
signatures := []string{
"fontconfig",
"freetype",
"ft_load_glyph",
"ft_face",
"fc-list",
"fc-cache",
"glyph",
"typeface",
}
for _, sig := range signatures {
if strings.Contains(logStr, sig) {
return true
}
}
return false
}
+89
View File
@@ -0,0 +1,89 @@
package main
import (
"context"
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/gpu"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
"github.com/AvengeMedia/dankgo/shellapp"
)
var shellApp = shellapp.New(shellapp.Config{
ID: "danklinux",
EnvPrefix: "DMS",
QSAppID: "com.danklinux.dms",
Version: Version,
Embedded: embeddedShell{},
Boot: bootBackend,
PreLaunch: preLaunch,
ExtraEnv: dmsExtraEnv,
OnUIExit: logStartupFailure,
SessionRestartExitCode: dmsSessionRestartExitCode,
TryManagedRestart: trySystemdRestart,
})
type embeddedShell struct{}
func (embeddedShell) Available() bool { return shellembed.Available() }
func (embeddedShell) Extract(baseDir string) (string, error) { return shellembed.Extract(baseDir) }
func (embeddedShell) Prune(baseDir, keep string) { shellembed.Prune(baseDir, keep) }
type dmsBackend struct {
srv *server.Server
done chan error
}
func (b *dmsBackend) SocketPath() string { return b.srv.SocketPath() }
func (b *dmsBackend) Close() { b.srv.Close() }
func (b *dmsBackend) Done() <-chan error { return b.done }
func bootBackend(ctx context.Context) (shellapp.Backend, error) {
config.CleanupStrayHyprlandConfFile(log.Infof)
server.CLIVersion = Version
srv := server.New()
if err := srv.Listen(); err != nil {
return nil, err
}
backend := &dmsBackend{srv: srv, done: make(chan error, 1)}
go func() {
defer func() {
if r := recover(); r != nil {
backend.done <- fmt.Errorf("server panic: %v", r)
}
}()
backend.done <- srv.Serve(false)
}()
return backend, nil
}
func preLaunch() {
go printASCII()
ensureFontCache()
}
func dmsExtraEnv(string) []string {
var env []string
if selfPath, err := os.Executable(); err == nil {
env = append(env, "DMS_EXECUTABLE="+selfPath)
}
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
if _, set := os.LookupEnv("MALLOC_CONF"); !set {
env = append(env, "MALLOC_CONF=thp:never,narenas:4,dirty_decay_ms:3000")
}
env = append(env, gpu.EGLVendorEnv()...)
return env
}
Loaded 100 of 1847 files, more files were not shown because too many files have changed in this diff. Show more