- Settings, plugins and session state are backed up before dms changes w/restore options when returning to stable
- Snapshots are only taken when dms itself changes channel
- Info annotated on danklinux docs
- dankinstall enables EPEL 10 and installs from the existing COPRs. niri is the supported compositor on EL.
- Hyprland, mango and miracle-wm on EL are supported thanks to @kmf's dank-ws-copr repos
Closes#3442
Theme has no fontWeightNormal, so every unselected category tab in the
keybinds cheat sheet assigned undefined to font.weight. QML rejects
that and keeps the last value, so a tab stayed Medium after it was
deselected, and unselected tabs ignored the font weight setting.
Use Theme.fontWeight, the shell's regular weight.
On Hyprland, an island holding keyboard focus runs a focus grab that
only whitelisted the bar window. The full-screen dismiss layer wasn't
on the list, so the first click outside only ended the grab and the
card needed a second click to close. That hit every card that takes
keyboard focus (weather right away, media after a click inside).
Add the dismiss layer to the grab, the way DPopout whitelists its
background window.
* feat(island): add system tray slot and overflow menu to compact home
- Add system tray slot and overflow expander menu in Dynamic Island
- Share tray icon path resolution and DBus context menu fallback in TrayMenuManager
- Support edge-anchored context menu positioning across top, bottom, left, and right
- Lock bottom and edge anchoring in DContextMenu to prevent animation jitter
* fix(island): only instantiate tray menus when tray slot is enabled
* feat(island): expand tray item and caret click area to slot height
* fix(island): use Theme.surfaceTextHover and restore DContextMenu height behavior
* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island
* Revert "refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island"
This reverts commit 29888f89cd.
* refactor(tray): use shared TrayMenuManager helpers and TrayItemIcon, support tray order and IPC in island
* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid
* feat(island): support tray item drag and drop reordering
* fix(island): prevent re-entrant tray drag reordering on model destruction
* fix(tray): remove native submenu anchor hydration to prevent popup stutter
* fix(tray): decouple submenu hydrator from anchor window to avoid stutter
* fix(tray): restore anchor window on submenu hydrator and disable on island
* tray: fix bar overflow surface & reuse max visible items on the island
Qt skips QPA plugins built for a different major.minor without any
visible error. Out-of-tree plugins such as the AUR qt6ct-kde keep the
file in place after a Qt update, so the check reported them as
installed while Qt apps and tray icons lost their theme. Compare the
version in the plugin's .note.qt.metadata ELF note with the installed
Qt and warn on a mismatch.
* feat(bar): support auto-hide for island in overlay mode
* fix(settings): restore selectedIslandDocked scope for overlay islands
* fix(bar): keep edge hover strip while island auto-hide is revealed
* island: open media and the power menu in one step, fix dash edits
- Clicking media on the home face opens the player. Closing a sheet
goes back to the face the user picked, and an opt-in setting
switches to media when playback starts.
- The power menu can open in the island (Super+X, the dock button),
with its own route row in the island settings.
- An island collapses when another popout opens on its screen.
- The widgets page hides the center section the island replaces.
- Dragging or resizing a dashboard card no longer blanks the overview:
Qt hands the committed list over as a non-array, which read as empty.
- The island blur region stays inside the rounded fill.
* fix(island): address review feedback on switching faces by mouse
Opening the player with one click removed the only mouse path from
the home face to the media face. A right-click on the collapsed island
now swaps between the two while media is available.
The swap resizes the island under the pointer, so hover-open stays off
until the pointer leaves. Without a face to swap to, the press is not
accepted and reaches the bar's own right-click action as before.
* notifications: allow extra conditions per rule
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* notifications: add translator context to the rule condition strings
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* don't use deprecated widgets
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bbedward <[email protected]>
The rename IPC always read the name from NiriService, so on Hyprland the
field opened empty. Hyprland names are stored as "<id> <name>", so the
prefill drops that prefix to keep a second rename from stacking it.
Pam.qml holds the prompt text PAM asked for and separates "a response is
in flight" from "the conversation is open", so the UI stops treating an
open conversation as busy. The lock screen shows the prompt instead of a
hardcoded "Password", and clears the field when a new prompt arrives.
* displays: send the vrr setting on the wlr-output path
The toggle shown on compositors without their own output writer (sway,
labwc, ...) was dropped when the heads were built, so it never applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* remove test
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bbedward <[email protected]>
qtengine had its own plugin check, which printed its rows at the end of
the Environment section instead of under QT_QPA_PLATFORMTHEME, with
different names and messages than qt6ct or kde.
Fold it into the shared platform theme check: the Qt6 plugin is always
checked, the Qt5 plugin too when QT_QPA_PLATFORMTHEME names qtengine and
a Qt5 build tool can be asked. Also add a row for
~/.config/qtengine/config.json, which is what makes Qt apps follow the
DMS colors, and say in verbose mode that an unset
QT_QPA_PLATFORMTHEME_QT6 falls back to QT_QPA_PLATFORMTHEME.
The XDG_MENU_PREFIX check only looked in /etc/xdg/menus, so a menu file
in ~/.config/menus, which the menu spec searches first, was reported as
missing. Search the user config dir and every XDG_CONFIG_DIRS entry.
* settings: explain why the autostart settings page is empty
Fixed an issue where the autostart apps page was blank if
"xdg-desktop-autostart" was inactive.
The page was still reachable through the search function but not present
in the "Applicatiosn" settings sidebar tab.
* settings: point unavailable autostart applications to new docs section
* plugins: forward directory metadata (version, icon, permissions, dates) to the shell
* plugins: ranking module for recommendations, trending and picks
* plugins: mark new and recently updated plugins on the browser cards, keep badges clear of the vote count
* i18n: allow the lowercase plugin recency badge next to the New action
The label had no width, so a long one kept its natural width and drew
under the toggle in a narrow settings panel. Give it the column width,
like the description below it; StyledText already wraps by default.
* fix(settings): show what frame mode controls
With frame mode on, many settings did nothing or were hidden while
still applying, and nothing said so. The frame takes its color and
opacity from the first non-island bar, but that bar's Surface card is
hidden while the frame styles it, so neither had a visible control.
Add Background and Override/Opacity rows to the Appearance Frame card,
bound to the same frame bar the renderer reads. Settings the frame
provides in connected mode get a "Managed by Frame in Connected Mode"
note (Interface style border, corner style, background blur and dock
opacity, shadow targets, launcher border); settings with no effect get
"Disabled by Frame Mode" (maximize detection, bar shadow, the bar's
overlay layer in separate mode). Bar size, the bar's overlay layer in
connected mode, dock Over fullscreen and the controls of overlay-layer
docks and launchers are shown again, since they still apply.
Notification Over fullscreen shows as forced on when the frame covers
every display, and launcher rows the Spotlight style ignores are
disabled.
Also relabel the frame Width row as Thickness, raise the inset padding
maximum to its automatic range, and drop a FrameWindow handler for the
removed frameOpacity signal. No new strings.
* settings: show what frame mode controls, fix review findings
---------
Co-authored-by: purian23 <[email protected]>
* fix(frame): make connected mode honor the settings it overrides
Several frame-mode behaviors contradicted what the settings UI shows:
- Frame -> Blur gated blur for every shell surface in connected mode,
including the Settings window, polkit, context menus, OSDs and toasts.
WindowBlur now defaults to Background blur; only surfaces painted
with the frame color (connected popouts, slideouts, Control Center)
follow Frame -> Blur, and frame-owned surfaces are blurred by the
frame as before.
- The notepad slideout painted its own fill at the notepad
transparency. On screens where the frame owns connected chrome it now
claims a frame surface (new connected_arc slot 5), so the frame paints
it flush with the cutout, with fillets, and follows the slide.
- Notification popups painted a card surface on top of the frame body,
stacking fills. That layer is transparent when the frame paints it.
- Connected mode stripped shadow, border and corner styling from every
bar, including bars on displays without a frame, and hid the rows.
Styling is now stripped and restored per bar based on whether it
covers a frame display, re-checked on screen, frame display and
display-name-mode changes.
- Auto popup gaps and Gap size were hidden in separate mode although
they still apply there; they now hide only in connected mode.
- The launcher style picker was hidden in connected mode while Island
still overrode the connected launcher. Connected mode now always uses
the connected launcher; the picker shows Full, disabled, with a note
linking to the Bar layout picker.
- The frame shadow ignored Shadows -> Apply to -> Bars.
* fix(frame): address review feedback on connected mode
- Skip bar style reconciliation while the screen list is empty, so a
hotplug doesn't restore and re-strip every bar.
- Replace the 16 copies of the connector blur anchor and cutout in
FrameWindow with a ConnectorBlurRegion inline component.
- Let connected mode use the island launcher again now that the island
can sit in any bar. The picker offers Full and Island there and hides
Spotlight, which opens the full launcher in that mode anyway; the note
and the SettingsControlledBy bar target are dropped.
* frame: address review findings on connected mode
---------
Co-authored-by: purian23 <[email protected]>
- new blur variants, glow, wobble, motion blur and groupbar support
- new inputs support for mouse, touchpad and keyboard settings
- greeter, cheatsheet and scroll capture dispatch in Lua
- scrolling workspace screenshot focus fix
- fix scrolling screenshots from being cut-off on fractional-scale displays
- workspaces are keyed by address on the beta branch and upcoming stable release where named workspaces have no id
- bind flags, per-device binds, new window-rule keys and effects
- outputs default to preferred/auto/auto and gate HDR/VRR on hardwareDetails
- new dms setup/installer systemd session support on hyprland-session.target
- Auto detect the snake_case dialect of Mango master from the binary, write configs and respell existing fragments with:
`dms config mango-migrate`
- Setup backs up and replaces config.conf, moves binds and rules into dms/
- Start DMS from mango-session.target with systemd or exec-once without
- fix cheatsheet run action and bind precedence, window-rule and display save updates
- Support urgent active tag, greeter loading /etc/mango, per-screen overviews
* settings: add support for multiple settings files
PR: #3425
Introduces a drop-in settings files drectory - config.d/.
SettingsStore.toJson preserves settings set in any file (even set to
default values).
* Unload settingsFolderModel if config.d doesn't exist
In scroll fill mode the parallax layer decodes its own copy of the
wallpaper (parallaxImage, asynchronous). renderActive only waited for
currentWallpaper and nextWallpaper, so after a wallpaper change it could
drop to false and let updatesEnabled run out after the three settle
frames while parallaxImage was still loading. The window then kept
showing the black backing until something else, like a workspace
switch, started rendering again.
Count parallaxImage loading as active too. When it turns Ready,
renderActive flips back and onRenderActiveChanged invalidates, so the
finished image gets its settle frames.
Fixes#3702. Diagnosis and fix by @clethbridgedaniel in the issue.
it doesnt interfere, with a setting to turn it off
Some compositors like kwin/labwc/sway cannot have this without breaking
some built in layering they do.
niri can have this, but for focus follow mouse it may need to be disabled.
fixes#3717
* fix(hyprland): write window rules Hyprland 0.56 accepts, keep DMS windows opaque
The Lua window-rule writer emitted match keys and effects Hyprland 0.56
does not know: `floating`/`pinned`/`initialised` as match keys and
`noborder`/`norounding` as effects. It now writes `float`/`pin` and
`border_size = 0`/`rounding = 0`; the parser still reads the old
spellings, so existing files load and are rewritten on the next save.
"No Rounding" now suppresses the corner radius so a rule has one
`rounding` key.
The rule editor no longer offers fields the Hyprland writer drops
(Window Opening output/workspace, Size Constraints, the Initialised
match), and Save is disabled while only one half of a move or size
pair is filled. Every window-rule write now reloads the compositor
through one helper, which also covers Hyprland.
DMS windows draw their own translucent background, so any compositor
opacity rule or decoration opacity multiplied on top of it. DMS now
keeps a hidden rule for its own app id: `opaque = true` on Hyprland
(applied after rule and decoration opacity, independent of order, and
leaving the window's own alpha alone) and `opacity 1.0` on niri, which
the store keeps last. It is re-added on start when missing and hidden
from the rules list and the focused-window menu.
Compositor layout gains a Hyprland "Window opacity" slider writing
decoration.active_opacity/inactive_opacity; 100 leaves the user's own
config in charge.
HyprlandService.reloadConfig queues callbacks: Proc keeps one callback
per id, so a reload requested while the layout writer's reload was
pending dropped its callback and left layout generation stuck.
* fix(windowrules): cap corner radius at 20
Hyprland 0.56's Lua window_rule rejects rounding above 20 ("value 24
is more than the maximum of 20") and the rule loses its rounding. The
Hyprland writer now clamps to 20, and the editor slider stops at 20
for every compositor.
dms screenshot put only image/png on the clipboard. Terminals can only
paste text, so pasting a screenshot into a terminal app (e.g. a CLI
that attaches images from a pasted path) did nothing, while a file
copied through the server's clipboard.copyFile pastes its path.
When the screenshot is saved to a file, copy it the same way:
gnome-copied-files, text/uri-list, the path as text/plain, and the
image bytes. The server is asked first so the copy lands in history;
without it, the same offers are served locally. The offer list is
shared with Manager.CopyFile through clipboard.FileOffers.
copyMultiFork now waits for the child to take the selection, like
copyForkCached, so a failed local copy is reported instead of lost.
dms cl copy --download shares this path and now falls back to a local
copy when the server is down or rejects the file. --no-file and PPM
keep the image-only copy.
* feat(notifications): timed per-app popup mutes (1h / 4h) with expiry
* fix test assertions
* notifications: let a timed mute expire without taking the rule's other settings
---------
Co-authored-by: bbedward <[email protected]>
* 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.
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
* feat(freebsd): add comprehensive native FreeBSD support
* Update BatteryService.qml
* Add battery probe completion and fallback updates
* Add isVideoSource function and update PwObjectTracker filter
* Add isPipewireVideoSource function to AudioService
Added a function to check if a Pipewire node is a video source.
* Refactor video source check to use AudioService
* Remove unnecessary call in BatteryService.qml
* Add call to updateFreeBsdBatteryFallback on completion
* Simplify AudioService by eliminating redundant bindings
Removed unnecessary bindings in the AudioService loader to streamline the loading process.
* Improve error handling in CheckUpdates function
Handle specific exit error for package upgrade check.
* Enhance FreeBSD network detection logic
* Implement FreeBSD Wi-Fi hardware discovery in Initialize
Added FreeBSD Wi-Fi hardware discovery and updated initialization logic to handle cases where no wpa_supplicant control socket is found.
* Implement Ethernet state monitoring in StartMonitoring
Added monitoring for Ethernet state updates in StartMonitoring function.
* Add Ethernet management functions to backend
* Remove unimplemented Ethernet control methods
Removed unimplemented Ethernet control methods from WpaSupplicantBackend.
* Implement SetWiFiEnabled method for WiFi control
Add error handling and state management for WiFi control
* Update core/internal/server/network/backend_wpa_ethernet.go
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* Implement Ethernet monitoring in WpaSupplicantBackend
* Refactor startEthernetMonitor and add debounce logic
Refactor the startEthernetMonitor function to improve readability and maintainability. Introduce a debounce mechanism for processing relevant FreeBSD route events.
* Reorder import statements in backend_pkg.go
* Remove unnecessary blank line in backend_wpa_ethernet.go
* Implement FreeBSD audio polling based on UI visibility
Introduce a polling mechanism for FreeBSD audio device updates based on visibility of UI elements.
* Add readonly freeBsdPollingRequested property and optimize polling
Introduce a readonly property to determine if FreeBSD polling is requested based on various UI conditions. Update command execution to prioritize polling efficiency.
* Simplify freeBsdPollingRequested logic
Removed unnecessary conditions from freeBsdPollingRequested property.
* Implement reference counting for battery consumers
Added reference counting to manage battery information consumers and updated related functions for better battery state handling.
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* policy: load the CLI policy file from an internal package
The plugin and theme registries need to read the policy too, and
internal packages cannot import package main.
* registries: let the CLI policy turn the registries off
With disable_registries set in /usr/share/dms/cli-policy.json or
/etc/dms/cli-policy.json (the /etc file wins), the plugin and theme
registries start without sources, so nothing is cloned or pulled.
Installed plugins and themes keep working.
deprecation notice to plugin compatibility shims
- When qml-common became a thing, we left qs.Widgets shims in place for
plugin compatibility
- It's annoying to need to maintain them when qml-common has any change,
add a warning so we can safely deprecate them in the future
- One shot rename the common stuff to D to make it shorter and nicer to
write
On compositors with no include support (sway, labwc, ...) the displays page
showed a setup banner whose fix button can never succeed.
Co-authored-by: Claude Opus 5.5 <[email protected]>
The drawer's modal adapter lacked alignedX/alignedY/effectiveScreen, so
LauncherContextMenu treated the offsets as 0 and opened the menu near the
top-left of the screen.
Co-authored-by: Claude Opus 5.5 <[email protected]>
A directory's stat size passes the MaxEntrySize check, so CopyFile tried to
read it and every folder restore failed with "is a directory". Skip the read
for directories; the uri-list and plain path offers already cover them.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* lock: take the lock back when the shell restarts into a locked session
Check the state once when the module completes, on locked or lockedHint,
and handle it the way onSessionLocked does, including the wait while the
session is inactive. After a logind Unlock that the shell ignored for a
lock it started, only lockedHint still says locked; and the module loads
asynchronously, so the state can arrive before its Connections exist.
* loginctl: read LockedHint from logind on unlock
logind's Unlock signal does not clear LockedHint, only the locker does.
Forcing it false told a shell restarted in that window that the session
was no longer locked.
* added launcher dmenu mode
* fixed prechecker issues
* added unit tests for dms dmenu command
* added workaround for matugen printing error message to stdout
* correct help message: matching is case-*in*sensitive
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* no longer save dmenu selections to history; now properly close dmenu sockets
* increased parity with rofi -dmenu flags; added -i as an ignored flag to match dmenu
* go mod tidied
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* settings: add a link to the source code in plugin details
* style: remove unnecessary whitespace, add empty line
* show link to the plugin source instead of explanation
* fix: check for empty string instead of undefined
* feat(dock): push neighboring icons apart on magnification
* fix(dock): cap magnification expansion and disable it in taskbar mode
* fix(dock): scale pushed icon offsets to the capped magnification expansion
* fix(dock): avoid binding loop when reading expansion ratio
- caps lock, power profile, idle inhibitor and charging pulse in the island
- microphone mute and volume as a level transient
- privacy slot while mic, camera or screen share is in use
- duo battery style: arc around the network glyph with signal dots
* 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.
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.
EntryToFile and the preview helpers stripped file:// without
percent-decoding, so history entries whose paths contain non-ASCII
characters stat'ed a literal %E5... path and copyEntry failed with
"file not found". CopyFile additionally hard-failed any file larger
than MaxEntrySize (5MB default) right after stat, so even decoded
entries for bigger files could never be restored to the clipboard.
Decode the path in EntryToFile, uriListPreview and
tryReadImageFromURI. In CopyFile only read file content when it fits
the size cap; oversized files still get offered as uri-list /
gnome-copied-files / plain path, and the stored entry keeps the file
URI instead of an empty body.
Keep encode and decode symmetric: every URI this service stores or
offers now goes through encodeFileURI (percent-encoded) and every
read path decodes exactly once through pathFromFileURI, so a literal
%20 in a file name survives the round trip. Regression tests cover
space / literal %20 / # / CJK / oversized names across EntryToFile,
uriListPreview, the image probe and handleCopyEntry.
* bluetooth: identify codecs from the profile description
PipeWire describes profiles in the daemon's locale and only names the codec in
that description for the bare profile (LHDC settles on "a2dp-sink"), so the old
English "codec %s" match dropped LHDC profiles on non-English systems. Parsing
the translated shape instead only moves the coupling to word order and
punctuation.
Emit the profile index and raw description from the lua script and let the UI
name the codec by matching the untranslated literal from one codec table.
Switch by index, falling back to the profile name or codec literal, so no
translated text is parsed. Also teach the BlueZ fallback the Savitech ids
(LHDC v5 and v3).
Test: quickshell/tests/bluez-codec.test.mjs
* bluetooth: show unknown codec without masking bluez
* bluetooth: ignore stale codec query results
* feat(screenshot): add Ctrl+C shortcut to copy selection without saving
Add Ctrl+C shortcut in interactive region selection mode to immediately
copy the current selection (or hovered snap target) to clipboard without
saving to a file.
- Works during mid-drag, on hovered targets, or on existing selections
- Bypasses file saving and forces clipboard output
- Emits aborted status in JSON mode so external callers (such as Quick
Capture) exit cleanly without launching an annotation editor
- Exits cleanly without error if triggered with no selection
- Updates HUD items with Ctrl+C copy shortcut
- Hides corner resize handles and maintains crosshair cursor while dragging with Ctrl held
* fix(screenshot): bypass stdout and scroll phase when copyOnly is active
- Disable stdout output when copyOnly is active to ensure the image
is copied to the clipboard instead of being written to stdout.
- Write 'Copied to clipboard' to stderr if --stdout was originally passed.
- Bypass entering scrollPhase in finishSelection() if copyOnly is set,
immediately capturing and copying the selected region.
Focused dot expands to 3× width (horizontal dock) or 3× height
(vertical dock) using Theme.shortDuration + Easing.OutCubic.
Applies to both circle and line indicator styles.
* feat(dock): add proximity icon magnification with customizable curves
* fix(dock): restrict proximity magnification to dock and add translator context
* feat(screenshot): snap to active popouts and modals in region mode
Allow region screenshot selector to detect and snap to visible shell
popouts and modals on hover and click or keyboard confirmation.
- PopoutManager.qml: add getActiveSurfaces() collecting visible
DankPopout and DankModal global bounding boxes
- DMSShellIPC.qml: add screenshot.getSurfaces IPC endpoint
- core: fetch snap targets from shell IPC during region screenshot init
- selector: highlight snap targets on hover with surface name and dimensions
- selector: click or Enter/Space on hovered target locks selection to surface bounds
- selector: allow hover snap over pre-selection (SelectionState.fromPreSelect)
- selector: reset selection on degenerate (1x1) click release for quick re-selection
- unit tests for display name formatting, hover detection, and render bounds
* fix(screenshot): correct snap off-by-one and drag-away cancellation
- region_input: fix drag-away snap cancellation by setting hasSelection=true
and clearing preSelect so freeform dragging renders and is not discarded
- region_input: add snapToTarget helper subtracting 1 device pixel (devPxX/Y)
from currentX/currentY to prevent inclusive coordinate off-by-one (420x580 -> 420x580)
- region_render: compute hover bounds with w = round(width * scaleX) and
x2 = x1 + w - 1 to eliminate +1 device pixel in render bounds
- region_snap_test: assert exact 420x580 bounds for both hover and snapped states
* style(screenshot): format region_snap_test.go with golangci-lint-fmt
* 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
A compositor resolves a bind's keysym on the first level of the active
layout. Layouts that put punctuation higher up therefore cannot reach it
at all: de:neo has bracketleft on layer 4 and plain de on AltGr, so
Mod+bracketleft is a bind that can never fire. The editor recorded and
stored it anyway and gave no sign that anything was wrong (#3585).
`dms keybinds keymap` now reports what the seat's xkb keymap puts on the
first level of every physical key, and the editor says so instead of
letting a dead bind through.
The keymap a compositor hands out writes its keysyms as hex, so only the
range the keymap package can name back is usable for the comparison.
That is the ASCII range, which is where the problem lives, and the
command reports the vocabulary alongside the keys so the caller can tell
"the layout cannot reach this" from "we could not name it" and stay
quiet in the second case.
* lock: lock before sleep when the command skips logind
Custom hibernate never emits PrepareForSleep, so the session stayed unlocked. Wait for the compositor lock on every sleep path, and release the delay inhibitor if that lock finished first.
* lock: release the sleep inhibitor only after lockerReady
* lock: read the sleep inhibitor under its mutex in the race test
Add word processor, spreadsheet, presentation and archive manager
selectors to default apps, fix calendar lookup to query text/calendar
first so Dank Calendar is detected, and expand image mime coverage
with tiff/heic/heif/jxl.
scrollbar: interface style toggle and no scrollbars in the dash
dash: never allow scrollbars
cc: widget sheet cards, header close button and content-sized dialogs
settings: stop repeating the page title as the first card label
translations: index untitled cards by their page, not a nested row
fixes#3604fixes#3609
* powermenu: derive shortcuts and extra actions from one source
The power menu matched keys with a switch that repeated the key hints
getPowerActionData already returns, plus a second key list for release.
Look the pressed key up against the hints of the visible actions instead,
so every action with a hint gets its shortcut and the two cannot drift.
Side effects:
- Ctrl+letter no longer triggers actions. Only Ctrl+P was excluded
before, so Ctrl+R still rebooted. Ctrl combinations are navigation.
- U now triggers switch user when it is shown. Its hint already said U.
- Releasing a letter with no visible action no longer cancels a hold.
SessionService now owns the custom:N action ids as customPowerActions
and extraPowerActions, which the power menu and the launcher both use.
Actions resolve by index into that list instead of slicing the prefix.
* powermenu: add actions that reboot into a chosen efi boot entry
Settings → Power & Sleep gets a "Reboot to another OS" card that lists
the firmware's active EFI entries through efibootmgr. Each saved entry
becomes a "Reboot to <name>" action in the power menu and the launcher,
bound to 1-9 in the order it was added.
Picking one lists the entries again, and stops with a toast if the id no
longer carries the saved label. Otherwise it sets BootNext through pkexec
and reboots through SessionService, so custom reboot commands still apply.
BootEntryService reports why entries are unavailable: not booted in UEFI
mode, efibootmgr missing, or efibootmgr failing with its stderr. Saved
entries stay listed in settings whatever the status, so a synced config
on another machine can still remove them.
efibootmgr parsing and the picker's filtering live in BootEntries.js,
covered by tests/boot-entries.test.mjs.
* powermenu: ignore special keys in action shortcuts
- The island is now a bar widget so any bar can host one alongside the island layout and dot
- Routing got simpler: pick which shortcuts open here, the rest follow the shared rules
- IPC close/hide take down every surface showing the activity
- Qt 6.11 pointer prune workaround so the expanded island stays clickable
* fix(sysmon): prevent transparency flicker when switching to performance tab
* fix(sysmon): preload tabs on modal open and ensure immediate floating surface resolution
* 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
* lock: keep the fingerprint reader usable across suspend
The reader stopped responding after the lid was closed and opened, after a long
idle period, and after suspend.
The shell held a PAM session for as long as the screen was locked, so a suspend
always landed on a running verify. fprintd then suspends the device under the
verify, pam_fprintd's ReleaseDevice fails with "device is still busy", and the
claim is never released, so every later Claim fails until fprintd restarts. The
reader is now released when the monitors blank and armed again when they come
back, so a suspend has nothing left to wedge.
The PAM profile drops to max-tries=1 and sets an explicit timeout. pam_fprintd
only counts a wrong finger, so one finished session is now one wrong scan and
maxFprintTries means the number of scans it names rather than five times that.
A timeout expiry arrives as PamResult.Error, exactly like a device fault, so a
session that ran at least as long as the configured timeout is treated as a
planned renewal instead of spending the error budget and painting an error.
A suspend that still lands while the screen is on wedges as before. Recovery now
waits out fprintd's idle timeout instead of climbing to it, which brings the
reader back about 37 seconds after resume instead of about 98.
SessionService gains lidOpened from UPower, because logind can ignore the lid on
AC and resume alone is not enough. The lock PAM state machine is fixed alongside:
PamResult.Failed was dropped, a pending retry outlived the contexts that owned
it, and an availability check reset the wrong-scan counter.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q5LQHivuucTLXwLkvZMfcn
* lock: time the fingerprint session from the verify, not the claim
pam_fprintd's timeout runs from the verify, but the session age was measured
from start(), which folds the claim into it. A slow claim plus a verify that
failed before its own timeout could add up past the timeout and be taken for a
planned renewal, skipping the backoff. A session that never reached a verify had
the same problem.
The age now starts at the message pam_fprintd sends when the verify begins, so a
session that never verified can never qualify as an expiry, and a slow claim no
longer ages a fault. The message is translated and quickshell clears it before
completed, so only its timing is used, never its text.
That message arrives after pam_fprintd has already armed its deadline, because
VerifyStart is asynchronous, so the age read here runs short of the real timeout
and a slack makes it up. The bias is deliberate. A fault misread as a timeout
costs one extra retry, while a timeout misread as a fault parks the reader behind
a backoff that grows to a minute, which on an idle lock screen would leave it
unusable most of the time.
Also corrects the comment that claimed a timeout is reported without a message.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q5LQHivuucTLXwLkvZMfcn
---------
Co-authored-by: Claude Opus 5 <[email protected]>
Autostart apps (e.g. KeePassXC) that call RegisterStatusNotifierItem
before the watcher owns its name fail silently and never re-register.
Until now the recovery scan only ran on suspend/resume or watcher
owner change, so first-boot orphans were never rescued.
Schedule a one-time delayed scan when the watcher first appears.
recoverTrayItems() already deduplicates via registeredConnIDs, so this
cannot double-register apps that registered normally.
Pressing Enter in the name field fires both the text field's accepted
signal and the dialog's own Return handler, so submitAndClose() runs
twice. The first call renames and hides the window, which clears the
field; the second call then renames to an empty string and unsets the
workspace name. The OK button only fires once, so it was unaffected.
Ignore submits once the window is hidden.
Claude-Session: https://claude.ai/code/session_01791wgvD8EqJaAeURYXYbT5
Signed-off-by: Thomas Kroll <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* matugen: support multiple injected palettes with settings ui
Run user-configured external commands on each theme build and merge their
JSON output into matugen's import data under a namespace, so templates can
reference e.g. {{mypalette.color0}} alongside dank16. Multiple palettes are
supported, each under its own namespace. Command output may be nested JSON
(pywal/wallust shape). A settings page manages the list; a legacy
single-object config file is still read.
* settings: map PaletteInjectTab in search index
* matugen: bound palette-inject command with timeout and WaitDelay
* fix palette column spacing (review suggested
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* remove wallpaper kind guard (review suggestion)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* fixup! remove wallpaper kind guard (review suggestion)
---------
Co-authored-by: bbedward <[email protected]>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* feat(keybinds): redesign cheatsheet with category navigation, M3 styling, and click-to-execute
- Add category sidebar with Material Symbols icons and badge counts
- Move search field to top of sidebar with Ctrl+F shortcut
- Implement 2-column masonry layout for keybind items
- Deduplicate duplicate actions and stack multi-key combos vertically
- Collapse consecutive workspace keybindings (1-9) into range entries
- Add media & hardware icons for volume, mic, brightness, and PrtSc keycaps
- Preserve Niri window filter properties and distinguish same-app Alt-Tab binds
- Enable click-to-execute for keybind actions across supported compositors
* fix(keybinds): preserve niri action properties and disable execution on range rows
* fix(keybinds): convert niri action properties to CLI flags and disable IPC execution for window switcher binds
* fix(keybinds): pass skip-confirmation as boolean switch in niri executeAction
* fix(keybinds): handle niri spawn-sh actions in executeAction
- A new minimal and fun island instance (dot) that gets out of your way to be tossed anywhere on your screen
- Dankisland freedom is here too! It can now also be used in anywhere mode and used horizontally or vertically as you see fit!
dnf lists the packages an upgrade obsoletes indented beneath it, under a
separate heading. Those rows are removals carrying their installed
version, so they were parsed as upgrades from a version to itself and
counted as pending. Skip indented rows. The heading is translated, so
matching it would only work in English.
* mux: add herdr backend
* mux: remove herdr service tests
* mux: include herdr in settings subtitle
* mux: add translator context to the multiplexers subtitle
* mux: drive the service from a backend table
Each backend (tmux, zellij, herdr) is one entry in MuxBackends.js with
its commands and parser, so the service no longer branches on muxType
in eight places and the settings dropdown lists the table keys. One
availability check runs for the selected backend and re-runs when the
backend changes. The herdr parser reads the real {sessions:[...]}
shape only.
dnf prints one row per (package, repo), so a package offered at the same
version by two enabled repos is listed twice and inflates the pending
count. Key the parse on (name.arch, version) and keep the first row.
* feat(bar): show edit window rule in focused app popout when rule exists
Check existing DMS-managed window rules when the focused window popout
opens. If a matching rule exists for the focused window, show
'Edit window rule' and open the rule in edit mode via showEdit().
Otherwise, keep showing 'Add window rule' for new rule creation.
* fix(bar): address PR review feedback for focused window rules
- Clear matchingRule up front in checkMatchingRule() to prevent showing previous window's rule while async query is in flight
- Reuse existing catalog term 'Edit Window Rule' to leverage existing translations
* i18n: pick the DankCommon translation for the chosen locale
updateLocale() switches the shell translation, but the DankCommon one was
picked once at startup from the system locale and never changed. With DMS set
to English on a German system, plugin strings that fall back to I18n.tr()
(Suspend, Reboot, ...) stayed German. _pickCommonTranslation() now uses the
resolved locale and runs again from useLocale(); for English, where no common
file exists, the table is cleared.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* i18n: clear the DankCommon table when falling back to English
_fallbackToEnglish() left the common table as it was picked for the previous
locale. Setting Language to "System default" on a system without a shell
translation (an English one, cs_CZ, ...) goes through that path, so the shell
switched to English while DankCommon strings stayed in the old language - the
same mismatch this branch fixes for the useLocale() path.
The function now resolves to "en" before re-picking, which clears the table
because no English common file exists, and it also covers the case where the
selected translation file is missing or fails to parse.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
- Standard bars respond to the global surface opacity again
- Island dash stays open when an edit-mode menu is used
- Weather forecast chip and island level groove follow the foreground opacity
- Control center tile icons follow blur and foreground settings like the slider icons
- Restored the layer outline on control center tiles and slider rows
- Hover popouts setting is visible for island bars
- Bumped dank-qml-common
saveSettings() set _selfWrite before setText(). When the serialized
settings were identical to the file, nothing was written, no
fileChanged followed, and the flag stayed set. The next external edit
of settings.json was then taken for our own write and never reloaded.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
them towards theme
controlCenter: make a real grid, add numerous more tile varieties, add
icon size option
surfaces: fix elevation levels in general, and shift them down
settings: restyles and re-org
sliders: scale down universally
- This adds native persistent slots per compositor such as Hyprland, Mango & Sway
- Not all compositors record preloading workspaces so those options return as minimum placeholders
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.
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.
* launcher: add vpn connections as a search provider
Adds a dms_vpn built-in launcher plugin that lists every configured VPN
profile and toggles it on enter. Connected rows show a lock icon and a
Connected badge, in-progress rows show Connecting, everything else shows
a crossed-out key.
Profiles come from the network state the shell already fetches at
startup, so there is no new timer, process or request. The provider
returns nothing when the backend is not NetworkManager.
It ships without a trigger. A trigger prefix replaces the whole result
set, so claiming "vpn" would have hidden every application whenever
someone typed it to find their VPN client. Matching on the vpn keyword
instead keeps apps and profiles in the same list.
Co-Authored-By: Claude Opus 5 <[email protected]>
* launcher: hide vpn profiles on the home view without a trigger
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* launcher: list vpn profiles when browsing the plugin
The empty query guard also applied when opening VPN from the Browse
section, so the plugin view came up empty. The plugin filter path now
opts out of the guard, while the home view still hides the profiles.
Co-Authored-By: Claude Opus 5 <[email protected]>
* launcher: stop matching vpn profiles on connect and disconnect
Keywords match by substring, so "connect" also matched "disconnect" and
listed connected profiles too. Picking one of those disconnected it.
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Turn on do not disturb while a screen share is active so notification
popups and their contents stay off the shared screen. The unread dot and
the existing dnd bypasses are unaffected.
Detection reuses the screen sharing privacy indicator: niri cast ipc,
otherwise pipewire and xdg-desktop-portal. Neither source says when it
has finished reporting, so a share reading as gone is only acted on once
it stays gone for four seconds. Engaging stays immediate, and turning the
setting off releases right away since that is a user action rather than a
detection guess.
Turning dnd off by hand during a share is remembered against the sources
that were sharing at the time, so it holds for that share and across a
shell reload. A share started after that one has ended gets a fresh
decision.
Co-authored-by: Claude Opus 5 <[email protected]>
* feat(display): per-output ICC color profiles and color temperature
Rebuild of #3388 on current master: the previous branch was cut from an
older base and reverted unrelated fixes in the gamma stack and the display
settings UI.
ICC profiles:
- core/internal/icc: ICC v2/v4 parser (TRC, LUT8/16 and parametric curves,
vcgt, description/version/color space) plus gamma ramp generation, with
synthetic coverage and optional vendor-profile tests (ICC_TEST_DIR).
- wayland manager: load iccProfiles from the compositor's DMS config dir,
build the ramp from the profile when one is set (falling back to the
temperature ramp when it is missing or unparsable), and re-apply on
output hotplug and after resume.
- ICC ramps apply while the night light schedule is disabled; outputs
without a profile keep an identity ramp.
- desc/mluc offsets are validated before slicing, so a malformed profile
returns an error instead of panicking the daemon.
Per-output temperature:
- outputTemps in the same config; SetOutputTemp accepts 1000-10000K and the
Display Config slider exposes 3000-10000K, which matters for displays
calibrated at a higher white point (e.g. a 7000K profile).
Gamma control lifecycle:
- needsControls() now keeps the controls alive for configured ICC profiles
and per-output temperatures, so toggling the night light off no longer
destroys the controls and drops the ramps.
- Re-apply paths clear the per-output dedup state instead of adding a
manager-level guard, which would have suppressed contrast-only writes and
the post-resume forced resend (#1235), and left a recreated control
without a ramp.
- Monitor sleep/disconnect still re-establishes controls when an output
comes back.
IPC / CLI / UI:
- wayland.icc.{getStatus,apply,remove,listOutputs,setTemp,getTemps}
- dms icc list | info <file> | apply <output> <file> | remove <output> | status
- Display Config output card gains a Color Profile row (browse/apply/remove
with description, version, color space and active state) and a per-output
color temperature slider, backed by Services/ICCService.qml.
Config path:
- DMSConfigDir() follows the compositor layout (niri/dms, hypr/dms,
mango/dms) via the shared compositor detection and utils.XDGConfigHome(),
replacing the hardcoded niri path in both the manager and `dms icc`.
Tests: TZ=UTC go test ./... (60 packages ok), including red/green coverage
for the icc parser bounds checks and needsControls().
* feat(icc): add `dms icc set-temp` and show per-output temps in `status`
The per-output temperature override was reachable from the Display Config
slider and from `wayland.icc.setTemp` over IPC, but not from the CLI.
- `dms icc set-temp <output> <kelvin>` (alias `setTemp`) sets a per-output
override, 1000-10000K, and 0 clears it so the output follows the night
light schedule again. Range checking matches the daemon so an invalid
value fails in the CLI instead of over IPC.
- `dms icc status` gains a Temp column (the override in K, or `schedule`),
so an override can be verified without opening the settings UI.
Verified against a running daemon: `dms icc status` reports 7000K for the
three overridden outputs and `schedule` for the remaining one.
* feat(icc): treat the per-output temperature as the profile's reference white point
The per-output temperature was documented as the white point a profile was
produced at, but an output with a profile ignored it entirely: the ramp was
generated from the profile and the temperature only applied to outputs
without one (or as the fallback when a profile failed to parse). A display
calibrated at 7000K therefore got the profile ramp, and enabling the night
light had no effect on it at all.
- `ProfileRampWithTemp` composes the profile ramp with the ratio between the
target temperature ramp and the reference ramp, so the temperature is the
white point the profile describes and the night light shifts relative to
it.
- `applyGamma` passes the night light temperature as the target, and
`noTempTarget` when the schedule is disabled, which leaves profiled
outputs at their reference white point and drives plain outputs with the
neutral ramp.
- The re-apply sentinel stays 0, so it cannot collide with an applied
temperature.
Covered by TestProfileRampWithTemp (reference == target is a no-op, warmer
and cooler targets move the expected channels).
Also: `dms icc set-temp` help text describes the reference-white-point
semantics.
* fix(icc): load the configured profiles even when controls already exist
Restoring the upstream "output returned, re-establishing controls" branch
made the registry handler establish the gamma controls before the startup
post runs, and that post returned early on `m.controlsInitialized`, so the
configured ICC profiles and per-output temperatures were never attached:
every output reported "(none)" and the night light ran with defaults until
the user re-applied a profile by hand.
- `initializeControlsAndICC` (the startup post) loads the configuration
first and only creates the controls when they are missing.
- The loading is split into `loadConfiguredICC` / `applyConfiguredICCForOutput`
so it can also run per output.
- A hotplugged output now gets its configured profile and temperature as soon
as its name is known (name handler) or when its control is created, which
also makes the "re-applies on hotplug" claim in the description true.
- After attaching, the output's dedup state is cleared so the ramp is written.
Covered by TestManager_LoadConfiguredICCWhenControlsAlreadyExist and
TestManager_AttachConfiguredICCForNamedOutput.
* fix(icc): make the per-output temperature an actual target
The per-output value was treated as the white point a profile was produced
at, which meant a display with a profile never changed when the user set a
temperature: the profile ramp was applied as measured and the value only
mattered when a night light target existed. Per-display temperatures were
therefore unusable on profiled outputs while the night light was disabled,
which is the normal state for users who want each monitor at its own value.
- `effectiveTempTarget` decides per output: a per-output override wins over
the night light schedule, the schedule applies when there is no override,
and `noTempTarget` means neither. `0` is "no override", not 0K.
- `applyGamma` composes the override on top of the profile ramp, treating the
profile as measured at 6500K (D65), and drives outputs without a profile
with the same target so both paths agree.
- `dms icc set-temp` help text and the Display Config slider description go
back to describing an independent per-output temperature.
Covered by TestEffectiveTempTarget (override wins, schedule fallback, none)
and the updated TestProfileRampWithTemp (7000K cools, 5000K warms relative to
the 6500K reference).
* feat(icc): show the profile details in Display Config
The profile row only showed the description, so there was no way to tell
which profile a display actually has applied (whitepoint, curve type, file
provenance) without leaving the settings page.
- `ICCStatus` carries the descriptive metadata: class, tone-curve kind (plus
gamma or table size), vcgt channels/entries, white point chromaticity with
a derived CCT and standard illuminant name, file size and mtime.
- `internal/icc` gains `WhitePointXY`, `WhitePointCCT` (McCamy),
`WhitePointName` (D50/D65) and `TRCKind`, all covered by tests.
- Display Config's Color Profile row gets an info button that opens
`ICCProfileInfoModal` with those fields and the profile path, alongside
the existing browse/remove actions.
- `dms icc info` reports the white point as name/xy/CCT instead of raw XYZ.
* fix(icc): keep the daemon, the state snapshot and the temp slider honest
A malformed profile could still take the daemon down, the ICC getters ran off
the wayland actor goroutine, the pushed state hid a removal, and the
per-output temperature slider dropped wheel changes.
- `internal/icc`: the tag table was sized with `uint32` arithmetic, so
`tagCount*12` wrapped (`0x15555556*12 == 8`) and a 140-byte profile passed
the bounds check, allocated ~4.3 GB and then panicked. `ParseFile` runs on
the wayland actor goroutine, which has no `recover()`, so a bad profile
persisted in `wayland.json` took the daemon down at startup. The count is now
compared against the bytes that are actually left before anything is sized
with it.
- The manager publishes an ICC snapshot (`iccStatus`/`iccTemps`) whenever the
actor changes an output's profile or temperature, and `GetICCStatus`/
`GetOutputTemps` serve that snapshot instead of ranging `m.outputs` and
reading fields the actor owns. That removes the data race reported by
`go test -race` for `dms icc status` running against a hotplug, and keeps the
per-profile `os.Stat` off the night light animation path, where it ran on
every `updateStateFromSchedule` (including each 25 K step).
- `State.ICCProfiles`/`OutputTemps` drop `omitempty`, so removing the last
profile pushes `{}` instead of omitting the key: the QML truthiness guard
then clears `ICCService.status` instead of leaving the removed profile on
screen with a green "active" dot.
- `removeOutputByRegistryName` (extracted from the registry remove handler)
deletes the `outputNames`/`outputRegNames` entries along with the output, so
a disconnected monitor stops being listed by `dms icc status`/`listOutputs`,
and a rebound `wl_output` reusing the released object ID cannot attach the
previous monitor's profile from the stale name.
- The per-output temperature slider sets `wheelEnabled: false` like the other
drag-finished sliders: `DankSlider`'s wheel handler assigns `value` and emits
`sliderValueChanged` without `sliderDragFinished`, so scrolling dropped the
change, froze the label on the scrolled value and destroyed the value binding.
Covered by TestParseBytesTagTableBounds (allocates 4,299,161,600 bytes at
parser.go:138 and dies with an unrecovered fatal error on the previous
commit), TestManager_ICCStatusServesPublishedSnapshot,
TestManager_RemoveOutputByRegistryNamePrunesNames and
TestManager_ICCStatusConcurrentPublishAndRead; a temporary copy of the getter
test against the previous commit reports DATA RACE and passes here.
* fix(icc): keep the attached profile when a gamma control is re-created
`setupOutputControls` stored a fresh `outputState` for every output it touched,
and `createControls` calls it whenever the controls are rebuilt, so recreating a
control dropped the output's `iccPath`/`iccProfile`/`outputTemp` — configured
state that does not belong to the control. The path is dormant today because
`destroyControls` only runs when nothing is configured (`needsControls()` covers
`ICCProfiles`/`OutputTemps`), but the snapshot published after `createControls`
would then report whatever the rebuilt state holds instead of the profile the
display is configured with.
`controlStateFor` reuses the state of a known output and resets only what belongs
to the new control: the ramp bookkeeping and the failure budget (rampSize,
failed, retryCount, lastFailTime, lastTemp, lastGamma, lastContrast). A control
that has not reported gamma_size yet has to receive its ramp again, while the
profile and the per-output temperature survive. An output seen for the first
time still starts from a clean state.
Covered by TestManager_ControlStateReuseKeepsAttachedICC.
* fix(icc): keep the config a real snapshot and make the per-output temperature apply
Third review round.
- `Config` is copied by value all over the manager, but `ICCProfiles` and
`OutputTemps` are maps, so every copy aliased the ones the manager writes under
`configMutex`. `updateStateFromSchedule` stored such a copy in `m.state` and it
was marshalled on the connection writer goroutine while `ApplyICC` stored a
profile from the IPC goroutine (`fatal error: concurrent map iteration and map
write`, which no `recover()` catches); `SaveConfig` marshalled its copy after
the unlock, and `loadConfiguredICC`/`attachConfiguredICC` indexed the maps after
releasing the lock. Every copy that outlives the lock is cloned now
(`cloneConfig`/`configSnapshot`), so a value copy is a snapshot again.
- `SetOutputTemp` never created the gamma controls. On a default install (night
light off, gamma 1.0, contrast 1.0, no profile, no override) they do not exist,
so the `Range` over `m.outputs` matched nothing: the value was written to
`wayland.json`, no ramp was applied, and the settings slider snapped back to
"Default" until a daemon restart. It now persists first, creates the controls
through `ensureOutputControls` (shared with `ApplyICC`), and pushes the state
from the actor, and the published temperatures report the configured value even
before an output is known.
- A TRC is not a video card gamma ramp: `rTRC`/`gTRC`/`bTRC` describe the
display's own transfer function, so a gamma-2.2 profile was written as
`t^(1/2.2)` — mid gray 0.5 became 47909/65535 — washing the display out instead
of correcting it. `ApplyICC` refuses a profile without `vcgt`, the attach path
skips one with a warning, and `GenerateGammaRamp` returns an error instead of
falling back to the TRC or to an identity ramp. TRC stays profile metadata
(`dms icc info`, the settings dialog).
- `icc: applied ICC ramp` was logged at Info on every 25 K animation step; it is
Debug now, which is where a per-step message belongs.
- `I18n.tr("No information available")` in the profile info modal passes its
translator context, like every other string this PR adds.
Covered by TestManager_ConfigSnapshotOwnsItsMaps,
TestManager_ConfigSnapshotConcurrentMarshal (the previous commit dies with
`fatal error: concurrent map iteration and map write`),
TestManager_SetOutputTempWithoutControlsPublishesValue (the previous commit
reports 0 for the value it just stored), TestGenerateGammaRampRequiresVCGT, and
the updated TestProfileRampWithTemp, whose fixture now carries an identity vcgt
table instead of relying on an empty profile.
* fix(icc): push the state when only the ICC part changed
Found while sweeping the same class of problem: `stateChanged` compared only the
schedule fields, so a change that touches nothing else — applying or removing a
profile, `dms icc set-temp` — was treated as "no change" and the notifier never
pushed it. The settings page then only caught up because the UI path calls
`requestStatus()` afterwards, and the CLI path did not catch up at all, which is
the symptom the omitempty fix was meant to remove.
`iccStateChanged` compares the ICC profile status and the per-output
temperatures, so those changes reach the subscribers on their own. Covered by
the new TestStateChanged cases (profile applied, profile removed, profile
description changed, output temp changed, unchanged ICC state); the four
change cases report `stateChanged() = false` on the previous commit.
* fix(icc): keep the night light state, prune names without a control, use a 0 sentinel
Fifth review round.
- `SaveICCConfig` replaces `SaveConfig` on the three ICC paths (apply, remove,
set-temp). Writing the daemon's whole config persisted the night light fields
it happens to hold in memory: with the night light on, applying a profile
stored `"Enabled": true`, and since the shell only ever sends
`wayland.gamma.setEnabled` when the mode is on, turning it off afterwards (an
in-memory change on the daemon side, no write) left that `true` on disk — after
log out/in the daemon came up driving the schedule while the UI showed night
mode off, with no way to resync short of a toggle. The ICC save now reads the
file and rewrites only `iccProfiles`/`outputTemps`, so the fields the shell
owns stay as they are.
- `removeOutputByRegistryName` resolved the output from `m.outputs`, which only
has entries once the gamma controls exist. On a default install (night light
off, no profile, no override) no control is ever created, so a disconnected
monitor kept its `outputNames`/`outputRegNames` entries for the life of the
daemon: `dms icc status`/`listOutputs` listed monitors that were gone, and both
maps grew with every dock/undock cycle. The object ID is resolved from
`m.outputRegNames` (populated for every `wl_output`), the name entries are
dropped before the output state is looked up, and a state that is known without
a registry name still falls back to matching its own.
- The Display Config temperature row used `7000` as its fallback for an output
with no override. `publishICCState` only copies entries with `temp != 0`, so
the value is absent rather than `0` for an unconfigured output: the card claimed
"7000K" while the daemon drove the night light or neutral, and a user who
deliberately set 7000K got the "no override" colouring because the highlight
tested `!== 7000`. The sentinel is `0` now, which is what the `Default` label
branches and the "is overridden" tests were written against.
Covered by TestSaveICCConfigKeepsNightLightFields (the previous commit brings the
night light back on after a restart), TestManager_RemoveOutputByRegistryNameWithoutControl
(the previous commit keeps an unplugged monitor listed) and the existing
TestManager_RemoveOutputByRegistryNamePrunesNames.
* fix(icc): validate the CLI arguments and the IPC parameters of the ICC commands
Sweep of the two surfaces the review has not looked at yet, so the failures are
found here instead of in the next round.
- `dms icc apply` copied the profile into the config directory before asking the
daemon, and only the *parse* was checked locally. A profile the daemon refuses
(no `vcgt` table, non-RGB color space) was therefore left in the profile
directory for `dms icc list` to keep offering, and an IPC failure left the copy
behind as well. The command now checks the same rules the daemon enforces
before copying anything, and removes the copy it just made when the request
fails (a file that was already there is left alone).
- `dms icc list` marks a profile without a `vcgt` table, so the rule that makes
`apply` refuse it is visible where the profiles are listed.
- `ApplyICC`/`RemoveICC`/`SetOutputTemp` reach the manager over IPC with whatever
the caller sent: an empty (or whitespace-padded) output name used to be
persisted as an entry in `wayland.json` that no `wl_output` can ever match, and
a relative profile path was stored to be resolved again on the next start, from
a different working directory. Both are rejected now, before anything is
written.
Verified with the built CLI against the previous binary: with a profile that has
no vcgt table, `dms icc apply` now exits before touching the config directory
(`residual=[]`) where the previous one copied the file and went on to the IPC
(`residual=[nogamma.icc]`); with a valid profile and no daemon reachable, the
previous one leaves `vcgt.icc` behind and this one does not. Covered by
TestManager_RejectsInvalidICCArguments.
* fix(icc): apply the user gamma and contrast on profiled outputs
* fix(icc): recreate failed controls without the night light and allow clearing a per-output temp
* test(icc): cover the recreated control and the cleared temperature override
Follow-up on d55d3d1f, which fixed both points without tests:
- TestManager_RecreateOutputControlFollowsNeedsControls pins the predicate: with
the night light off and nothing configured there is nothing to recreate, and
with a configured profile or a per-output temperature the control has to be
recreated. Both configured cases return nil on 9226251c.
- TestManager_SetOutputTempZeroClearsTheOverride pins what the reset button in
Display Config does end to end: the stored value and the published entry both
disappear, so the row reads "Default" and the output follows the schedule.
Also keeps the reset button's slot instead of revealing it on demand. The
temperature row shares its column with the slider below it, so the height change
between "Default" (no button) and an override moved the slider 14 px under the
pointer while it was being dragged; the button is now always laid out and only
its opacity and enabled state follow the override, which also lets the label
width drop the conditional.
* fix(icc): replicate a one-channel vcgt and keep the temp slider in sync
* fix(icc): release idle gamma controls and push the output list
* fix(icc): dedupe ramps on the effective target
* fix(icc): never overwrite a same-named profile in the config dir
---------
Co-authored-by: bbedward <[email protected]>
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
* 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.
* 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
* Island: Added option to choose the control center icon to be either
Battery gauge or Connectivity
* Add Control Center to settings search index
* island: keep connectivity dim opacity off the blink target
DankBlink animates opacity and forces it back to 1.0 when it stops,
which broke the icon's binding and left the disabled state fully opaque.
Adds Fortinet gateway support to the NetworkManager openconnect path.
A SAML gateway authenticates through the browser, with the loopback
redirect handled in core and the session traded for the cookie
openconnect needs. Certificate trust is confirmed once and stored as
the digest openfortivpn uses.
openfortivpn config files can be imported as profiles. A saml-login
line selects the browser login, anything else imports as a password
profile carrying whatever credentials the file had.
Co-authored-by: Claude Opus 5 <[email protected]>
When a backend runs the upgrade in a terminal (paru/yay or a custom
command), the popout unconditionally reopened itself once the terminal
closed. Add an updaterReopenAfterUpgrade setting (default: on, keeping
current behavior) so users can opt out and keep the popout closed after
the terminal exits.
* idle inhibitor: add duration presets and timed expiry
Right-click the bar widget or expand the Control Center pill to keep the
system awake for a set duration rather than only indefinitely. The deadline
is persisted so it survives a shell restart, and is re-checked on resume
because timers stop across suspend.
Adds `dms ipc call inhibit enableFor <minutes>`, and `inhibit status` now
reports the remaining time.
* idle inhibitor: add translator context to new duration strings
* refactor(systemtray): convert overflow menu and context menu to DankPopout
* fix(systemtray): correct window-local anchor position mapping and enable contentHandlesKeys for tray menu keyboard navigation
* fix(systemtray): force active focus on tray menu content
* fix(systemtray): address inline PR review feedback
- Force active focus on open for key navigation
- Unregister menu only if matching active instance
- Map anchor trigger position relative to screen origin
* fix(systemtray): gate onMenuOpenChanged on useOverflowPopup
- Prevent registering overflowPopout in inline expansion mode
* fix(systemtray): correct context menu positioning in overflow popout
- Handle screen global coordinate mapping for both connected and standalone popout modes
* fix(systemtray): handle per-screen backend and vertical bar anchor Y offset
- Use overflowPopout.useConnectedBackend for per-screen discriminator
- Add vertical anchor Y centering offset in showForTrayItem
* fix(systemtray): center overflow popout on vertical bar caret
- Add vertical orientation anchor Y offset in _openOverflowAt
* fix(systemtray): clear submenu entryStack on tray menu open
- Expose entryStack alias on trayMenuContentRoot
- Clear entryStack on trayMenuPopout onOpened to avoid submenu state leakage
The cycle fallback fired from a never-cycled state. With one display
enabled and another marked off in settings, a DPMS standby that drops
the DisplayPort link looked like an unplug, and DMS sent an On to the
disabled display over niri IPC. It stayed on until the next config
reload and the workspaces round tripped through it.
Track each output's enabled state the first time a cycle touches it,
drop the entry once it is back at that state, and only rescue outputs
that were on before the cycle disabled them. Outputs that were already
off are never enabled automatically.
* 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]>
* 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
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
* 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
* 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]>
* 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.
* 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.
* 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.
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.
* 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
* 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.
* 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.
* 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
- 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#2360Closes#3246
- 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
* 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.
* 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>
* 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.
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.
* 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]>
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.
* 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
* 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>
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]>
* 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]>
* 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
* 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]>
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
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.
* 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]>
* 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>
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
* 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
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.
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]>
* 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]>
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.
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.
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.
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
* 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]>
* 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]>
* 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]>
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.
**Common widgets from `qs.DCommon.Widgets`:**`StyledText`, `StyledRect`, `DIcon`, `DButton`, `DToggle`, `DTextField`, `DSlider`, `DGridView`, `CachingImage`. The old `Dank*` names in `qs.Widgets` are deprecated.
See [theme-reference.md](references/theme-reference.md) for the complete property list.
**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
4. Enable your plugin and add it to the DankBar layout, a dock, or the dash (tabs show up on their own; add cards from the dash edit mode)
**Runtime plugin discovery via IPC:**
@@ -557,6 +653,10 @@ Plugin IDs are validated against `^[a-zA-Z0-9_\-:]{1,64}$`.
11.**Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`
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
14.**Using `"type": "widget"` for a dash tab** - Dash surfaces need `"type": "dash"` / `"dashCard"` or a `components` key
15.**Timers in dash tabs or cards that ignore `live`** - The dash content outlives the popout; gate on `live`
16.**Reading `dash.options` from a bar widget** - `options` only reach dash surfaces; share values through `settings` and `pluginData`
17.**Raw theme colors in a dash card** - Use `accentColor` / `contentColor` / `mutedColor` so the card follows its tone
importqs.Modules.DankDash.Overview// Card, for tiles inside a tab
```
**For clipboard/exec:**`import Quickshell`
**For processes:**`import Quickshell.Io`
**For networking:**`import Quickshell.Networking`
@@ -598,7 +708,8 @@ import qs.Common
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
- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar and dock pills, attached dock panels, popouts, click actions, CC integration
The dash popout (`dms ipc call dash open`) holds tabs and an overview grid of cards. A plugin can add a tab (`dash` surface), a card (`dashCard` surface), or both. Built-in tabs and cards use the same registry (`quickshell/Modules/DankDash/DashRegistry.qml`).
Requires DMS 1.7 or later: set `"requires_dms": ">=1.7.0"`.
## Picking the manifest form
| Plugin provides | Manifest |
|-----------------|----------|
| Tab only | `"type": "dash"`, `"component": "./Tab.qml"` |
| Card only | `"type": "dashCard"`, `"component": "./Card.qml"` |
| Dash plus bar/dock widget, daemon, ... | `"type": "composite"` with every surface in `components` |
Add the optional `dash` block for labels, card size and options (see plugin-manifest-reference.md, Dash Block).
## Rules that trip people up
- Only enabled plugins appear in the dash. There is no "disabled" placeholder.
- A plugin tab shows in the tab bar as soon as the plugin loads. Cards are never placed automatically; users add them in edit mode (three-dot menu, Edit, Add widget).
- Tab id is `plugin_<pluginId>`. Use it with `dms ipc call dash open plugin_<pluginId>` and `popoutService.toggleDankDash("plugin_" + pluginId, x, y, width, section, screen)`.
- A tab is created when it becomes current and destroyed when the user leaves it. The dash content stays alive after the popout closes, so gate every Timer, animation and service ref on `live`.
- Widths are grid columns (user picks 3 to 8, default 6). Rows are 96 px (`DashMetrics.gridRowUnit`). Cards must lay out for every size in `minW..maxW` x `minH..maxH`.
-`dash.options` values reach only the dash surfaces as `options.<key>`. A bar widget or daemon in the same plugin cannot read them; use a `settings` component and `pluginData` for values every surface needs.
-`widgets` is a reserved option key.
- Use `I18n.trFor("<plugin id literal>", "...")` for every string.
The grid is 4 columns (2 when narrower than `Theme.smallBreakpoint`). `enabled: false` on a definition leaves it in the Add menu. The layout is saved in `dashOptions[entryId].widgets`.
Clicks: when the plugin also has a tab, a click on a `clickable` card opens it. Set `opensTab: false` to handle the click yourself. Non-clickable cards are skipped by keyboard navigation unless `focusTarget` points at a child control.
Use the card colors, never raw `Theme.primary` or `Theme.surfaceText`, so tones work.
## Testing
```bash
dms ipc call plugins reload myPlugin
dms ipc call dash open plugin_myPlugin
```
Working examples in `quickshell/PLUGINS/`: `DashTabExample` (standalone tab), `DashCardExample` (standalone card), `DashCounterExample` (tab + card + bar/dock widget).
@@ -174,7 +174,7 @@ Retrieve a plugin's installation directory at runtime:
vardir=pluginService.getPluginPath(pluginId)
```
Returns the absolute path to the plugin's directory (e.g., `~/.config/DankMaterialShell/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.
Returns the absolute path to the plugin's directory (e.g., `~/.config/dms/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.
| `author` | string | Creator name or email | Non-empty |
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `composite` |
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`,`dash`, `dashCard`,`composite` |
| `capabilities` | array | Plugin capabilities | At least 1 string item |
One of `component` or `components` is required (not both):
@@ -17,7 +17,7 @@ One of `component` or `components` is required (not both):
| Field | Type | Description | Validation |
|-------|------|-------------|------------|
| `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` |
| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher` |
| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard` |
## Conditional Requirements
@@ -38,6 +38,7 @@ One of `component` or `components` is required (not both):
| `requires` | array | Deprecated alias for `dependencies` |
| `dash` | object | Dash tab label, overview card size range and dash options. See Dash Block below. |
## Permissions
@@ -59,6 +60,8 @@ Capabilities are free-form strings that describe what the plugin does. Common va
-`monitoring` - system/service monitoring
-`launcher` - launcher search provider
-`desktop-widget` - desktop background widget
-`dash-tab` - dash tab
-`dash-card` - dash overview card
-`ai` - AI/LLM integration
-`slideout` - uses slideout panel
@@ -119,7 +122,68 @@ The `components` field maps surface names to QML paths, allowing a single plugin
}
```
Valid surface keys: `widget`, `desktop`, `daemon`, `launcher`. Provide any subset. Each surface is loaded independently in the appropriate registry.
Valid surface keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard`. Provide any subset. Each surface is loaded independently in the appropriate registry.
## Dash Block
Plugins with a `dash` or `dashCard` surface can describe them in a `dash` object. Everything is optional.
| `icon`, `title` | Tab icon and label. Fall back to the top-level `icon` and `name` |
| `card.title`, `card.icon` | Card name and icon in the Add widget menu. Fall back to the tab label and icon |
| `card.w`, `card.h` | Default size in grid cells. Columns are 3 to 8 (default 6), rows are 96 px |
| `card.minW` .. `card.maxH` | Resize range. Widths clamp to the user's column count; heights have no fixed limit |
| `options` | Rows in the dash options sheet and Settings > Dashboard. Read as `options.<key>` in the tab and card. Types: `toggle`, `choice`, `number`. `widgets` is a reserved key |
## Dash Examples
Standalone tab and standalone card use the single file form:
```json
{
"id":"dashTabExample",
"name":"Stopwatch Example",
"description":"A standalone dash tab",
"version":"1.0.0",
"author":"Developer Name",
"type":"dash",
"capabilities":["dash-tab"],
"component":"./StopwatchTab.qml",
"dash":{"title":"Stopwatch"},
"requires_dms":">=1.7.0"
}
```
```json
{
"id":"dashCardExample",
"name":"Progress Example",
"description":"A standalone overview card",
"version":"1.0.0",
"author":"Developer Name",
"type":"dashCard",
"capabilities":["dash-card"],
"component":"./ProgressCard.qml",
"dash":{"card":{"w":2,"h":1,"maxW":4,"maxH":2}},
"requires_dms":">=1.7.0"
}
```
A tab plus a card, or dash surfaces plus a bar widget, use `components` with `"type": "composite"`.
| `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). |
@@ -241,16 +241,49 @@ PluginComponent {
**Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically.
## Docks
Users add any loaded widget plugin to a dock from the dock's edit mode or Settings > Dock > Apps & widgets. Nothing in the manifest is needed.
- Define both `horizontalBarPill` and `verticalBarPill`; docks sit on any edge.
- Size icons with `root.iconSize` and text with `Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)`. The dock hands its own thickness and scale in those properties.
- Gate timers and animations on `surfaceLive` (host visible and `visibilityCommand` passing). Docks auto-hide.
- Don't assume one instance. Use `widgetInstanceId` if an instance needs its own state.
Optional attached panel: set `attachedContent: Component { ... }`. When the dock's "Open widgets" setting is Inline (`surfaceContext.inlineExpansion`), a click grows the dock and shows the panel next to the apps. With Popout (the default) the normal `popoutContent` opens instead; a widget with no popout still expands inline. Bars always use `popoutContent`. A `pillClickAction` always wins over both.
```qml
PluginComponent{
id: root
attachedContent:Component{
Column{
padding:Theme.spacingM
spacing:Theme.spacingM
DButton{
text:I18n.trFor("myPlugin","Close")
onClicked:root.surfaceContext.dismissExpansion()
}
}
}
}
```
The panel spans the dock's length and grows up to 320 px away from the edge (at most half the screen). The plugin cannot size it. `attachedActive` is true while this widget owns the panel. Escape closes it. See `quickshell/PLUGINS/AttachedPanelExample` and `quickshell/PLUGINS/SURFACE_WIDGETS.md`.
## Popout Namespace
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
```qml
PluginComponent{
layerNamespacePlugin:true
layerNamespacePlugin:"my-plugin"
}
```
The shell prefixes it, so the popout's namespace becomes `dms:plugins:my-plugin`.
## Reading Plugin Data
Access saved settings reactively via the injected `pluginData`:
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 and the base branch is fetched as `origin/${{ steps.pr.outputs.base }}`. Use `gh pr view ${{ github.event.issue.number }}` for title and description.
Get the diff from git, never `gh pr diff` (GitHub refuses diffs over 20000 lines): `git diff --stat origin/${{ steps.pr.outputs.base }}...HEAD`, then `git diff origin/${{ steps.pr.outputs.base }}...HEAD -- <path>` per file or directory. Read surrounding code as needed. Split a large PR across Task agents by area.
Steps:
1. Read CONTRIBUTING.md.
2. Read the PR title, description and diff.
3. Check existing comments so you don't repeat them: `gh api --method GET repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments` and `gh pr view ${{ github.event.issue.number }} --comments`. Skip findings already covered by a comment that still applies.
4. Verify each candidate finding against the code. Drop anything you can't confirm.
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-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 D* wrappers (DFlickable, DListView, DGridView).
- Deprecated shims: added code that uses a deprecated type or imports a deprecated module. Every shim calls `Deprecation.type(` or `Deprecation.module(`; list them with `git grep -h -e "Deprecation.type(" -e "Deprecation.module(" origin/${{ steps.pr.outputs.base }} -- quickshell`, which gives the old and new name. Grep the base, not HEAD, since a PR branched before the rename has no shims. Flag it even when the rest of the file still uses old names on the PR's base; suggest the new name and a rebase if the base predates it.
- CONTRIBUTING.md rules: user-facing strings in `I18n.tr()` with translator context, reuse existing terms from quickshell/translations/en.json; en.json and template.json must not be touched in a PR; mocks under core/internal/mocks/ are regenerated not hand-edited; new dux-qml-common contract properties need the matching library change.
Don't flag style or naming (deprecated shims are not naming), pre-existing issues, anything a linter or compiler catches, speculative input-dependent problems, missing tests or docs, or refactor ideas. Not sure it's real: don't post it.
Review only. Never edit files, commit, push, approve or request changes. Do not build, vet, format or run tests; CI already does that and the tools are not available here. Post each finding once. Keep comments short.
claude_args:|
--model claude-opus-5-5
--max-turns 120
--allowedTools "Read,Grep,Glob,LS,Task,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr comment:*),Bash(gh api --method GET repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*),Bash(git merge-base:*),Bash(git grep:*)"
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'
DMS is an open-source desktop shell for Wayland compositors on linux, built to work with many compositors.
## Repo Structure
- core/ - Go. `cmd/dms` runs quickshell, CLI utilities, and the unix socket server the UI depends on. `cmd/dankinstall` is a separate installer TUI.
- quickshell/ - the entire UI: Common/ (singletons, Theme, settings), Services/ (system and core access), Modules/ (bar, dock, lock, ...), Modals/, Widgets/ (shell-specific widgets and deprecated Dank* shims), PLUGINS/, translations/
- dux-qml-common/ - submodule of shared QML widgets
- docs/, distro/ - IPC and theme docs, packaging
## Rules
- Keep it simple.
- Do not edit code in response to a question.
- Never commit, push, or open a PR unless asked.
- Point out glaring issues we missed.
- The shell runs 24/7. Audit every change for idle CPU, extra processes, timers, and retained memory.
-`core` and `quickshell` share the socket protocol. Changing one means checking the other.
- QML uses Theme tokens, never hardcoded colors, spacing, or constants.
- Use the D* widgets from qs.DCommon.Widgets (DListView, DFlickable, and so on) instead of raw ListView/Flickable/ScrollView. The Dank* names in Widgets/ are deprecated shims.
- User-facing text goes through I18n.tr(). Reuse catalog terms. Never edit translation catalogs.
- Guard clauses, early returns.
- Comment only what the code cannot say: a non-obvious why, a trap, a workaround. Never narrate what the code does or restate a name. Keep it to a line or two, and delete useless or paragraph comments you come across.
- Do not edit generated code. Mocks come from mockery.
- No library for a simple problem. Use a modern one for a complex, common problem.
## Tests
- A test must fail on the broken code. Otherwise do not write it.
- No useless tests: "main.go contains func main()", defaults, "it instantiates", behavior the change didn't touch.
- QML tests must be fast. Call logic directly, load the smallest component, no fixed delays or animation waits, few meaningful assertions over a matrix.
- Tests cover `.js` modules, functions of `Services/` and `Common/` singletons, and IPC handlers. Never load, slice or mock anything under `Modules/`, `Modals/` or `Widgets/`, and never spawn a compositor, display server or shell. Those tests only ever broke on UI tweaks.
## Connections vs bound properties
- In per-item or per-screen components (delegates, bar widgets, popout and modal hosts, OSDs), no `Connections` for `on<Prop>Changed` handlers.
- Source in the same file: put the handler on it.
- Source outside (singleton, loader item, delegate parent): bind to a typed readonly property, never `var`, and handle that.
- Keep `Connections` for plain signals, hand-emitted change signals (`SettingsData.barConfigsChanged()`), constant-pointer targets, reassignable targets, and per-frame `var` sources.
- A bound handler fires once at creation if the initial value differs from the type default, and never on a same-value write. Convert only idempotent bodies. `enabled:` conditions become a guard inside the handler.
- Singletons under Services/ and Common/ keep their `Connections`.
## Commit Messages
`area: short lowercase explanation`, for example `network: report wifi state from the associated adapter`. Description only if required, in simple language.
@@ -6,10 +6,10 @@ 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`:
Clone with submodules — the shared widget library ([dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common)) is vendored at `dux-qml-common/` and symlinked into `quickshell/DCommon`:
@@ -54,34 +54,29 @@ make run # dev build, then launch against the live quickshell/ tree
The UI config dir resolves in order: `-c <dir>`, `DMS_SHELL_DIR`, the dir a running instance is using, then the embedded UI. Each candidate must contain `shell.qml`. `make run` uses `-c $(pwd)/quickshell`, so QML edits hot-reload from the working tree.
The Go core depends on [dankgo](https://github.com/AvengeMedia/dankgo) for logging, XDG paths, the IPC transport, and the quickshell process lifecycle. To develop against a local dankgo checkout, create a gitignored `go.work` at the repo root:
The Go core depends on [dankgo](https://github.com/duxlabs-dev/dux-go) for logging, XDG paths, the IPC transport, and the quickshell process lifecycle. To develop against a local dankgo checkout, create a gitignored `go.work` at the repo root:
```
go 1.26.1
use (
./core
../dankgo
)
go work init ./core ../dankgo
```
## Shared widgets (dank-qml-common)
## Shared widgets (dux-qml-common)
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
Everything under `quickshell/DCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dux-qml-common` submodule. It is a normal git worktree:
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
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.
1. Edit files under `dux-qml-common/` (or through the `quickshell/DCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dux-qml-common`.
2. Commit and PR those changes in the `dux-qml-common` repo: `cd dux-qml-common && git switch -c my-change`, push, open the PR there.
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
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:
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dux-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so existing `qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` imports keep working for the shell and for plugins. The `Dank*` widget names are deprecated: each one logs a warning naming its replacement. New code imports `qs.DCommon.Widgets` and uses the `D*` names (`DButton`, `DIcon`, `DListView`, and so on).
## VSCode Setup
@@ -143,7 +138,9 @@ touch .qmlls.ini
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6**`qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`.
6.Make your changes, test, and open a pull request.
6.Run `make test-qml` for QML unit tests (`nix develop --command make test-qml` with Nix). These use Qt 6 `qmltestrunner` offscreen and do not require a running shell. Set `QMLTESTRUNNER=/path/to/qmltestrunner` if needed.
7. Make your changes, test, and open a pull request.
### I18n/Localization
@@ -159,7 +156,9 @@ Text {
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
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/DCommon/` are owned by the dux-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dux-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dux-qml-common/DCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dux-qml-common`-tagged terms from the DMS project.
### GO (`core` directory)
@@ -169,6 +168,33 @@ Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo bu
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.
## Generative AI
Using an LLM to help write code is fine. Submitting its output unread is not.
- You are responsible for every line you submit. You have read it, tested it, and can explain it in review.
- Say in the PR when a meaningful part of the code was AI generated.
- Write PR descriptions, issues, and comments yourself. No AI-written or AI-rewritten text. A few plain sentences in your own words beat a page of generated summary. PRs and issues with AI-written text get closed without review.
- You can use AI to troubleshoot, gather data, and information for new issues. The description should be written by a human.
- If you are not an english-native speaker you can use AI to translate your request or PR description
- Do not file issues or leave comments you have not verified yourself. Reports that do not reproduce get closed.
- PRs that read like unreviewed output, with narrating comments, invented APIs, or style that ignores the file they are in, get closed without review.
## Pull request
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
[>)](https://aur.archlinux.org/packages/dms-shell-git)
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
Dux Shell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure
This is a monorepo containing both the shell interface and the core backend services:
@@ -83,10 +85,10 @@ One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, o
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
**System Monitoring**
Real-time CPU, RAM, GPU metrics and temperatures built into the dms daemon (powered by [dgop](https://github.com/AvengeMedia/dgop)). Process list with search and management.
Real-time CPU, RAM, GPU metrics and temperatures built into the dms daemon (powered by [dgop](https://github.com/duxlabs-dev/dux-gop)). Process list with search and management.
**Powerful Launcher**
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
Spotlight-style search for applications, files ([dsearch](https://github.com/duxlabs-dev/dux-search)), emojis, running windows, calculator, and commands. Extensible with plugins.
**Control Center**
Unified interface for network, Bluetooth, audio devices, display settings, and night mode.
@@ -98,18 +100,18 @@ Notification center with grouping, rich text support, and keyboard navigation.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/duxlabs-dev/dux-greeter).
**Plugin System**
Extend functionality with the [plugin registry](https://plugins.danklinux.com). DMS keeps
`~/.config/DankMaterialShell/plugins.lock.json` synchronized with managed plugin installs and
Extend functionality with the [plugin registry](https://plugins.duxlabs.dev). DMS keeps
`~/.config/dms/plugins.lock.json` synchronized with managed plugin installs and
their exact Git commits, so the same plugins can be reproduced on another machine.
## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
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.
- [dank-greeter](https://github.com/duxlabs-dev/dux-greeter) - greetd login screen with the Dux Shell aesthetic. The Greeter tab in DMS settings is the front-end for it.
- [dankcalendar](https://github.com/duxlabs-dev/dux-calendar) - Local, Google, Microsoft, and CalDAV calendars for the Dux desktop.
- [dgop](https://github.com/duxlabs-dev/dux-gop) - System monitoring TUI and Go library; its library powers the process list and dashboard widgets inside the dms daemon.
- [dsearch](https://github.com/duxlabs-dev/dux-search) - Fast file search that powers file results in the launcher.
- [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
- [dankgo](https://github.com/duxlabs-dev/dux-go) - Common Go modules behind the single binary apps.
- [Reverier-Xu](https://github.com/Reverier-Xu) - [Ori-fcitx5](https://github.com/Reverier-Xu/Ori-fcitx5) inspiration for Fcitx5 theme
## Star History
[](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
[](https://star-history.dera.page/#duxlabs-dev/dux-shell&type=date&legend=top-left)
Go-based backend for DankMaterialShell providing system integration, IPC, and installation tools.
Go-based backend for Dux Shell providing system integration, IPC, and installation tools.
**See [root README](../README.md) for project overview and installation.**
@@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication.
-`dms brightness [list|set]` - Control display/monitor brightness
-`dms color pick` - Native color picker (see below)
-`dms update` - Update DMS and dependencies (disabled in distro packages)
-`dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
-`dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/duxlabs-dev/dux-greeter) binary
### Color Picker
@@ -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/duxlabs-dev/dux-go) `go-wayland-scanner`:
```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
Headless mode requires cached sudo credentials. Run `sudo -v` first:
Headless mode requires cached credentials or a passwordless rule for your privilege escalation tool (sudo, doas, or run0). With sudo, run `sudo -v` first:
| `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
@@ -174,7 +184,7 @@ sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t ki
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.
`dms-greeter` is disabled by default; use `--include-deps dms-greeter` to enable it.
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.
@@ -183,7 +193,7 @@ When no flags are provided, `dankinstall` launches the interactive TUI.
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 (`--include-deps`, `--exclude-deps`, `--replace-configs`, `--replace-configs-all`, `--yes`) are rejected in TUI mode.
- 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
@@ -215,6 +225,6 @@ Most packages available in standard repos. Minimal building required.
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
**Void Linux**
Uses XBPS with the DMS and DankLinux self-hosted repositories.
Uses XBPS with the DMS and Dux Labs self-hosted repositories.
See installer output for distribution-specific details during installation.
@@ -35,6 +36,7 @@ var resolveIncludeCmd = &cobra.Command{
"layout.lua",
"outputs.lua",
"cursor.lua",
"input.lua",
"windowrules.lua",
"cursor.kdl",
"layout.kdl",
@@ -42,6 +44,7 @@ var resolveIncludeCmd = &cobra.Command{
"binds.kdl",
"input.kdl",
"cursor.conf",
"input.conf",
"layout.conf",
"outputs.conf",
"binds.conf",
@@ -52,8 +55,32 @@ var resolveIncludeCmd = &cobra.Command{
Run:runResolveInclude,
}
varmangoMigrateMainbool
varmangoMigrateCmd=&cobra.Command{
Use:"mango-migrate",
Short:"Respell Mango config keys for the installed Mango",
Long:"Detects whether the installed Mango uses legacy or snake_case config keys, rewrites the DMS fragments in mango/dms to match, and prints the dialect on the last line. --main also rewrites config.conf after a backup.",
dmenuCmd.Flags().StringVarP(&dmenuActiveSpec,"active","a","","Mark rows active by index (e.g. \"1,3,7:11,-3:\")")
dmenuCmd.Flags().StringVarP(&dmenuUrgentSpec,"urgent","u","","Mark rows urgent by index (same spec syntax as --active)")
dmenuCmd.Flags().BoolVar(&dmenuOnlyMatch,"only-match",false,"Reject free text; only a listed item may be returned; also blocks Escape/click-outside cancel, forcing a real pick")
dmenuCmd.Flags().BoolVar(&dmenuNoCustom,"no-custom",false,"Reject free text; only a listed item may be returned (unlike --only-match, Escape/cancel still works normally)")
dmenuCmd.Flags().StringVar(&dmenuSelect,"select","","Pre-select the first row matching this string (or, with --dump, filter to matching rows)")
dmenuCmd.Flags().StringVar(&dmenuMesg,"mesg","","Extra message line shown alongside the list")
dmenuCmd.Flags().StringVar(&dmenuInput,"input","","Read items from this file instead of stdin")
dmenuCmd.Flags().BoolVar(&dmenuPassword,"password",false,"Mask typed input (cosmetic only, not a security boundary)")
dmenuCmd.Flags().BoolVar(&dmenuMarkupRows,"markup-rows",false,"Render each row's text as rich markup")
dmenuCmd.Flags().BoolVar(&dmenuSync,"sync",false,"Read all input to EOF before showing the picker (default: stream as items arrive)")
dmenuCmd.Flags().BoolVar(&dmenuNoRunIfEmpty,"no-run-if-empty",false,"If stdin is empty, exit 1 immediately with no UI shown")
dmenuCmd.Flags().StringVar(&dmenuFormat,"format","s","What to print on accept: s=text (default), i=0-based index, d=1-based index, q=shell-quoted, p=markup-stripped, f=typed filter text, F=quoted filter text")
dmenuCmd.Flags().BoolVar(&dmenuMultiSelect,"multi-select",false,"Allow selecting multiple rows; each is printed on its own line on accept")
dmenuCmd.Flags().BoolVar(&dmenuDump,"dump",false,"Apply --select against the item list and print the result immediately; no UI is ever shown")
dmenuCmd.Flags().StringArrayVar(&dmenuKeybindSpecs,"keybind",nil,"Bind a custom accept key (repeatable): N=keyspec, e.g. --keybind 1=ctrl+e (N is 1-19); exits 9+N (10-28) instead of 0 when accepted that way")
dmenuCmd.Flags().BoolVar(&dmenuPrintInfo,"print-info",false,"Also print an accepted row's info metadata (the per-row \\0info\\x1f... field, see the per-row metadata protocol) on a second line")
dmenuCmd.Flags().BoolVar(&dmenuDisableHist,"disable-history",false,"Don't add this session's search query to the launcher's persistent history (default: recorded, like other launcher modes)")
dmenuCmd.Flags().StringVar(&dmenuView,"view","","Override the result view mode for this invocation: list, grid, or tile (default: list)")
dmenuCmd.Flags().StringVar(&dmenuSize,"size","","Override the popup size preset for this invocation: 1-4, matching the Appearance settings size buttons (default: the shell's own launcher size setting)")
dmenuCmd.Flags().StringVar(&dmenuIcon,"icon","","Show a Material Symbols icon next to the prompt badge (default: no icon)")
return[]checkResult{{catEnvironment,"XDG_MENU_PREFIX",statusWarn,fmt.Sprintf("%s (menu file not found)",menuPrefix),fmt.Sprintf("Dolphin 'Open with…' dialog may be empty. Ensure /etc/xdg/menus/%sapplications.menu exists.",menuPrefix),doctorDocsURL+"#xdg-menu-prefix"}}
return[]checkResult{{catEnvironment,"XDG_MENU_PREFIX",statusWarn,fmt.Sprintf("%s (menu file not found)",menuPrefix),fmt.Sprintf("Dolphin 'Open with…' dialog may be empty. Ensure %sapplications.menu exists in ~/.config/menus or /etc/xdg/menus.",menuPrefix),doctorDocsURL+"#xdg-menu-prefix"}}
}
if_,err:=exec.LookPath("keditfiletype");err==nil{
return[]checkResult{{catEnvironment,"XDG_MENU_PREFIX",statusWarn,"Not set","Dolphin file associations and 'Open with…' dialog may be empty. Set XDG_MENU_PREFIX=plasma- in your compositor's environment block.",doctorDocsURL+"#xdg-menu-prefix"}}
return[]checkResult{{catEnvironment,"qtengine config",statusInfo,"Not found","Qt apps use qtengine's default palette until DMS generates a theme.",url}}
}
varcfgstruct{
Themestruct{
ColorSchemestring`json:"colorScheme"`
}`json:"theme"`
}
iferr==nil{
err=json.Unmarshal(data,&cfg)
}
iferr!=nil{
return[]checkResult{{catEnvironment,"qtengine config",statusWarn,"Unreadable",fmt.Sprintf("DMS won't overwrite %s while it can't parse it, so Qt apps stop following DMS colors. Fix or remove the file.",path),url}}
// 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.
ifvalue=="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),
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,
}
}
// Qt skips QPA plugins built against another Qt major.minor without a
// warning, so an out-of-tree plugin silently stops loading after a Qt update.
catEnvironment,name,statusWarn,fmt.Sprintf("Built for Qt %s, Qt is %s",built,qt.version),
fmt.Sprintf("Qt ignores platform theme plugins built for another Qt minor version, so Qt apps silently fall back to a built-in theme, breaking icon lookups and palettes. Rebuild or reinstall the package providing %s against the installed Qt, then restart the shell.",plugin),
"Obsolete overview binds in "+strings.Join(overviewBinds,", "),
"mousebind=NONE,btn_left,toggleoverview,1 and mousebind=NONE,btn_right,killclient,0 swallow normal clicks; remove them.",url})
}
returnresults
}
funccheckAqueousConfigHelper()checkResult{
url:=doctorDocsURL+"#compositor-checks"
path,err:=exec.LookPath("aqueous-config")
iferr!=nil{
returncheckResult{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"
returncheckResult{catCompositor,"Background Blur",statusWarn,"Unsupported","Compositor does not support ext-background-effect-v1",doctorDocsURL+"#compositor-checks"}
returncheckResult{catOptionalFeatures,"adw-gtk3",statusOK,"Installed","GTK3 dynamic theming (system copy, applied on next theme apply)",optionalFeaturesURL}
caseuserLight||userDark||sysLight||sysDark:
missing:="adw-gtk3-dark"
ifuserDark||sysDark{
missing="adw-gtk3"
}
returncheckResult{catOptionalFeatures,"adw-gtk3",statusWarn,"Missing "+missing,"GTK3 dynamic theming needs both variants",optionalFeaturesURL}
default:
returncheckResult{catOptionalFeatures,"adw-gtk3",statusInfo,"Not installed","GTK3 dynamic theming; without it the global CSS override applies",optionalFeaturesURL}
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"})
hint:="Apps cannot follow light/dark mode. The portal is D-Bus activated and needs graphical-session.target; without systemd, or a compositor that never starts that target (Mango before 0.17.1), exec the portal backend and xdg-desktop-portal at session start."
@@ -46,7 +51,7 @@ 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,
}
varkeybindsRemoveCmd=&cobra.Command{
@@ -54,7 +59,7 @@ var keybindsRemoveCmd = &cobra.Command{
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,
}
varkeybindsResetCmd=&cobra.Command{
@@ -62,10 +67,23 @@ var keybindsResetCmd = &cobra.Command{
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:runKeybindsReset,
Run:runKeybindsEdit,
}
varkeybindsKeymapCmd=&cobra.Command{
Use:"keymap",
Short:"Report which keysym each physical key carries on the first level",
Long:"Print the keysyms the active keyboard layout puts on the first level of every physical key, keyed by xkb keycode.\n\n"+
"Compositors resolve bind keysyms at that level, so a keysym missing from this list cannot be bound however many modifiers are pressed. "+
"The keybind editor uses it to warn instead of storing a bind that never fires.",
Run:runKeybindsKeymap,
}
funcinit(){
keybindsCmd.PersistentFlags().String("expected-generation","","Configuration generation observed when editing Aqueous bindings")
keybindsSetCmd.Flags().Bool("no-inhibiting",false,"Keep bind active when shortcuts are inhibited (allow-inhibiting=false)")
keybindsSetCmd.Flags().String("replace-key","","Original key to replace (removes old key)")
keybindsSetCmd.Flags().String("flags","","Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
keybindsSetCmd.Flags().String("flags","","Hyprland bind flags as letters: e repeat, l locked, r release, o long-press, n non-consuming, m mouse, t transparent, i ignore-mods, u submap-universal, a auto-consuming, x allow-input-capture")
Long:"Manage the registries DMS fetches plugins and themes from. The official registry is always active; additional registries can be added by name and git URL.",
Long:"Manage the registries DMS fetches plugins and themes from. The official registry is always active and additional registries can be added by name and git URL, unless the CLI policy sets disable_registries.",
@@ -113,7 +124,7 @@ If no previous region exists, falls back to interactive selection.`,
varssWindowCmd=&cobra.Command{
Use:"window",
Short:"Capture the focused window",
Long:`Capture the currently focused window. Supported on Hyprland, Mango, and niri.`,
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,
}
@@ -150,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)
},
}
funcinit(){
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().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)")
setupHeadlessCmd.Flags().String("compositor","","Compositor to configure: niri, hyprland, or mango")
setupHeadlessCmd.Flags().String("terminal","","Also deploy a terminal config: ghostty, kitty, or alacritty (binds otherwise use $TERMINAL or the first installed terminal)")
setupHeadlessCmd.Flags().Bool("no-systemd",false,"Deploy session config without systemd integration")
setupHeadlessCmd.Flags().Bool("force",false,"Overwrite existing configs (timestamped backups are created)")
setupHeadlessCmd.Flags().Bool("skip-existing",false,"Warn and skip instead of failing when a config already exists")
setupHeadlessCmd.Flags().Bool("stock-binds",false,"Replace existing keybinds with the DMS stock set (a backup is kept)")
fmt.Printf("\nSettings saved by beta may lose options on stable. Restore the backup from your last\nstable run (%s)? Your beta settings stay in the beta backup. [y/N] ",path)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.