Compare commits

..
371 Commits
Author SHA1 Message Date
dms-ci[bot] 4a87e8227d bump VERSION to v1.6.3 2026-10-07 17:47:03 +00:00
bbedward c0b26caf2b dank-qml-common: classify scroll input by phase instead of pixelDelta
stable-1.6 branch of the submodule carries dank-qml-common 1ae7222 (port of d630451), so DankFlickable, DankListView and DankGridView stop treating Qt 6.12 mouse wheels as touchpads. The flake input, flake.lock and .gitmodules follow that branch.
2026-10-07 13:15:39 -04:00
Vladimir Kostelei c6c40dbced fix: fingerprint lid open (#3448)
* lock: keep the fingerprint reader usable across suspend

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

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

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

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

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

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

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

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

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

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

---------

(cherry picked from commit d7074a53eb)
2026-10-07 13:05:09 -04:00
Satoshoe 02ddd010a0 settings: skip unchanged saves so external edits are not swallowed (#3504)
saveSettings() set _selfWrite before setText(). When the serialized
settings were identical to the file, nothing was written, no
fileChanged followed, and the flag stayed set. The next external edit
of settings.json was then taken for our own write and never reloaded.

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

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

(cherry picked from commit 089dc98166)
2026-10-07 13:03:41 -04:00
Artem Timofeev 0adbdfdde1 fix(power): reconcile refresh rate after resume (#3459)
(cherry picked from commit 8fedfae1dc)
2026-10-07 13:01:32 -04:00
LemonMon fb002d1977 fix: stabilize tray item key to prevent position jumps on property changes (#3386)
* fix: keep tray order stable when tooltipTitle changes dynamically

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

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

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

* fix: bail on ambiguous saved tray keys in fallback

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

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

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

* tray: reuse free key slots so a restarted singleton keeps its plain id

(cherry picked from commit 60342b7416)
2026-10-07 13:01:06 -04:00
Marlon Kranz 8b7c9085ca fix(notifications): edit rules from the menu instead of deleting them (#3670)
* fix(notifications): edit rules from the menu instead of deleting them

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

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

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

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

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

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

The notification sound played before the check that dismisses a
notification with neither a popup nor a center entry, so dropped
notifications (transient while muted, Popup only plus Mute) still made
a sound. Play it only for notifications that are kept, and not when a
mute rule applies, like Do Not Disturb.

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

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

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

(cherry picked from commit 0b9d931ea3)
2026-10-07 12:58:37 -04:00
Pedro Jose Soto Rivera 7c04b61bcc network(networkd): treat bridge, bond, team and vlan links as wired (#3706)
* network(networkd): treat bridge, bond, team and vlan links as wired

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

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

Fixes #3463

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

Name prefixes can't cover every container/VM bridge (lxcbr0, waydroid0,
...). Check the bridge's members in /sys/class/net/<br>/brif instead and
only accept it as the uplink when at least one member is not virtual
(veth*, tap*, ...). This replaces the lxdbr*/incusbr* prefixes.

(cherry picked from commit f886789835)
2026-10-07 12:58:37 -04:00
Pedro Jose Soto Rivera bc4916a132 network(iwd): follow the Wi-Fi device when iwd recreates it (#3707)
When iwd restarts it recreates wlan0 under a new D-Bus path. Only the
Station interface was tracked on InterfacesAdded, so devicePath kept
pointing at the dead object: the PropertiesChanged match for the new
device was never registered, updateState() read Powered from the old
path, and SetWiFiEnabled() failed with "No matching method found".
WiFiEnabled stayed false after handleStationRemoved() until DMS was
restarted.

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

Refs #3495

(cherry picked from commit ff50b492b1)
2026-10-07 12:58:37 -04:00
Stephanos Kamprogiannis 58a356bb05 idle: wake monitors after automatic lock (#3633)
Locking rearms the normal idle monitors and can lose their wake transition.
Enable the existing locked-monitor wake listener for idle power-off too.

(cherry picked from commit 22fa08f0ff)
2026-10-07 12:58:37 -04:00
arfan 3061e1301d fix(theme-vscode): update editorSuggestWidget colors for improved visibility (#3599)
* fix(theme-vscode): update editorSuggestWidget colors for improved visibility in dark, default, and light themes

* fix(theme-vscode): update button.hoverBackground to use primary_fixed colors for consistency

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

(cherry picked from commit f3af870d93)
2026-10-07 12:58:37 -04:00
bbedward 10dbe73335 matugen: fix kde color scheme templates ignoring the accent
(cherry picked from commit f102a83d9f)
2026-10-07 12:58:37 -04:00
Huỳnh Thiện Lộc b6fee3a98f fix(island): avoid bottom clipping in media expanded sheet (#3593)
(cherry picked from commit 0717908d53)
2026-10-07 12:58:37 -04:00
Thomas Kroll 6166d51c7c fix(workspace): ignore duplicate submit in rename modal (#3549)
Pressing Enter in the name field fires both the text field's accepted
signal and the dialog's own Return handler, so submitAndClose() runs
twice. The first call renames and hides the window, which clears the
field; the second call then renames to an empty string and unsets the
workspace name. The OK button only fires once, so it was unaffected.

Ignore submits once the window is hidden.

Signed-off-by: Thomas Kroll <[email protected]>
(cherry picked from commit eb0cb80884)
2026-10-07 12:58:37 -04:00
Huỳnh Thiện Lộc d06fcccff9 fix(notepad): hide placeholder during IME composition (#3646)
(cherry picked from commit 340f2d179c)
2026-10-07 12:58:37 -04:00
Marlon Kranz d9cfeded47 fix(lock): show the lock screen on every display when none is selected (#3666)
With two or more screens connected and none of them in the lock screen
display list (a laptop docked at a different desk), every surface was
inactive and no password field rendered, so the only way out was a
TTY. getFilteredScreens only falls back to all screens with
showOnLastDisplay and exactly one screen, which the lock screen picker
never sets. Treat an empty result as all screens, the same as the
default "all" preference.

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

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

(cherry picked from commit 49a639a358)
2026-09-17 14:03:53 +00:00
Huỳnh Thiện Lộc 402a8515ee feat(screenshot): add flag to control HUD scale and visibility (#3398)
* feat(screenshot): add --hud flag to control HUD scale and visibility

* fix(screenshot): clamp HUD scale to 1-4 and clarify flag description

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

(cherry picked from commit b4f6da25ee)
2026-09-17 09:43:51 -04:00
Domen Kožar fd0564a3ec tray: use available width for menu labels (#3456)
(cherry picked from commit 1bf35426ea)
2026-09-17 09:43:22 -04:00
Marlon Kranz 07f8675a7b fix(frame): keep launcher edge-hover on the Top layer (#3453)
Opening the launcher by hovering the frame edge requested the Overlay
layer via CompositorService.framePeerSurfacesUseOverlayForScreen(),
which resolves to whether the frame is configured for the screen and
is true in normal use. DankLauncherV2ModalConnected's
frameOwnsConnectedChrome requires the Top layer, so the overlay
request silently dropped connected-chrome anchoring: the launcher
opened dead-centered with a different animation instead of emerging
from the configured frame side.

The keybind/bar-button path already opens on the Top layer by passing
no overlay argument. The edge-hover trigger only runs in connected
frame mode to begin with (gated by usesConnectedFrameChromeForScreen
in this file's own eligible check), so there's no case where it needs
standalone/overlay behavior. Match the keybind path.

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

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

(cherry picked from commit c9506bc847)
2026-09-16 13:28:09 +00:00
Giacomo Di Clerico fde2181c3b tailscale: handle profile switches and bus updates reactively (#3427)
* tailscale: handle profile switches and bus updates reactively

* tailscale: address review feedback on test coverage and dead code

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

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

(cherry picked from commit 6e9a657132)
2026-09-15 16:46:47 -04:00
Mattias 69955286a7 privacy: stop counting screen captures as camera use (#3428)
Apps capturing the screen through PipeWire, such as OBS, create a live
video input stream, which the camera check counted as camera use. Skip
streams tagged with the Screen media role.

(cherry picked from commit 6ecdd6d45d)
2026-09-15 16:46:47 -04:00
Timofey d3a02f6342 hyprland: use focusedMonitor instead of focusedWorkspace.monitor (#3424)
Hyprland's workspace refactor (hyprwm/Hyprland#16140) broke
Hyprland.focusedWorkspace.monitor as a way to find the focused screen
(quickshell-mirror/quickshell#1119). Quickshell already exposes
Hyprland.focusedMonitor directly, so read the monitor from there

(cherry picked from commit 05c807b7cd)
2026-09-15 16:46:47 -04:00
gibbert 86234e5b09 fix(audio): avoid false negative when some event sounds can't be found (#3437)
(cherry picked from commit 6af2b1fbc3)
2026-09-15 16:46:47 -04:00
deepfriedret 938fabe27d fix(media widget): prevent title from truncating with scroll song title on (#3436)
(cherry picked from commit 0133962936)
2026-09-15 16:46:47 -04:00
Mario Mohar 1b34aa0ce1 fix(battery): report when no charge threshold exists instead of faking success (#3385)
* fix(battery): report when no charge threshold exists instead of faking success

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

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

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

BatteryService.batteryAvailable is already the gate used twice in this
file for the same reason.

(cherry picked from commit 6b50696b8a)
2026-09-15 16:46:47 -04:00
Huỳnh Thiện Lộc 76ed5d68fa fix(clipboard): preserve pinned state when editing entries (#3412)
* fix(clipboard): preserve pinned state when editing entries

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

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

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

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

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

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

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

* fix(clipboard): pre-fetch entry before opening editor and allow retry on load failure

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

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

(cherry picked from commit 60675ecb39)
2026-09-10 03:36:41 +00:00
dms-ci[bot] 06cd88e990 nix: update vendorHash for go.mod changes 2026-09-09 09:10:51 -04:00
dms-ci[bot] aa4b99def4 bump VERSION to v1.6.1 2026-09-08 17:14:24 +00:00
Zoey Bauer 4015f634c4 add support for shelly (#3367)
* add support for shelly

* fixed terminal failure

* updated to have it not allow adding ignore aur

* updated error strings

* Fixed label

port 1.6

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

(cherry picked from commit 2bf2754ad2)
2026-09-08 14:19:25 +00:00
Huỳnh Thiện Lộc 1efc10dc7e fix(screenshot): force full-damage commit on cursor toggle (#3365)
* fix(screenshot): force full-damage commit on cursor toggle with compositor v4

* test(screenshot): drop tautological cursor-mismatch test

(cherry picked from commit 0f058468a5)
2026-09-08 09:24:09 -04:00
BB 94710cf643 feat: support for Aqueous Compositor (#3357)
* updated dank to support aqueous

* further updates

* aqueous support work

* updated to support workspace options better in aqueous

* added blur to dms shell for aqueous

* added keybinding update

* remove unneeded blur

* clean up per claud review

* swapped to ipc

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

* fix staticcheck

* aqueous: consolidate more aqueous specific stuff

* add aqueous logo

---------

Co-authored-by: Zoey <[email protected]>
(cherry picked from commit b9365610c8)
2026-09-08 09:23:54 -04:00
bbedward 3711cb11fa core: bump dankgo
fixes #3361
port 1.6

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

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

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

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

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

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

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

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

(cherry picked from commit 0890db93db)
2026-09-07 13:55:38 +00:00
Huỳnh Thiện Lộc 3f09d7b1c8 feat(screenshot): add touch & stylus support for region capture (#3353)
* feat(screenshot): add touch and stylus input support for region capture

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

port 1.6

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

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

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

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

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

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

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

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

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

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

port 1.6

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

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

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

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

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

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

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

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

port 1.6

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

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

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

fixes #3293
port 1.6

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

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

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

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

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

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

* chore(dash): remove unused todayButton id

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #3074

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

Two points from review on #3020.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

On an affected system:

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

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

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

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

Found in review by claude[bot].

* Update core/cmd/dms/commands_doctor.go

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

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-08-31 21:04:56 -04:00
bbedward 99c5f96575 bluetooth: toggle power from the adapter's live state,
fixes #3242
2026-08-31 19:55:31 -04:00
bbedward 7cc0955358 i18n: sync, new polish terms from @raikiu-source 2026-08-31 19:37:13 -04:00
bbedward 285ce66d99 update README video 2026-08-31 14:20:45 -04:00
Mario Mohar 887509548b fix(theme): take sun times from the local calendar day, not the UTC one (#3237)
* fix(theme): take sun times from the local calendar day, not the UTC one

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #3198

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

Review follow-up.

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

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

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

Catalogs untouched.

* chore: re-trigger CI

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

* clipboard: survive mmap faults from corrupted db pages

---------

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

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

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

* fix(dock): track fullscreen state declaratively

* fix(dock): clear overlay mask during fullscreen

* feat(dock): add fullscreen visibility option

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

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

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

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

* Update quickshell/Services/MprisController.qml

Concur with Claude's suggestion.

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

* Update quickshell/Services/MprisController.qml

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

---------

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

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

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

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

* fix(settings): resolve cellular tab index collision

* fix: address review feedback for cellular support

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

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

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

* fix: make Cellular selectable and the modem row clickable

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

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

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

* fix: address second round of cellular review feedback

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

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

* fix: address remaining cellular review comments

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

* fix: pass nmcli connection selector as separate argument

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

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

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

* apply review comments

* fix lints

* update debug doc

* fix style inconsistencies

---------

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

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

* fix(settings): shorten battery power label

* refactor(settings): remove duplicate power source

* chore(settings): update search index

* fix(settings): combine battery estimate time

* fix(settings): address battery layout review

* fix(settings): keep battery fallback translatable

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

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

* fix(clipboard): address drag drop review

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

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

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

* fix(launcher): wrap reverse action navigation

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

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

* add another dry run to get smart type preview

* fix too much keywords in search index

* revert dont extract translations

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

* re-extract settings search index

* make portal sync no longer disables matugen smart mode

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

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

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

`sunHourAngle` computed

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

but the sunrise equation is

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

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

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

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

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

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

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

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

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

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

* fix(suncalc): classify each solar threshold independently

---------

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

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

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

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

* fix(screenshot): rehome moved regions

* fix(screenshot): clamp region to active output

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

---------

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

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

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

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

* Update quickshell/Modules/ProcessList/ProcessListPopout.qml

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

---------

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

* fix(screenshot): reuse cursor shape device

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

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

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

* fix(wallpaper): persist folder selection across restarts

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

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

* fix(bar): address focused window review

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

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

* fix(screenshot): skip disjoint output overlays

* fix(screenshot): address multi-monitor review

* fix(screenshot): handle cross-output review

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

---------

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

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

launcher: fixed animated images in plugins that use them

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

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

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

* fix(audio): handle onShowPortSelector in AudioOutputDetail

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

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

* chore: remove unused property in AudioPortSelector

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* core: bump dgop version

---------

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

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

* fix(chooser): avoid copy shortcut conflict

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

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

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

* fix(ipc): handle indexed responses

* fix(ipc): follow error string lint

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

---------

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

* feat(niri): close overview on window focus

---------

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

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

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

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

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

---------

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

* feat(dock): add bar length padding

* feat(dock): expand bar padding range

* refactor(dock): group bar corner settings

* fix(i18n): avoid attached term variant

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

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

---------

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

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

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

---------

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

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

---------

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

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

* fix(matugen): reload Fcitx5 theme
2026-08-15 12:26:18 -04:00
bbedward fdf0d73b30 displays: apply refresh rate changes without compositor config reload
related #3073
2026-08-15 12:24:10 -04:00
bbedward ef05dbed9b clipboard: release virtual keyboard keys on paste error paths
related #3073
2026-08-15 12:23:25 -04:00
1154 changed files with 179670 additions and 37685 deletions
+31
View File
@@ -514,6 +514,37 @@ Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
**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)
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# Prunes retired XBPS packages from the R2 bucket so it stays inside the
# Cloudflare free tier. Only objects under ARCHIVE_PREFIX are eligible; live
# repositories (<repo>/current/) are never touched.
#
# publish-void-r2.sh writes retired packages to archive/<repo>/<published_at>/.
# Snapshots are dropped oldest first:
# 1. snapshots older than KEEP_DAYS, keeping KEEP_MIN_SNAPSHOTS per repo
# 2. then more until the whole bucket is <= MAX_BUCKET_GB
set -euo pipefail
require_env() {
local name
for name in "$@"; do
if [[ -z "${!name:-}" ]]; then
echo "error: $name is required" >&2
exit 2
fi
done
}
require_env R2_BUCKET R2_ENDPOINT
ARCHIVE_PREFIX="${ARCHIVE_PREFIX:-archive/}"
MAX_BUCKET_GB="${MAX_BUCKET_GB:-8}"
KEEP_DAYS="${KEEP_DAYS:-30}"
KEEP_MIN_SNAPSHOTS="${KEEP_MIN_SNAPSHOTS:-2}"
DRY_RUN="${DRY_RUN:-false}"
case "$ARCHIVE_PREFIX" in
*/) ;;
*) echo "error: ARCHIVE_PREFIX must end with /" >&2; exit 2 ;;
esac
aws_r2() {
aws --endpoint-url "$R2_ENDPOINT" "$@"
}
human() {
numfmt --to=iec-i --suffix=B "$1"
}
objects="$(aws_r2 s3api list-objects-v2 \
--bucket "$R2_BUCKET" \
--output json \
--query 'Contents[].{key:Key,size:Size}')"
[[ -n "$objects" && "$objects" != null ]] || objects='[]'
total="$(jq 'map(.size) | add // 0' <<<"$objects")"
budget="$(awk -v g="$MAX_BUCKET_GB" 'BEGIN { printf "%d", g * 1073741824 }')"
now="$(date -u +%s)"
snapshots="$(jq -c --arg p "$ARCHIVE_PREFIX" '
map(select(.key | startswith($p)))
| map(.rel = (.key[($p | length):] | split("/")))
| map(select((.rel | length) >= 3))
| group_by(.rel[0:2])
| map({
repo: .[0].rel[0],
stamp: .[0].rel[1],
prefix: ($p + (.[0].rel[0:2] | join("/")) + "/"),
size: (map(.size) | add),
count: length
})
| sort_by(.stamp)
' <<<"$objects")"
to_delete="$(jq -c \
--argjson now "$now" \
--argjson keepDays "$KEEP_DAYS" \
--argjson keepMin "$KEEP_MIN_SNAPSHOTS" \
--argjson total "$total" \
--argjson budget "$budget" '
def epoch:
.stamp
| sub("T(?<h>[0-9]{2})-(?<m>[0-9]{2})-(?<s>[0-9]{2})Z$"; "T\(.h):\(.m):\(.s)Z")
| try fromdateiso8601 catch null;
(group_by(.repo) | map({key: .[0].repo, value: length}) | from_entries) as $perRepo
| reduce .[] as $s ({kept: $perRepo, total: $total, delete: []};
($s | epoch) as $e
| (.kept[$s.repo] > $keepMin and $e != null and ($now - $e) > $keepDays * 86400) as $expired
| (.total > $budget) as $over
| if $expired or $over then
.delete += [$s + {reason: (if $over then "budget" else "age" end)}]
| .total -= $s.size
| .kept[$s.repo] -= 1
else . end)
| .delete
' <<<"$snapshots")"
echo "bucket: $R2_BUCKET ($(human "$total") in $(jq length <<<"$objects") objects)"
echo "budget: $(human "$budget") (MAX_BUCKET_GB=$MAX_BUCKET_GB)"
echo "archive: $(jq length <<<"$snapshots") snapshots, $(human "$(jq 'map(.size) | add // 0' <<<"$snapshots")")"
echo "policy: KEEP_DAYS=$KEEP_DAYS KEEP_MIN_SNAPSHOTS=$KEEP_MIN_SNAPSHOTS DRY_RUN=$DRY_RUN"
deleted=0
freed=0
while IFS=$'\t' read -r prefix size count reason; do
[[ -n "$prefix" ]] || continue
# Belt and braces: never delete outside the archive or anything live.
case "$prefix" in
"$ARCHIVE_PREFIX"*) ;;
*) echo "error: refusing to delete $prefix (outside $ARCHIVE_PREFIX)" >&2; exit 1 ;;
esac
case "$prefix" in
*/current/*) echo "error: refusing to delete live prefix $prefix" >&2; exit 1 ;;
esac
if [[ "$DRY_RUN" == "true" ]]; then
echo "would delete $prefix ($count objects, $(human "$size"), $reason)"
else
echo "deleting $prefix ($count objects, $(human "$size"), $reason)"
aws_r2 s3 rm "s3://${R2_BUCKET}/${prefix}" --recursive --only-show-errors
fi
deleted=$((deleted + 1))
freed=$((freed + size))
done < <(jq -r '.[] | [.prefix, .size, .count, .reason] | @tsv' <<<"$to_delete")
remaining=$((total - freed))
echo "pruned: $deleted snapshots, $(human "$freed") freed, $(human "$remaining") remaining"
if (( remaining > budget )); then
echo "::warning::bucket is still $(human "$remaining") after pruning (budget $(human "$budget")); live repositories exceed the budget"
fi
note=""
if [[ "$DRY_RUN" == "true" ]]; then
note=" (dry run)"
fi
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
{
echo "### R2 prune: \`$R2_BUCKET\`"
echo
echo "| | |"
echo "|---|---|"
echo "| Before | $(human "$total") |"
echo "| Budget | $(human "$budget") |"
echo "| Snapshots pruned | $deleted$note |"
echo "| Freed | $(human "$freed") |"
echo "| After | $(human "$remaining") |"
if (( deleted > 0 )); then
echo
echo "| Snapshot | Objects | Bytes | Reason |"
echo "|---|---|---|---|"
jq -r '.[] | "| `\(.prefix)` | \(.count) | \(.size) | \(.reason) |"' <<<"$to_delete"
fi
} >> "$GITHUB_STEP_SUMMARY"
fi
+115
View File
@@ -0,0 +1,115 @@
name: Claude Review
# `/claude review` on a PR by a member of an allowed team. Review only, no edits or pushes.
# Secrets: CLAUDE_CODE_OAUTH_TOKEN (claude setup-token), CLAUDE_REVIEW_ORG_READ_TOKEN (PAT, org Members: read)
on:
issue_comment:
types: [created]
permissions: {}
jobs:
gate:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/claude review')
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
outputs:
allowed: ${{ steps.team.outputs.allowed }}
steps:
- name: Check team membership
id: team
env:
GH_TOKEN: ${{ secrets.CLAUDE_REVIEW_ORG_READ_TOKEN }}
ORG: ${{ github.repository_owner }}
USER: ${{ github.event.comment.user.login }}
TEAMS: dms-collaborators owners
run: |
allowed=false
for team in $TEAMS; do
state=$(gh api "orgs/$ORG/teams/$team/memberships/$USER" --jq .state 2>/dev/null || true)
[ "$state" = active ] && allowed=true && break
done
echo "allowed=$allowed" >> "$GITHUB_OUTPUT"
- name: React
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ALLOWED: ${{ steps.team.outputs.allowed }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
[ "$ALLOWED" = true ] && content=eyes || content=-1
gh api -X POST "repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions" -f "content=$content"
review:
needs: gate
if: needs.gate.outputs.allowed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: claude-review-${{ github.event.issue.number }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
ref: refs/pull/${{ github.event.issue.number }}/head
fetch-depth: 0
submodules: recursive
- uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
Review pull request #${{ github.event.issue.number }} in ${{ github.repository }}, requested by @${{ github.event.comment.user.login }}:
---
${{ github.event.comment.body }}
---
Text after `/claude review` is an optional focus. Honor it but still report anything serious outside it.
The PR head is checked out. Use `gh pr view ${{ github.event.issue.number }}` and `gh pr diff ${{ github.event.issue.number }}`, read surrounding code as needed.
Steps:
1. Read CONTRIBUTING.md.
2. Read the PR title, description and diff.
3. Check existing comments so you don't repeat them: `gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments` and `gh pr view ${{ github.event.issue.number }} --comments`. Skip findings already covered by a comment that still applies.
4. Verify each candidate finding against the code. Drop anything you can't confirm.
5. Post one inline comment per confirmed finding with `mcp__github_inline_comment__create_inline_comment` (`confirmed: true`). Say what's wrong, why, and the fix. Add a committable suggestion only if it alone fully fixes the issue.
6. Post one new summary comment with `gh pr comment ${{ github.event.issue.number }} --body ...`. Never edit earlier comments. Format:
## Claude review
<one line verdict>
- <finding, file:line> per inline comment posted this run
Checked: <one line>. Model: claude-opus-5.
No findings: "No issues found." plus what was checked.
Flag only issues introduced by this PR:
- Bugs: wrong logic, crashes, nil/undefined access, races, leaks (timers, connections, processes, handles), bad Go error handling, edge cases that will clearly be hit.
- QML: undefined refs, missing imports, binding loops, bindings that never update, handlers on the wrong object, raw Flickable/ListView/ScrollView instead of the Dank* wrappers.
- Security: command injection, path traversal, unsafe file permissions, secrets in code.
- CONTRIBUTING.md rules: user-facing strings in `I18n.tr()` with translator context, reuse existing terms from quickshell/translations/en.json; en.json and template.json must not be touched in a PR; mocks under core/internal/mocks/ are regenerated not hand-edited; new dank-qml-common contract properties need the matching library change.
- Go: unformatted code, go.mod/go.sum out of sync with imports, tests that no longer compile.
Don't flag style or naming, pre-existing issues, anything a linter or compiler catches, speculative input-dependent problems, missing tests or docs, or refactor ideas. Not sure it's real: don't post it.
Review only. Never edit files, commit, push, approve or request changes. Post each finding once. Keep comments short.
claude_args: |
--model claude-opus-5
--max-turns 80
--allowedTools "Read,Grep,Glob,LS,Task,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*),Bash(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*)"
--disallowedTools "Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch"
- if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr comment ${{ github.event.issue.number }} --body "Claude review failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+13 -5
View File
@@ -31,18 +31,25 @@ jobs:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
run: sudo apt-get update && sudo apt-get install -y flatpak
- name: Add flathub
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Cache flatpak runtime
uses: actions/cache@v4
with:
path: ~/.local/share/flatpak
key: flatpak-org.freedesktop.Platform-24.08
- name: Add a flatpak that mutagen could support
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Install a flatpak for tests
run: |
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak info org.freedesktop.Platform//24.08 >/dev/null 2>&1 || \
flatpak install --user -y org.freedesktop.Platform//24.08
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache-dependency-path: ./core/go.sum
- name: Test
run: go test -v ./...
@@ -75,6 +82,7 @@ jobs:
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache-dependency-path: ./core/go.sum
- name: Build all packages
env:
+66
View File
@@ -0,0 +1,66 @@
name: Prune Void R2 archive
# Keeps the danklinux-void bucket inside the Cloudflare R2 free tier by
# deleting the oldest archive/<repo>/<timestamp>/ snapshots written by
# publish-void-r2.sh. Live repositories (<repo>/current/) are never touched.
on:
schedule:
- cron: "30 6 * * *"
workflow_dispatch:
inputs:
dry_run:
description: 'Report what would be deleted without deleting'
type: boolean
required: false
default: false
max_bucket_gb:
description: 'Total bucket size to prune down to (GB)'
type: string
required: false
default: '8'
keep_days:
description: 'Delete archive snapshots older than this many days'
type: string
required: false
default: '30'
keep_min_snapshots:
description: 'Always keep at least this many snapshots per repo (age rule only)'
type: string
required: false
default: '2'
permissions:
contents: read
# Serialize with the XBPS publisher so a snapshot is never pruned mid-publish.
concurrency:
group: void-xbps-${{ github.ref }}
cancel-in-progress: false
jobs:
prune:
name: Prune archive snapshots
runs-on: ubuntu-latest
if: github.repository == 'AvengeMedia/DankMaterialShell'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: true
R2_BUCKET: danklinux-void
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
ARCHIVE_PREFIX: archive/
MAX_BUCKET_GB: ${{ github.event.inputs.max_bucket_gb || '8' }}
KEEP_DAYS: ${{ github.event.inputs.keep_days || '30' }}
KEEP_MIN_SNAPSHOTS: ${{ github.event.inputs.keep_min_snapshots || '2' }}
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: false
- name: Prune archive
run: bash .github/scripts/prune-void-r2.sh
+57
View File
@@ -23,7 +23,26 @@ concurrency:
cancel-in-progress: true
jobs:
verify-version:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag }}
- name: Verify quickshell/VERSION matches the tag
env:
TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
version=$(cat quickshell/VERSION)
if [ "${version#v}" != "${TAG#v}" ]; then
echo "::error::quickshell/VERSION is '${version}' but the tag is '${TAG}'"; exit 1
fi
build-core:
needs: [verify-version]
runs-on: ubuntu-latest
strategy:
matrix:
@@ -497,3 +516,41 @@ jobs:
exit 0
fi
bash scripts/notify-issues.sh "$PREVIOUS_TAG" "$TAG"
# Pin the satellite repos to this release's version. Each dispatch target
# decides for itself whether to tag: X.Y.0 always tags, X.Y.Z skips when the
# repo has no commits of its own since its last tag.
# fanout:
# name: Pin ${{ matrix.repo }}
# runs-on: ubuntu-latest
# needs: [release]
# if: ${{ !contains(inputs.tag, '-') }}
# strategy:
# fail-fast: false
# matrix:
# repo:
# - dgop
# - dankgo
# - dank-qml-common
# - dankcalendar
# - danksearch
# - dank-greeter
# steps:
# - name: Create GitHub App token
# id: app_token
# uses: actions/create-github-app-token@v3
# with:
# app-id: ${{ secrets.APP_ID }}
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
# owner: ${{ github.repository_owner }}
# repositories: ${{ matrix.repo }}
#
# - name: Dispatch pin-release
# env:
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
# REPO: ${{ github.repository_owner }}/${{ matrix.repo }}
# TAG: ${{ inputs.tag }}
# run: |
# set -euo pipefail
# gh workflow run pin-release.yml --repo "$REPO" -f version="$TAG"
# echo "dispatched pin-release ${TAG} to ${REPO}" >> "$GITHUB_STEP_SUMMARY"
+1
View File
@@ -117,6 +117,7 @@ core.*
.envrc
.direnv/
quickshell/dms-plugins
quickshell/dms-plugins-external
__pycache__
.vscode/
+1 -1
View File
@@ -1,4 +1,4 @@
[submodule "dank-qml-common"]
path = dank-qml-common
url = https://github.com/AvengeMedia/dank-qml-common.git
branch = master
branch = stable-1.6
+1 -1
View File
@@ -26,7 +26,7 @@ repos:
name: settings search index is up to date
entry: bash -c 'python3 quickshell/translations/extract_settings_index.py >/dev/null || exit 1; if ! git diff --exit-code -- quickshell/translations/settings_search_index.json; then echo "settings_search_index.json is out of date; run quickshell/translations/extract_settings_index.py and stage the result" >&2; exit 1; fi'
language: system
files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$
files: ^quickshell/(Modules/Settings/.*\.qml|Common/SettingsTabs\.qml|Services/SettingsSearchService\.qml|translations/(extract_settings_index\.py|en\.json))$
pass_filenames: false
- repo: local
hooks:
+62
View File
@@ -0,0 +1,62 @@
## What is this?
DMS is an open-source, MIT-licensed desktop shell for Wayland compositors on linux. Like KWin, gnome-shell, or cosmic-shell - but designed to work with many different compositors.
## Repo Structure:
There are 2 main apps in the repo, they are designed to operate together. Additionally there is a dank-qml-common submodule which contains common QML widgets, like a shared library, that is leveraged by other quickshell apps in the Dank Desktop suite.
There is a GO module (dankgo) that is used by core for certain common go functionality.
### 1- core:
Contains 2 apps:
1. cmd/dms - The parent process for quickshell (shell runner), and general purpose tools and utilities (such as screenshot, clipboard). It also runs the unix socket server the UI uses for functionality quickshell doesn't provide.
2. cmd/dankinstall - An entirely separate app, a TUI for quickly installing DMS
### 2- quickshell:
A quickshell application, the entirety of the UI for DMS. It uses quickshells own services supplemented by the core unix socket server for additional functionality.
Layout:
- Common/ - app-wide singletons and helpers (Theme, SettingsData, SessionData, I18n, PopoutManager)
- Services/ - headless singletons that talk to the system and to core
- Modules/ - the visible shell: DankBar, ControlCenter, Dock, Lock, Notifications, OSD, and so on
- Modals/ - large standalone surfaces: Settings, DankLauncherV2, Clipboard, Greeter
- Widgets/ - reusable Dank* components (DankListView, DankIcon, DankToggle, ...)
- PLUGINS/ - the plugin system; third-party plugins load from the user's config dir
- translations/ - POEditor-synced catalogs
### Other directories:
- docs/ - IPC reference, custom theme docs
- distro/ - packaging for debian, fedora, nix, opensuse, ubuntu, void
## General Rules:
- Keep it simple. Do not overcomplicate things.
- Follow each apps' own conventions. For example, QML widgets use Theme tokens instead of hardcoding colors, spacing, or other constants.
- Resource usage is extremely important to DMS. The shell runs 24/7 on every machine it's installed on - audit any change for idle CPU cost, extra processes, timers, and retained memory.
- `core` and `quickshell` are tightly coupled through the unix socket protocol. When you are changing one, be mindful of the consequences for the other.
- Use the Dank* wrappers in Widgets/ (DankListView, DankFlickable, and so on) instead of raw ListView/Flickable/ScrollView.
- All user-facing text goes through I18n.tr(). Prefer reusing existing catalog terms over adding new ones. Never edit the translation catalogs by hand, they are synced with POEditor.
- Do not start editing code in response to a question. We'll tell you when to edit code.
- Do not leave paragraphs of comments on top of the code. You should try to avoid them as much as possible with understandable function names and code. If they are necessary even then, make them concise. Remove such comments when you come by them in the codebase. Comments should always move with code, not be left behind.
- Use guard statement patterns in any code you write.
- Do not write any useless tests, tests should cover input output validation - not useless things like "main.go contains func main()"
- Do not edit generated code directly, mocks are generated with mockery. Not edited by hand
- If we are missing a glaring issue when we ask you to do something, do not hesitate to point it out.
- Reinvent the wheel but do not reinvent the car. If you are solving a simple problem do not introduce a library. If you are solving a complex but a common problem, there is likely a modern library for it, if so, use it.
- Never commit or push code unless explicitly asked to do so.
- Never make a PR unless explicitly asked to do so.
## Commit Messages
Commit messages start with the part of the system they touched, followed by a short lowercase explanation of the work:
network: report wifi state from the associated adapter
doctor: flag config files that fail to parse
The title should be concise. Description should explain the work in more detail (only if required) while still being concise. Use simple language, do not try to sound smart.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+17
View File
@@ -159,6 +159,8 @@ Text {
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
Don't re-extract the translations. `en.json` and `template.json` are synced with POEditor by a maintainer script, so running `extract_translations.py` in your PR just makes a diff that fights the next sync. Add your `I18n.tr()` calls and leave the catalogs alone. (`settings_search_index.json` is the exception, the pre-commit hook regenerates that one when you touch settings QML.)
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
### GO (`core` directory)
@@ -169,6 +171,21 @@ 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.
## Pull request
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
+4 -13
View File
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build dev run clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
.PHONY: all build dev run clean lint-qml install install-bin install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
all: build
@@ -53,14 +53,6 @@ install-bin:
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Binary installed"
install-shell:
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
@test -e $(SHELL_DIR)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
@mkdir -p $(SHELL_INSTALL_DIR)
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@echo "Shell files installed"
install-completions:
@echo "Installing shell completions..."
@mkdir -p $(DATA_DIR)/bash-completion/completions
@@ -98,7 +90,7 @@ install-desktop:
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entries installed"
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
install: install-bin install-completions install-systemd install-icon install-desktop
@echo ""
@echo "Installation complete!"
@echo ""
@@ -111,7 +103,7 @@ uninstall-bin:
@echo "Binary removed"
uninstall-shell:
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
@echo "Removing pre-1.6 shell files from $(SHELL_INSTALL_DIR)..."
@rm -rf $(SHELL_INSTALL_DIR)
@echo "Shell files removed"
@@ -159,7 +151,6 @@ help:
@echo "Install:"
@echo " install - Build and install everything (requires sudo)"
@echo " install-bin - Install only the binary"
@echo " install-shell - Install only shell files"
@echo " install-completions - Install only shell completions"
@echo " install-systemd - Install only systemd service"
@echo " install-icon - Install only icon"
@@ -168,7 +159,7 @@ help:
@echo "Uninstall:"
@echo " uninstall - Remove everything (requires sudo)"
@echo " uninstall-bin - Remove only the binary"
@echo " uninstall-shell - Remove only shell files"
@echo " uninstall-shell - Remove shell files left by pre-1.6 installs"
@echo " uninstall-completions - Remove only shell completions"
@echo " uninstall-systemd - Remove only systemd service"
@echo " uninstall-icon - Remove only icon"
+2 -2
View File
@@ -47,7 +47,7 @@ DankMaterialShell/
<div align="center">
https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
https://github.com/user-attachments/assets/d310418b-cc44-4ace-ac0d-2fc0c851291d
</div>
@@ -203,7 +203,7 @@ For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeM
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=AvengeMedia/DankMaterialShell&type=date&legend=top-left)](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
[![Star History Chart](https://star-history.dera.page/svg?repos=AvengeMedia/DankMaterialShell&type=date&legend=top-left)](https://star-history.dera.page/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
## License
+5 -1
View File
@@ -8,9 +8,13 @@ Requisite=graphical-session.target
Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms
ExecReload=/bin/kill -USR1 $MAINPID
LimitNOFILE=16384:infinity
Restart=on-failure
RestartForceExitStatus=TEMPFAIL
SuccessExitStatus=TEMPFAIL
RestartSec=1.23
TimeoutStartSec=90s
TimeoutStopSec=10
[Install]
+5 -1
View File
@@ -72,7 +72,6 @@ linters:
- path: _test\.go
linters:
- errcheck
- govet
- unused
- ineffassign
- staticcheck
@@ -97,6 +96,11 @@ linters:
- linters:
- ineffassign
path: internal/proto/
# Exclude proto pkg SA4006 (generated protocol code has trailing dead offset increments)
- linters:
- staticcheck
text: "SA4006:"
path: internal/proto/
# binary.Write/Read to bytes.Buffer can't fail
- linters:
- errcheck
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.12.2
rev: v2.13.1
hooks:
- id: golangci-lint-fmt
require_serial: true
+2 -1
View File
@@ -135,7 +135,8 @@ fmt:
vet:
@echo "Running go vet..."
$(GO) vet ./...
$(GO) vet $$($(GO) list ./... | grep -v internal/highlight/lexers)
$(GO) vet -composites=false ./internal/highlight/lexers
deps:
@echo "Updating dependencies..."
+18 -8
View File
@@ -132,9 +132,11 @@ This runs gofmt, golangci-lint, tests, and builds before each commit when `core/
**Regenerating Wayland Protocol Bindings:**
Protocol XML files live in `internal/proto/xml/`. Bindings are generated with the [dankgo](https://github.com/AvengeMedia/dankgo) `go-wayland-scanner`:
```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
go run github.com/AvengeMedia/dankgo/cmd/go-wayland-scanner@latest \
-i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
-pkg wlr_gamma_control -o internal/proto/wlr_gamma_control/gamma_control.go
```
@@ -155,17 +157,25 @@ curl -fsSL https://install.danklinux.com | sh
**Headless (unattended):**
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:
```bash
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --include-deps dms-greeter -y
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --dms-greeter -y
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty --git-deps niri,quickshell --all-features -y
```
| Flag | Short | Description |
|------|-------|-------------|
| `--compositor <niri|hyprland>` | `-c` | Compositor/WM to install (required for headless) |
| `--term <ghostty|kitty|alacritty>` | `-t` | Terminal emulator (required for headless) |
| `--compositor <niri\|hyprland\|mango>` | `-c` | Compositor/WM to install (required for headless) |
| `--term <ghostty\|kitty\|alacritty>` | `-t` | Terminal emulator (required for headless) |
| `--privesc <sudo\|doas\|run0>` | | Privilege escalation tool (default: autodetect) |
| `--git` | | Install the git version of every dependency that has one |
| `--git-deps <name,...>` | | Install the git version of specific dependencies (e.g. `niri,quickshell`) |
| `--all-features` | | Enable all optional dependencies (`dms-greeter`, `danksearch`, `dankcalendar`) |
| `--dms-greeter` | | Install dms-greeter |
| `--danksearch` | | Install danksearch and enable its user indexing service |
| `--dankcalendar` | | Install dankcalendar |
| `--include-deps <name,...>` | | Enable optional dependencies (e.g. `dms-greeter`) |
| `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
@@ -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
+25 -18
View File
@@ -14,10 +14,13 @@ import (
var Version = "dev"
// Flag variables bound via pflag
var (
compositor string
term string
privescTool string
gitAll bool
gitDeps []string
allFeatures bool
includeDeps []string
excludeDeps []string
replaceConfigs []string
@@ -25,6 +28,7 @@ var (
yes bool
danksearch bool
dankcalendar bool
dmsGreeter bool
)
var rootCmd = &cobra.Command{
@@ -35,8 +39,8 @@ var rootCmd = &cobra.Command{
Without flags, it launches an interactive TUI. Providing either --compositor
or --term activates headless (unattended) mode, which requires both flags.
Headless mode requires cached sudo credentials. Run 'sudo -v' beforehand, or
configure passwordless sudo for your user.`,
Headless mode requires cached credentials or a passwordless rule for your
privilege escalation tool (sudo, doas, or run0).`,
Args: cobra.NoArgs,
RunE: runDankinstall,
SilenceErrors: true,
@@ -46,6 +50,10 @@ configure passwordless sudo for your user.`,
func init() {
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)")
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
rootCmd.Flags().StringVar(&privescTool, "privesc", "", "Privilege escalation tool: sudo, doas, or run0 (default: autodetect)")
rootCmd.Flags().BoolVar(&gitAll, "git", false, "Install the git version of every dep that has one")
rootCmd.Flags().StringSliceVar(&gitDeps, "git-deps", []string{}, "Deps to install the git version of (e.g. niri,quickshell)")
rootCmd.Flags().BoolVar(&allFeatures, "all-features", false, "Enable all optional deps (dms-greeter, danksearch, dankcalendar)")
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
@@ -53,6 +61,7 @@ func init() {
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
rootCmd.Flags().BoolVar(&danksearch, "danksearch", false, "Install danksearch and enable its user indexing service")
rootCmd.Flags().BoolVar(&dankcalendar, "dankcalendar", false, "Install dankcalendar")
rootCmd.Flags().BoolVar(&dmsGreeter, "dms-greeter", false, "Install dms-greeter")
}
func main() {
@@ -71,8 +80,11 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
headlessMode := compositor != "" || term != ""
if !headlessMode {
// Reject headless-only flags when running in TUI mode.
headlessOnly := []string{
"privesc",
"git",
"git-deps",
"all-features",
"include-deps",
"exclude-deps",
"replace-configs",
@@ -80,6 +92,7 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
"yes",
"danksearch",
"dankcalendar",
"dms-greeter",
}
var set []string
for _, name := range headlessOnly {
@@ -99,7 +112,6 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
}
func runHeadless() error {
// Validate required flags
if compositor == "" {
return fmt.Errorf("--compositor is required for headless mode (niri, hyprland, or mango)")
}
@@ -110,6 +122,10 @@ func runHeadless() error {
cfg := headless.Config{
Compositor: compositor,
Terminal: term,
PrivescTool: privescTool,
GitAll: gitAll,
GitDeps: gitDeps,
AllFeatures: allFeatures,
IncludeDeps: includeDeps,
ExcludeDeps: excludeDeps,
ReplaceConfigs: replaceConfigs,
@@ -117,11 +133,11 @@ func runHeadless() error {
Yes: yes,
DankSearch: danksearch,
DankCalendar: dankcalendar,
DmsGreeter: dmsGreeter,
}
runner := headless.NewRunner(cfg)
// Set up file logging
fileLogger, err := log.NewFileLogger()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to create log file: %v\n", err)
@@ -136,10 +152,6 @@ func runHeadless() error {
}
}()
} else {
// Drain the log channel to prevent blocking sends from deadlocking
// downstream components (distros, config deployer) that write to it.
// Use an explicit stop signal because this code does not own the
// runner log channel and cannot assume it will be closed.
defer drainLogChan(runner.GetLogChan())()
}
@@ -179,10 +191,6 @@ func runTUI() error {
if fileLogger != nil {
fileLogger.StartListening(model.GetLogChan())
} else {
// Drain the log channel to prevent blocking sends from deadlocking
// downstream components (distros, config deployer) that write to it.
// Use an explicit stop signal because this code does not own the
// model log channel and cannot assume it will be closed.
defer drainLogChan(model.GetLogChan())()
}
@@ -200,10 +208,9 @@ func runTUI() error {
return nil
}
// drainLogChan starts a goroutine that discards all messages from logCh,
// preventing blocking sends from deadlocking downstream components. It returns
// a cleanup function that signals the goroutine to stop and waits for it to
// exit. Callers should defer the returned function.
// drainLogChan discards messages from logCh so blocking sends in downstream
// components cannot deadlock when no file logger is attached; the returned
// cleanup stops the drain without assuming the channel will ever close.
func drainLogChan(logCh <-chan string) func() {
drainStop := make(chan struct{})
drainDone := make(chan struct{})
+8 -10
View File
@@ -8,13 +8,13 @@ import (
"strings"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight"
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight/lexers"
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight/styles"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/spf13/cobra"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
ghtml "github.com/yuin/goldmark/renderer/html"
@@ -116,7 +116,7 @@ func getCachedStyle(name string) *chroma.Style {
style := styles.Get(name)
if style == nil {
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
style = styles.Fallback
style = styles.Fallback()
}
cacheMutex.Lock()
@@ -209,12 +209,10 @@ func runChroma(cmd *cobra.Command, args []string) {
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithStyle(chromaStyle),
highlighting.WithFormatOptions(
html.WithClasses(!chromaInline),
),
),
highlight.Markdown{
Style: styles.Get(chromaStyle),
Formatter: html.New(html.WithClasses(!chromaInline)),
},
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
+1 -1
View File
@@ -932,7 +932,7 @@ func downloadToTempFile(rawURL string) (string, error) {
var contentType string
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
for attempt := range 3 {
if attempt > 0 {
time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
}
+19 -10
View File
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"runtime/debug"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
@@ -10,10 +11,11 @@ import (
)
var (
colorOutputFmt string
colorAutocopy bool
colorNotify bool
colorLowercase bool
colorOutputFmt string
colorAutocopy bool
colorNotify bool
colorLowercase bool
colorAllowMulti bool
)
var colorCmd = &cobra.Command{
@@ -53,7 +55,8 @@ Examples:
dms color pick --rgb # Output as RGB
dms color pick --json # Output all formats as JSON
dms color pick --hex -l # Output hex in lowercase
dms color pick -a # Auto-copy result to clipboard`,
dms color pick -a # Auto-copy result to clipboard
dms color pick --allow-multiple # Skip the one-picker-at-a-time guard`,
Run: runColorPick,
}
@@ -68,6 +71,7 @@ func init() {
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
colorPickCmd.Flags().BoolVar(&colorAllowMulti, "allow-multiple", false, "Open a picker even when another selection overlay is already open")
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
@@ -89,13 +93,18 @@ func runColorPick(cmd *cobra.Command, args []string) {
}
config := colorpicker.Config{
Format: format,
CustomFormat: colorOutputFmt,
Lowercase: colorLowercase,
Autocopy: colorAutocopy,
Notify: colorNotify,
Format: format,
CustomFormat: colorOutputFmt,
Lowercase: colorLowercase,
Autocopy: colorAutocopy,
Notify: colorNotify,
AllowMultiple: colorAllowMulti,
}
// One-shot process holding a few screen-sized buffers: skip GC cycles while it runs.
debug.SetGCPercent(-1)
debug.SetMemoryLimit(1 << 30)
picker := colorpicker.New(config)
color, err := picker.Run()
if err != nil {
+5 -5
View File
@@ -150,9 +150,9 @@ func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]b
}
baseDir := filepath.Dir(absPath)
lines := strings.Split(string(data), "\n")
lines := strings.SplitSeq(string(data), "\n")
for _, line := range lines {
for line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
@@ -229,7 +229,7 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
baseDir := filepath.Dir(absPath)
content := string(data)
for _, line := range strings.Split(content, "\n") {
for line := range strings.SplitSeq(content, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
continue
@@ -306,9 +306,9 @@ func mangowcFindInclude(filePath, target string, processed map[string]bool) bool
}
baseDir := filepath.Dir(absPath)
lines := strings.Split(string(data), "\n")
lines := strings.SplitSeq(string(data), "\n")
for _, line := range lines {
for line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
+5 -5
View File
@@ -79,11 +79,11 @@ func runDank16(cmd *cobra.Command, args []string) {
primaryLight = primaryColor
}
variantOpts := dank16.VariantOptions{
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
Background: background,
UseDPS: contrastAlgo == "dps",
IsLightMode: isLight,
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
BackgroundDark: background,
UseDPS: contrastAlgo == "dps",
IsLightMode: isLight,
}
variantColors := dank16.GenerateVariantPalette(variantOpts)
fmt.Print(dank16.GenerateVariantJSON(variantColors))
+353 -22
View File
@@ -2,7 +2,9 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"os/exec"
"path/filepath"
@@ -15,6 +17,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
@@ -92,6 +95,8 @@ var (
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
scrollVersionRegex = regexp.MustCompile(`scroll version (\d+\.\d+)`)
aqueousVersionRegex = regexp.MustCompile(`(?i)aqueous v?(\d+\.\d+(?:\.\d+)?)`)
)
var doctorCmd = &cobra.Command{
@@ -319,8 +324,16 @@ func checkSystemInfo() []checkResult {
func checkEnvironmentVars() []checkResult {
var results []checkResult
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")...)
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME_QT6")...)
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") != os.Getenv("QT_QPA_PLATFORMTHEME") {
results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME_QT6")...)
}
results = append(results, checkEnvVar("QS_ICON_THEME")...)
results = append(results, checkXDGMenuPrefix()...)
if matugen.QtengineActive() {
results = append(results, checkQtenginePlugin()...)
}
return results
}
@@ -352,6 +365,228 @@ func checkXDGMenuPrefix() []checkResult {
return nil
}
// qtQueryBinaries are the Qt build tools that can be asked where Qt looks for
// plugins, in probe order. A binary's name does not reliably say which Qt it
// belongs to — plain "qmake" is Qt5 on Arch and Qt6 elsewhere — so each one is
// asked for QT_VERSION and the major is taken from the answer.
var qtQueryBinaries = []struct{ bin, flag string }{
{"qmake6", "-query"},
{"qmake-qt6", "-query"},
{"qtpaths6", "--query"},
{"qtpaths-qt6", "--query"},
{"qmake-qt5", "-query"},
{"qtpaths-qt5", "--query"},
{"qmake", "-query"},
{"qtpaths", "--query"},
}
// checkQtenginePlugin verifies the qtengine platform theme plugin is installed
// where Qt actually searches, for every Qt major that can be interrogated.
//
// Qt is asked where it looks rather than the filesystem searched, because a
// package can install the Qt5 plugin under a prefix Qt5 does not search, after
// which Qt5 apps silently fall back while Qt6 apps look correct. A search would
// find the plugin and report OK on exactly that bug.
func checkQtenginePlugin() []checkResult {
url := doctorDocsURL + "#environment-variables"
pluginRoots := qtPluginRoots()
// qmake/qtpaths ship in dev packages most users lack — never warn on a Qt
// that could not be interrogated, and never claim it is fine either.
if len(pluginRoots) == 0 {
return []checkResult{{
catEnvironment, "qtengine plugin", statusInfo, "Cannot verify (no qmake/qtpaths)",
"Install a Qt development package to let dms doctor check the plugin against the path Qt actually searches.",
url,
}}
}
// Qt also searches QT_PLUGIN_PATH, so a plugin found there is loadable.
envRoots := filepath.SplitList(os.Getenv("QT_PLUGIN_PATH"))
var results []checkResult
for _, major := range slices.Sorted(maps.Keys(pluginRoots)) {
// qtengine only ships Qt5 and Qt6 plugins.
if major != "5" && major != "6" {
continue
}
root := pluginRoots[major]
name := fmt.Sprintf("qtengine plugin (Qt%s)", major)
plugin := qtenginePluginPath(append([]string{root}, envRoots...), major)
if plugin == "" {
expected := qtenginePluginFile(root, major)
results = append(results, checkResult{
catEnvironment, name, statusWarn, fmt.Sprintf("Not found in Qt%s plugin path", major),
fmt.Sprintf("Expected %s — Qt%s apps will silently fall back to an unthemed palette. The plugin is likely installed under a prefix Qt%s does not search.", expected, major, major),
url,
})
continue
}
results = append(results, checkResult{catEnvironment, name, statusOK, "Installed", plugin, url})
}
return results
}
// checkQtPlatformThemePlugin validates one of the QT_QPA_PLATFORMTHEME
// variables beyond mere presence: the value must name a platform theme whose
// plugin Qt can actually load. A wrong value fails silently at run time — Qt
// apps fall back to a built-in theme, icon lookups return nothing, and
// Quickshell tray menus draw a placeholder for every icon an app publishes.
func checkQtPlatformThemePlugin(varName string) []checkResult {
value := os.Getenv(varName)
// Unset is reported by checkEnvVar; qtengine has its own plugin check.
if value == "" || value == "qtengine" {
return nil
}
url := doctorDocsURL + "#environment-variables"
// qt6ct-kde is the AUR package name, not the theme value: with it set,
// Qt finds no platform theme at all and every Qt app falls back silently.
if value == "qt6ct-kde" {
return []checkResult{{
catEnvironment, value, statusWarn, "Package name, not a theme",
fmt.Sprintf("qt6ct-kde is the AUR package name; the theme value Qt expects is qt6ct. Qt apps get no platform theme, so palettes stay unstyled and icon lookups fail. Set %s=qt6ct, then restart the shell.", varName),
url,
}}
}
// DMS themes Qt6 through these variables; qt5ct is the Qt5 counterpart.
major := "6"
if value == "qt5ct" {
major = "5"
}
files := qtPlatformThemePluginFiles(value)
name := fmt.Sprintf("%s plugin (Qt%s)", value, major)
root := qtPluginRoots()[major]
// qmake/qtpaths ship in dev packages most users lack — never warn on a Qt
// that could not be interrogated, and never claim it is fine either.
if root == "" {
return []checkResult{{
catEnvironment, name, statusInfo, "Cannot verify (no qmake/qtpaths)",
"Install a Qt development package to let dms doctor check the plugin against the path Qt actually searches.",
url,
}}
}
// Qt also searches QT_PLUGIN_PATH, so a plugin found there is loadable.
plugin := qtPluginPath(append([]string{root}, filepath.SplitList(os.Getenv("QT_PLUGIN_PATH"))...), files...)
if plugin == "" {
// plasma-integration ships the kde theme under a file name that has
// changed across versions; a miss says nothing reliable about the
// install, so degrade to info instead of warning at a working setup.
if value == "kde" {
return []checkResult{{
catEnvironment, name, statusInfo, "Cannot verify (plasma-integration)",
"The kde theme comes from plasma-integration, whose plugin file name varies across versions. Verify visually that Qt apps follow your theme.",
url,
}}
}
expected := filepath.Join(root, "platformthemes", files[0])
return []checkResult{{
catEnvironment, name, statusWarn, fmt.Sprintf("Not found in Qt%s plugin path", major),
fmt.Sprintf("Expected %s — Qt apps silently fall back to a built-in theme, breaking icon lookups and palettes. Install the package providing it, or correct %s.", expected, varName),
url,
}}
}
return []checkResult{{catEnvironment, name, statusOK, "Installed", plugin, url}}
}
// qtPlatformThemePluginFiles lists the plugin file names that can provide a
// QT_QPA_PLATFORMTHEME value, most conventional first. Qt resolves the value
// through plugin metadata keys rather than file names, so these are the names
// each integration is known to ship; unmapped values fall back to Qt's
// libq<value> naming convention.
func qtPlatformThemePluginFiles(value string) []string {
if files, ok := map[string][]string{
"qt6ct": {"libqt6ct.so"},
"qt5ct": {"libqt5ct.so"},
"gtk3": {"libqgtk3.so"},
"kde": {"libqkdeplatformtheme.so"},
}[value]; ok {
return files
}
return []string{"libq" + value + ".so", "lib" + value + ".so"}
}
// qtPluginRoots asks every available Qt build tool where its Qt looks for
// plugins, keyed by Qt major. A binary's name does not reliably say which Qt
// it belongs to — plain "qmake" is Qt5 on Arch and Qt6 elsewhere — so each one
// is asked for QT_VERSION and the major is taken from the answer.
func qtPluginRoots() map[string]string {
pluginRoots := make(map[string]string)
for _, q := range qtQueryBinaries {
if _, err := exec.LookPath(q.bin); err != nil {
continue
}
major, _, _ := strings.Cut(qtQuery(q.bin, q.flag, "QT_VERSION"), ".")
if major == "" || pluginRoots[major] != "" {
continue
}
if root := qtQuery(q.bin, q.flag, "QT_INSTALL_PLUGINS"); root != "" {
pluginRoots[major] = root
}
}
return pluginRoots
}
// qtPluginPath returns the first of roots that holds any of files under
// platformthemes/, or "" if none does.
func qtPluginPath(roots []string, files ...string) string {
for _, root := range roots {
if root == "" {
continue
}
for _, file := range files {
path := filepath.Join(root, "platformthemes", file)
if _, err := os.Stat(path); err == nil {
return path
}
}
}
return ""
}
// qtenginePluginPath returns the first of roots that holds the qtengine platform
// theme plugin for the given Qt major, or "" if none does.
func qtenginePluginPath(roots []string, major string) string {
return qtPluginPath(roots, fmt.Sprintf("libqt%sengine-plugin.so", major))
}
func qtenginePluginFile(root, major string) string {
return filepath.Join(root, "platformthemes", fmt.Sprintf("libqt%sengine-plugin.so", major))
}
// qtQuery asks a Qt build tool for a single QLibraryInfo property.
func qtQuery(bin, flag, key string) string {
output, err := exec.Command(bin, flag, key).Output()
if err != nil {
return ""
}
return qtQueryValue(string(output), key)
}
// qtQueryValue extracts one property from a Qt query. Most tools print the bare
// value, but some ignore the requested property and dump every one as
// "KEY:value" lines; taking the whole output there would yield a bogus path.
func qtQueryValue(output, key string) string {
lines := strings.Split(strings.TrimSpace(output), "\n")
for _, line := range lines {
if value, found := strings.CutPrefix(strings.TrimSpace(line), key+":"); found {
return strings.TrimSpace(value)
}
}
if len(lines) == 1 {
return strings.TrimSpace(lines[0])
}
return ""
}
func checkXDGMenuFile(prefix string) bool {
menuPath := fmt.Sprintf("/etc/xdg/menus/%sapplications.menu", prefix)
_, err := os.Stat(menuPath)
@@ -383,9 +618,8 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
}
qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures)
qsDetails := ""
if doctorVerbose && qsPath != "" {
qsVersion, qsStatus, qsPath, qsDetails := getQuickshellVersionInfo(qsMissingFeatures)
if doctorVerbose && qsPath != "" && qsDetails == "" {
qsDetails = qsPath
}
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
@@ -420,30 +654,43 @@ func getDMSShellVersion() (version, path string) {
return "", ""
}
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string, string) {
if !utils.CommandExists("qs") {
return "Not installed", statusError, ""
return "Not installed", statusError, "", ""
}
qsPath, _ := exec.LookPath("qs")
output, err := exec.Command("qs", "--version").Output()
if err != nil {
return "Installed (version check failed)", statusWarn, qsPath
details := "Run 'qs --version' to inspect the failure."
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
details = quickshellVersionFailureDetails(string(exitErr.Stderr))
}
return "Installed (version check failed)", statusWarn, qsPath, details
}
fullVersion := strings.TrimSpace(string(output))
if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 {
if version.CompareVersions(matches[1], "0.2.0") < 0 {
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath, ""
}
if missingFeatures {
return fullVersion, statusWarn, qsPath
return fullVersion, statusWarn, qsPath, ""
}
return fullVersion, statusOK, qsPath
return fullVersion, statusOK, qsPath, ""
}
return fullVersion, statusWarn, qsPath
return fullVersion, statusWarn, qsPath, ""
}
func quickshellVersionFailureDetails(stderr string) string {
if strings.Contains(stderr, "undefined symbol:") && strings.Contains(stderr, "Qt_6_PRIVATE_API") {
return "Quickshell is incompatible with the installed Qt libraries. Rebuild or reinstall Quickshell against the current Qt version."
}
return "Run 'qs --version' to inspect the failure."
}
func checkDMSInstallation() []checkResult {
@@ -494,6 +741,8 @@ func checkWindowManagers() []checkResult {
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
{"Scroll", "scroll", "--version", scrollVersionRegex, []string{"scroll"}},
{"Aqueous", "aqueous", "-version", aqueousVersionRegex, []string{"aqueous"}},
}
var results []checkResult
@@ -526,7 +775,7 @@ func checkWindowManagers() []checkResult {
results = append(results, checkResult{
catCompositor, "Compositor", statusError,
"No supported Wayland compositor found",
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
"Install Hyprland, niri, Sway, River, Wayfire, labwc, mangowc, miracle-wm, Scroll, or Aqueous",
doctorDocsURL + "#compositor-checks",
})
}
@@ -535,11 +784,28 @@ func checkWindowManagers() []checkResult {
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
}
if os.Getenv("AQUEOUS_SOCKET") != "" {
results = append(results, checkAqueousConfigHelper())
}
results = append(results, checkCompositorBlurSupport())
return results
}
func checkAqueousConfigHelper() checkResult {
url := doctorDocsURL + "#compositor-checks"
path, err := exec.LookPath("aqueous-config")
if err != nil {
return checkResult{catCompositor, "aqueous-config", statusWarn, "Not found", "Keybind and display settings edits from the shell need the aqueous-config helper", url}
}
details := "Keybind and display settings edits from the shell"
if doctorVerbose {
details = path
}
return checkResult{catCompositor, "aqueous-config", statusOK, "Installed", details, url}
}
func checkCompositorBlurSupport() checkResult {
supported, err := blur.ProbeSupport()
if err != nil {
@@ -572,6 +838,8 @@ func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
func detectRunningWM() string {
switch {
case os.Getenv("AQUEOUS_SOCKET") != "":
return "Aqueous"
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
return "Hyprland"
case os.Getenv("NIRI_SOCKET") != "":
@@ -666,7 +934,7 @@ ShellRoot {
{"Polkit", "Authentication prompts"},
{"IdleMonitor", "Idle detection"},
{"IdleInhibitor", "Prevent idle/sleep"},
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
{"ShortcutInhibitor", "Allow shortcut management (niri, Aqueous)"},
{"BackgroundBlur", "Background blur API support in Quickshell"},
}
@@ -789,17 +1057,15 @@ func findQtPluginDirs() []string {
}
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
for dir := range strings.SplitSeq(envPath, ":") {
addDir(dir)
}
for _, dir := range filepath.SplitList(os.Getenv("QT_PLUGIN_PATH")) {
addDir(dir)
}
// Try qtpaths
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
addDir(strings.TrimSpace(string(output)))
for _, q := range qtQueryBinaries {
if _, err := exec.LookPath(q.bin); err != nil {
continue
}
addDir(qtQuery(q.bin, q.flag, "QT_INSTALL_PLUGINS"))
}
// Fallback: common distro paths
@@ -923,9 +1189,62 @@ func checkOptionalDependencies() []checkResult {
}
}
results = append(results, checkAdwGtk3())
return results
}
// Mirrors the search in quickshell/scripts/gtk.sh: user theme dirs, then XDG_DATA_DIRS.
func checkAdwGtk3() checkResult {
optionalFeaturesURL := doctorDocsURL + "#optional-features"
home, _ := os.UserHomeDir()
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = filepath.Join(home, ".local", "share")
}
userRoots := []string{filepath.Join(dataHome, "themes"), filepath.Join(home, ".themes")}
dataDirs := os.Getenv("XDG_DATA_DIRS")
if dataDirs == "" {
dataDirs = "/usr/local/share:/usr/share"
}
var systemRoots []string
for _, d := range strings.Split(dataDirs, ":") {
if d != "" {
systemRoots = append(systemRoots, filepath.Join(d, "themes"))
}
}
locate := func(roots []string, variant string) bool {
for _, root := range roots {
if info, err := os.Stat(filepath.Join(root, "adw-gtk3"+variant, "gtk-3.0")); err == nil && info.IsDir() {
return true
}
}
return false
}
userLight := locate(userRoots, "")
userDark := locate(userRoots, "-dark")
sysLight := locate(systemRoots, "")
sysDark := locate(systemRoots, "-dark")
switch {
case userLight && userDark:
return checkResult{catOptionalFeatures, "adw-gtk3", statusOK, "Installed", "GTK3 dynamic theming (user copy)", optionalFeaturesURL}
case sysLight && sysDark:
return checkResult{catOptionalFeatures, "adw-gtk3", statusOK, "Installed", "GTK3 dynamic theming (system copy, applied on next theme apply)", optionalFeaturesURL}
case userLight || userDark || sysLight || sysDark:
missing := "adw-gtk3-dark"
if userDark || sysDark {
missing = "adw-gtk3"
}
return checkResult{catOptionalFeatures, "adw-gtk3", statusWarn, "Missing " + missing, "GTK3 dynamic theming needs both variants", optionalFeaturesURL}
default:
return checkResult{catOptionalFeatures, "adw-gtk3", statusInfo, "Not installed", "GTK3 dynamic theming; without it the global CSS override applies", optionalFeaturesURL}
}
}
func checkConfigurationFiles() []checkResult {
configDir, _ := os.UserConfigDir()
cacheDir, _ := os.UserCacheDir()
@@ -953,6 +1272,11 @@ func checkConfigurationFiles() []checkResult {
status = statusWarn
message += " (read-only)"
}
// The shell falls back to defaults on a parse failure, so a broken file must not report ok.
if data, readErr := os.ReadFile(cf.path); readErr == nil && len(data) > 0 && !json.Valid(data) {
status = statusError
message = "Invalid JSON - the shell ignores this file and runs on defaults"
}
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
}
return results
@@ -985,6 +1309,13 @@ func checkSystemdServices() []checkResult {
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
}
if dmsState.exists && dmsState.enabled == "enabled" {
gsState := getServiceState("graphical-session.target", true)
if gsState.exists && gsState.active != "active" {
results = append(results, checkResult{catServices, "graphical-session.target", statusWarn, "Inactive", "Compositor session never activated it, so dms.service cannot autostart. On Hyprland, hyprland-session.target must exist and be started by the compositor.", doctorDocsURL + "#services"})
}
}
greetdState := getServiceState("greetd", false)
switch {
case greetdState.exists:
@@ -1222,8 +1553,8 @@ func fontInFcList(name, cacheLower string) bool {
if target == "" {
return false
}
for _, line := range strings.Split(cacheLower, "\n") {
for _, fam := range strings.Split(strings.TrimSpace(line), ",") {
for line := range strings.SplitSeq(cacheLower, "\n") {
for fam := range strings.SplitSeq(strings.TrimSpace(line), ",") {
if strings.TrimSpace(fam) == target {
return true
}
+237
View File
@@ -0,0 +1,237 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestQuickshellVersionFailureDetails(t *testing.T) {
tests := []struct {
name string
stderr string
want string
}{
{
name: "Qt private API symbol mismatch",
stderr: "qs: symbol lookup error: qs: undefined symbol: _ZN23QUntypedPropertyBindingC1EP23QPropertyBindingPrivate, version Qt_6_PRIVATE_API",
want: "Quickshell is incompatible with the installed Qt libraries. Rebuild or reinstall Quickshell against the current Qt version.",
},
{
name: "other failure",
stderr: "qs: unsupported option --version",
want: "Run 'qs --version' to inspect the failure.",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := quickshellVersionFailureDetails(tc.stderr); got != tc.want {
t.Fatalf("quickshellVersionFailureDetails(%q) = %q, want %q", tc.stderr, got, tc.want)
}
})
}
}
// Parsing a full property dump as a bare value would yield a bogus plugin root,
// making the qtengine check warn at a user whose install is fine.
func TestQtQueryValue(t *testing.T) {
const dumpAll = `QT_INSTALL_PREFIX:/usr
QT_INSTALL_PLUGINS:/usr/lib/qt/plugins
QT_VERSION:5.15.19`
tests := []struct {
name string
output string
key string
want string
}{
{
name: "bare value as qmake prints it",
output: "/usr/lib/qt6/plugins\n",
key: "QT_INSTALL_PLUGINS",
want: "/usr/lib/qt6/plugins",
},
{
name: "key:value line among a full property dump",
output: dumpAll,
key: "QT_INSTALL_PLUGINS",
want: "/usr/lib/qt/plugins",
},
{
name: "a different key from the same dump",
output: dumpAll,
key: "QT_VERSION",
want: "5.15.19",
},
{
name: "key absent from a multi line dump yields nothing",
output: dumpAll,
key: "QT_INSTALL_LIBEXECS",
want: "",
},
{
name: "empty output yields nothing",
output: "",
key: "QT_INSTALL_PLUGINS",
want: "",
},
{
name: "surrounding whitespace is trimmed",
output: " QT_INSTALL_PLUGINS: /usr/lib/qt/plugins \n",
key: "QT_INSTALL_PLUGINS",
want: "/usr/lib/qt/plugins",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := qtQueryValue(tc.output, tc.key); got != tc.want {
t.Fatalf("qtQueryValue(%q, %q) = %q, want %q", tc.output, tc.key, got, tc.want)
}
})
}
}
// stubQtQueryTool writes a fake Qt build tool answering QT_VERSION and
// QT_INSTALL_PLUGINS from the given values, and points PATH at its directory
// alone so the real toolchain cannot interfere.
func stubQtQueryTool(t *testing.T, bin, version, plugins string) string {
t.Helper()
dir := t.TempDir()
script := "#!/bin/sh\ncase \"$2\" in\n QT_VERSION) echo " + version +
" ;;\n QT_INSTALL_PLUGINS) echo " + plugins + " ;;\nesac\n"
if err := os.WriteFile(filepath.Join(dir, bin), []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir)
return dir
}
func writePlugin(t *testing.T, pluginsDir, file string) {
t.Helper()
if err := os.MkdirAll(filepath.Join(pluginsDir, "platformthemes"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(pluginsDir, "platformthemes", file), nil, 0o644); err != nil {
t.Fatal(err)
}
}
// qtenginePluginFile("", major) is a path with a directory component, and
// qtPluginPath joins platformthemes/ again — a regression here silently warns
// at every correctly installed system, so pin the full path down.
func TestQtenginePluginPath(t *testing.T) {
root := t.TempDir()
if got := qtenginePluginPath([]string{root}, "6"); got != "" {
t.Fatalf("missing plugin should yield an empty path, got %q", got)
}
want := filepath.Join(root, "platformthemes", "libqt6engine-plugin.so")
if err := os.MkdirAll(filepath.Join(root, "platformthemes"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(want, nil, 0o644); err != nil {
t.Fatal(err)
}
if got := qtenginePluginPath([]string{root}, "6"); got != want {
t.Fatalf("qtenginePluginPath = %q, want %q", got, want)
}
}
func TestCheckQtPlatformThemePlugin(t *testing.T) {
t.Run("qt6ct-kde is flagged as a package name, not a theme", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct-kde")
t.Setenv("PATH", t.TempDir()) // no Qt tools reachable
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 {
t.Fatalf("got %d results, want 1: %+v", len(results), results)
}
if results[0].status != statusWarn {
t.Fatalf("status = %q, want warn", results[0].status)
}
if !strings.Contains(results[0].details, "package name") || !strings.Contains(results[0].details, "qt6ct") {
t.Fatalf("details should name the package/value mixup, got %q", results[0].details)
}
})
t.Run("installed plugin reports OK", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
plugins := t.TempDir()
writePlugin(t, plugins, "libqt6ct.so")
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusOK {
t.Fatalf("got %+v, want one OK result", results)
}
if !strings.Contains(results[0].details, "libqt6ct.so") {
t.Fatalf("details should point at the plugin, got %q", results[0].details)
}
})
t.Run("missing plugin warns with the expected path", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
plugins := t.TempDir()
stubQtQueryTool(t, "qtpaths6", "6.11.2", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusWarn {
t.Fatalf("got %+v, want one warn result", results)
}
want := filepath.Join(plugins, "platformthemes", "libqt6ct.so")
if !strings.Contains(results[0].details, want) {
t.Fatalf("details should contain expected path %s, got %q", want, results[0].details)
}
})
t.Run("no Qt build tools degrades to info", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
t.Setenv("PATH", t.TempDir())
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusInfo {
t.Fatalf("got %+v, want one info result", results)
}
})
t.Run("unset and qtengine are left to their own checks", func(t *testing.T) {
t.Setenv("PATH", t.TempDir())
t.Setenv("QT_QPA_PLATFORMTHEME", "")
if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil {
t.Fatalf("unset should yield no results, got %+v", results)
}
t.Setenv("QT_QPA_PLATFORMTHEME", "qtengine")
if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil {
t.Fatalf("qtengine should yield no results, got %+v", results)
}
})
t.Run("qt6 variable is not satisfied by a Qt5 tool alone", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME_QT6", "qt6ct")
plugins := t.TempDir()
stubQtQueryTool(t, "qmake-qt5", "5.15.19", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME_QT6")
if len(results) != 1 || results[0].status != statusInfo {
t.Fatalf("got %+v, want one info result", results)
}
})
t.Run("kde plugin miss degrades to info", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "kde")
plugins := t.TempDir()
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusInfo {
t.Fatalf("got %+v, want one info result", results)
}
})
t.Run("gtk3 resolves through the libq convention", func(t *testing.T) {
t.Setenv("QT_QPA_PLATFORMTHEME", "gtk3")
plugins := t.TempDir()
writePlugin(t, plugins, "libqgtk3.so")
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
if len(results) != 1 || results[0].status != statusOK {
t.Fatalf("got %+v, want one OK result", results)
}
})
}
+34 -47
View File
@@ -3,19 +3,19 @@ package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
"github.com/spf13/cobra"
)
var dlOutput string
var dlUserAgent string
var dlHeaders []string
var dlTimeout int
var dlConnectTimeout int
var dlIPv4Only bool
var dlCmd = &cobra.Command{
@@ -33,65 +33,52 @@ var dlCmd = &cobra.Command{
func init() {
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
dlCmd.Flags().StringArrayVarP(&dlHeaders, "header", "H", nil, "Extra request header as 'Name: value' (repeatable)")
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Total request timeout in seconds")
dlCmd.Flags().IntVar(&dlConnectTimeout, "connect-timeout", 5, "Connection timeout in seconds")
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
}
func runDownload(url string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("invalid request: %w", err)
func parseHeaders(raw []string) (map[string]string, error) {
if len(raw) == 0 {
return nil, nil
}
switch {
case dlUserAgent != "":
req.Header.Set("User-Agent", dlUserAgent)
default:
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
}
dialer := &net.Dialer{Timeout: 5 * time.Second}
transport := &http.Transport{DialContext: dialer.DialContext}
if dlIPv4Only {
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp4", addr)
headers := make(map[string]string, len(raw))
for _, entry := range raw {
name, value, found := strings.Cut(entry, ":")
if !found {
return nil, fmt.Errorf("invalid header %q: expected 'Name: value'", entry)
}
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("invalid header %q: empty name", entry)
}
headers[name] = strings.TrimSpace(value)
}
client := &http.Client{Transport: transport}
return headers, nil
}
resp, err := client.Do(req)
func runDownload(url string) error {
headers, err := parseHeaders(dlHeaders)
if err != nil {
return fmt.Errorf("download failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
if dlOutput == "" {
_, err = io.Copy(os.Stdout, resp.Body)
return err
}
if dir := filepath.Dir(dlOutput); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("mkdir failed: %w", err)
}
opts := netfetch.Options{
Headers: headers,
UserAgent: dlUserAgent,
Timeout: time.Duration(dlTimeout) * time.Second,
ConnectTimeout: time.Duration(dlConnectTimeout) * time.Second,
IPv4Only: dlIPv4Only,
}
f, err := os.Create(dlOutput)
if err != nil {
return fmt.Errorf("create failed: %w", err)
if dlOutput == "" {
return netfetch.ToWriter(context.Background(), url, opts, os.Stdout)
}
defer f.Close()
if _, err := io.Copy(f, resp.Body); err != nil {
os.Remove(dlOutput)
return fmt.Errorf("write failed: %w", err)
if err := netfetch.ToFile(context.Background(), url, opts, dlOutput); err != nil {
return err
}
fmt.Println(dlOutput)
+4 -21
View File
@@ -16,6 +16,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
@@ -412,27 +413,11 @@ func updateDMSBinary() error {
}
fmt.Println("Fetching latest release version...")
cmd := exec.Command("curl", "-s", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest")
output, err := cmd.Output()
version, err := netfetch.LatestReleaseTag(context.Background())
if err != nil {
return fmt.Errorf("failed to fetch latest release: %w", err)
}
version := ""
for line := range strings.SplitSeq(string(output), "\n") {
if strings.Contains(line, "\"tag_name\"") {
parts := strings.Split(line, "\"")
if len(parts) >= 4 {
version = parts[3]
break
}
}
}
if version == "" {
return fmt.Errorf("could not determine latest version")
}
fmt.Printf("Latest version: %s\n", version)
tempDir, err := os.MkdirTemp("", "dms-update-*")
@@ -448,14 +433,12 @@ func updateDMSBinary() error {
checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
fmt.Println("Downloading dms binary...")
downloadCmd := exec.Command("curl", "-L", binaryURL, "-o", binaryPath)
if err := downloadCmd.Run(); err != nil {
if err := netfetch.ToFile(context.Background(), binaryURL, netfetch.Options{Timeout: 5 * time.Minute}, binaryPath); err != nil {
return fmt.Errorf("failed to download binary: %w", err)
}
fmt.Println("Downloading checksum...")
downloadCmd = exec.Command("curl", "-L", checksumURL, "-o", checksumPath)
if err := downloadCmd.Run(); err != nil {
if err := netfetch.ToFile(context.Background(), checksumURL, netfetch.Options{Timeout: 30 * time.Second}, checksumPath); err != nil {
return fmt.Errorf("failed to download checksum: %w", err)
}
+1 -1
View File
@@ -62,7 +62,7 @@ func greeterPackageInstallHint() string {
case distros.FamilyFedora:
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
case distros.FamilyArch:
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
return "Install from AUR with 'paru -S greetd-dms-greeter-bin' or 'yay -S greetd-dms-greeter-bin'"
case distros.FamilyVoid:
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://void.danklinux.com/dms/current' | sudo tee /etc/xbps.d/dms.conf)"
default:
+94 -79
View File
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -46,7 +47,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,
}
var keybindsRemoveCmd = &cobra.Command{
@@ -54,7 +55,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,
}
var keybindsResetCmd = &cobra.Command{
@@ -62,10 +63,14 @@ 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,
}
func init() {
keybindsCmd.PersistentFlags().String("expected-generation", "", "Configuration generation observed when editing Aqueous bindings")
for _, command := range []*cobra.Command{keybindsSetCmd, keybindsRemoveCmd, keybindsResetCmd} {
command.Flags().Bool("json", false, "Return structured mutation results, including errors")
}
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
@@ -93,6 +98,9 @@ func initializeProviders() {
registry := keybinds.GetDefaultRegistry()
hyprlandProvider := providers.NewHyprlandProvider("")
if err := registry.Register(providers.NewAqueousProvider()); err != nil {
log.Warnf("Failed to register Aqueous provider: %v", err)
}
if err := registry.Register(hyprlandProvider); err != nil {
log.Warnf("Failed to register Hyprland provider: %v", err)
}
@@ -206,85 +214,92 @@ func runKeybindsShow(cmd *cobra.Command, args []string) {
printCheatSheet(provider)
}
func getWritableProvider(name string) keybinds.WritableProvider {
provider, err := keybinds.GetDefaultRegistry().Get(name)
func keybindEditFailure(err error) map[string]any {
code := "command_failed"
var helperError *providers.AqueousError
if errors.As(err, &helperError) {
code = helperError.Code
}
return map[string]any{"success": false, "code": code, "message": err.Error()}
}
func runKeybindsEdit(cmd *cobra.Command, args []string) {
result, err := editKeybind(cmd, args)
if err == nil {
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(result)
return
}
structured, _ := cmd.Flags().GetBool("json")
if !structured {
log.Fatalf("Failed to save keybind: %v", err)
return
}
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(keybindEditFailure(err))
os.Exit(1)
}
func editKeybind(cmd *cobra.Command, args []string) (map[string]any, error) {
provider, err := keybinds.GetDefaultRegistry().Get(args[0])
if err != nil {
log.Fatalf("Error: %v", err)
return nil, err
}
key := args[1]
if aqueous, ok := provider.(*providers.AqueousProvider); ok {
generation, _ := cmd.Flags().GetString("expected-generation")
edit := providers.AqueousBindEdit{Generation: generation, Key: key, Remove: cmd.Name() != "set"}
if !edit.Remove {
for _, flag := range []string{"desc", "allow-when-locked", "cooldown-ms", "no-repeat", "no-inhibiting", "flags"} {
if cmd.Flags().Changed(flag) {
return nil, fmt.Errorf("aqueous does not support --%s in this provider", flag)
}
}
edit.Action = args[2]
edit.OriginalKey, _ = cmd.Flags().GetString("replace-key")
}
result, err := aqueous.Edit(cmd.Context(), edit)
if err != nil {
return nil, err
}
return map[string]any{"success": true, "code": "applied", "generation": result.String("generation"), "key": key}, nil
}
writable, ok := provider.(keybinds.WritableProvider)
if !ok {
log.Fatalf("Provider %s does not support writing keybinds", name)
return nil, fmt.Errorf("provider %s does not support writing keybinds", args[0])
}
return writable
}
func runKeybindsSet(cmd *cobra.Command, args []string) {
providerName, key, action := args[0], args[1], args[2]
writable := getWritableProvider(providerName)
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
_ = writable.RemoveBind(replaceKey)
}
options := make(map[string]any)
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
options["allow-when-locked"] = true
}
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
options["cooldown-ms"] = v
}
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false
}
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
options["allow-inhibiting"] = false
}
if v, _ := cmd.Flags().GetString("flags"); v != "" {
options["flags"] = v
}
desc, _ := cmd.Flags().GetString("desc")
if err := writable.SetBind(key, action, desc, options); err != nil {
log.Fatalf("Error setting keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"action": action,
"path": writable.GetOverridePath(),
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsRemove(_ *cobra.Command, args []string) {
providerName, key := args[0], args[1]
writable := getWritableProvider(providerName)
if err := writable.RemoveBind(key); err != nil {
log.Fatalf("Error removing keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"removed": true,
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsReset(_ *cobra.Command, args []string) {
providerName, key := args[0], args[1]
writable := getWritableProvider(providerName)
if err := writable.ResetBind(key); err != nil {
log.Fatalf("Error resetting keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"reset": true,
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
result := map[string]any{"success": true, "key": key}
switch cmd.Name() {
case "set":
options := make(map[string]any)
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
options["allow-when-locked"] = true
}
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
options["cooldown-ms"] = v
}
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false
}
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
options["allow-inhibiting"] = false
}
if v, _ := cmd.Flags().GetString("flags"); v != "" {
options["flags"] = v
}
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
_ = writable.RemoveBind(replaceKey)
}
desc, _ := cmd.Flags().GetString("desc")
err = writable.SetBind(key, args[2], desc, options)
result["action"] = args[2]
result["path"] = writable.GetOverridePath()
case "remove":
err = writable.RemoveBind(key)
result["removed"] = true
case "reset":
err = writable.ResetBind(key)
result["reset"] = true
default:
return nil, fmt.Errorf("unsupported keybind operation: %s", cmd.Name())
}
return result, err
}
+25
View File
@@ -0,0 +1,25 @@
package main
import (
"errors"
"fmt"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
)
func TestKeybindEditFailure(t *testing.T) {
for _, tc := range []struct {
err error
code string
}{
{&providers.AqueousError{Code: "external_change", Message: "changed"}, "external_change"},
{fmt.Errorf("helper: %w", &providers.AqueousError{Code: "read_only", Message: "denied"}), "read_only"},
{errors.New("transport failed"), "command_failed"},
} {
result := keybindEditFailure(tc.err)
if result["success"] != false || result["code"] != tc.code || result["message"] != tc.err.Error() {
t.Fatalf("unexpected failure result: %#v", result)
}
}
}
+27 -3
View File
@@ -44,11 +44,18 @@ var matugenPreviewCmd = &cobra.Command{
Run: runMatugenPreview,
}
var matugenQtengineCmd = &cobra.Command{
Use: "qtengine",
Short: "Sync qtengine's config with the current DMS theme",
Run: runMatugenQtengine,
}
func init() {
matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd)
matugenCmd.AddCommand(matugenCheckCmd)
matugenCmd.AddCommand(matugenPreviewCmd)
matugenCmd.AddCommand(matugenQtengineCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files")
@@ -56,21 +63,25 @@ func init() {
cmd.Flags().String("config-dir", "", "User config directory")
cmd.Flags().String("kind", "image", "Source type: image or hex")
cmd.Flags().String("value", "", "Wallpaper path or hex color")
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
cmd.Flags().String("mode", "dark", "Color mode: dark, light, or smart (smart requires matugen 4.2+ and an image source)")
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type (supports scheme-smart on matugen 4.2+)")
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
cmd.Flags().String("source-mode", "", "Source color selection: dominant, colorful, darkness, lightness, saturation, less-saturation, value")
}
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
matugenPreviewCmd.Flags().String("image", "", "Wallpaper image used to resolve the scheme-smart preview")
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
matugenQtengineCmd.Flags().String("config-dir", "", "User config directory")
matugenQtengineCmd.Flags().String("icon-theme", "", "Icon theme name")
}
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
@@ -88,6 +99,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
contrast, _ := cmd.Flags().GetFloat64("contrast")
sourceMode, _ := cmd.Flags().GetString("source-mode")
return matugen.Options{
StateDir: stateDir,
@@ -104,6 +116,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
SyncModeWithPortal: syncModeWithPortal,
TerminalsAlwaysDark: terminalsAlwaysDark,
SkipTemplates: skipTemplates,
SourceMode: sourceMode,
}
}
@@ -141,6 +154,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"skipTemplates": opts.SkipTemplates,
"contrast": opts.Contrast,
"sourceMode": opts.SourceMode,
"wait": wait,
},
}
@@ -212,8 +226,9 @@ func runMatugenCheck(cmd *cobra.Command, args []string) {
func runMatugenPreview(cmd *cobra.Command, args []string) {
sourceColor, _ := cmd.Flags().GetString("source-color")
imagePath, _ := cmd.Flags().GetString("image")
contrast, _ := cmd.Flags().GetFloat64("contrast")
previews, err := matugen.PreviewSchemes(sourceColor, contrast)
previews, err := matugen.PreviewSchemes(sourceColor, contrast, imagePath)
if err != nil {
log.Fatalf("Failed to generate Matugen previews: %v", err)
}
@@ -223,3 +238,12 @@ func runMatugenPreview(cmd *cobra.Command, args []string) {
}
fmt.Println(string(data))
}
func runMatugenQtengine(cmd *cobra.Command, args []string) {
configDir, _ := cmd.Flags().GetString("config-dir")
iconTheme, _ := cmd.Flags().GetString("icon-theme")
if err := matugen.SyncQtengineConfigAt(configDir, iconTheme); err != nil {
log.Fatalf("Failed to sync qtengine config: %v", err)
}
}
+14
View File
@@ -0,0 +1,14 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMatugenQtengineCommandAcceptsConfigDir(t *testing.T) {
flag := matugenQtengineCmd.Flags().Lookup("config-dir")
require.NotNil(t, flag)
assert.Equal(t, "", flag.DefValue)
}
+21 -1
View File
@@ -5,6 +5,7 @@ import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
@@ -61,8 +62,27 @@ func runNotify(cmd *cobra.Command, args []string) {
Timeout: int32(notifyTimeout),
}
if err := notify.Send(n); err != nil {
id, err := notify.Send(n)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if n.FilePath == "" {
return
}
watchNotificationAction(id, n.FilePath)
}
func watchNotificationAction(id uint32, path string) {
if id == 0 {
return
}
resp, ok := tryServerRequest(models.Request{
Method: "notify.watchAction",
Params: map[string]any{"id": id, "path": path},
})
if ok && resp.Error == "" {
return
}
notify.SpawnActionListener(id, path)
}
+98 -35
View File
@@ -2,21 +2,25 @@ package main
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
"github.com/spf13/cobra"
)
var (
ssOutputName string
ssSeat string
ssCursor string
ssFormat string
ssQuality int
@@ -29,11 +33,16 @@ var (
ssReset bool
ssStdout bool
ssJSON bool
ssGeometry bool
ssAllowMulti bool
ssHUD string
)
type screenshotMetadata struct {
Status string `json:"status"`
Path string `json:"path,omitempty"`
X *int `json:"x,omitempty"`
Y *int `json:"y,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Scale float64 `json:"scale,omitempty"`
@@ -51,7 +60,7 @@ Modes:
full - Capture the focused output
all - Capture all outputs combined
output - Capture a specific output by name
window - Capture the focused window (Hyprland/Mango/niri)
window - Capture the focused window (Hyprland/Mango/niri/Aqueous)
last - Capture the last selected region
scroll - Select a region, then scroll to capture a stitched tall image
@@ -65,7 +74,7 @@ Examples:
dms screenshot full # Full screen of focused output
dms screenshot all # All screens combined
dms screenshot output -o DP-1 # Specific output
dms screenshot window # Focused window (Hyprland)
dms screenshot window # Focused window
dms screenshot last # Last region (pre-selected)
dms screenshot --reset # Reset last region pre-selection
dms screenshot --no-clipboard # Save file only
@@ -74,6 +83,8 @@ Examples:
dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85
dms screenshot --json # Print capture metadata as JSON
dms screenshot --allow-multiple # Skip the one-selector-at-a-time guard
dms screenshot -g # Print selected region geometry (X,Y WxH) to stdout
dms screenshot scroll # Scroll capture, Enter finishes / Esc cancels
dms screenshot scroll --interval 250`,
}
@@ -113,7 +124,7 @@ If no previous region exists, falls back to interactive selection.`,
var ssWindowCmd = &cobra.Command{
Use: "window",
Short: "Capture the focused window",
Long: `Capture the currently focused window. Supported on Hyprland, 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)
},
}
func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().StringVar(&ssSeat, "seat", "", "Seat for Aqueous capture (required with multiple seats)")
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
@@ -168,6 +180,9 @@ func init() {
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)")
@@ -187,6 +202,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig()
config.Mode = mode
config.OutputName = ssOutputName
config.Seat = ssSeat
if strings.EqualFold(ssCursor, "on") {
config.Cursor = screenshot.CursorOn
}
@@ -196,6 +212,15 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config.NoConfirm = ssNoConfirm
config.Reset = ssReset
config.Stdout = ssStdout
config.Geometry = ssGeometry
config.AllowMultiple = ssAllowMulti
config.HUD = ssHUD
if ssGeometry {
config.Clipboard = false
config.SaveFile = false
config.Notify = false
}
if ssOutputDir != "" {
config.OutputDir = ssOutputDir
@@ -224,7 +249,8 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
return config
}
// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select. Best-effort.
// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select.
// Best-effort and not awaited: qs takes ~20ms to come up, longer than the selector needs to show.
func setPopoutScreenshotMode(begin bool) {
fn := "end"
if begin {
@@ -243,7 +269,7 @@ func setPopoutScreenshotMode(begin bool) {
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
}
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
_ = exec.Command("qs", cmdArgs...).Run()
_ = exec.Command("qs", cmdArgs...).Start()
}
func writeScreenshotJSON(meta screenshotMetadata) {
@@ -275,17 +301,24 @@ func runScreenshot(config screenshot.Config) {
fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout")
os.Exit(1)
}
if config.Geometry {
if config.Stdout {
fmt.Fprintln(os.Stderr, "Error: --geometry cannot be combined with --stdout")
os.Exit(1)
}
if config.Mode == screenshot.ModeScroll {
fmt.Fprintln(os.Stderr, "Error: --geometry cannot be combined with scroll mode")
os.Exit(1)
}
}
// Short-lived process over a few tens of MB: let the heap grow instead of paying GC cycles mid-capture.
debug.SetGCPercent(-1)
debug.SetMemoryLimit(1 << 30)
// Region select needs the keyboard; drop popout grabs for its duration.
result, err := func() (*screenshot.CaptureResult, error) {
interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion || config.Mode == screenshot.ModeScroll
if interactive {
setPopoutScreenshotMode(true)
defer setPopoutScreenshotMode(false)
}
return screenshot.New(config).Run()
}()
config.SelectorHook = setPopoutScreenshotMode
result, err := screenshot.New(config).Run()
if err != nil {
exitScreenshotError("", err)
}
@@ -294,17 +327,39 @@ func runScreenshot(config screenshot.Config) {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"})
}
if config.Geometry {
os.Exit(1)
}
os.Exit(0)
}
defer result.Buffer.Close()
if config.Geometry {
if ssJSON {
x := int(result.Region.X)
y := int(result.Region.Y)
writeScreenshotJSON(screenshotMetadata{
Status: "success",
X: &x,
Y: &y,
Width: int(result.Region.Width),
Height: int(result.Region.Height),
})
} else {
fmt.Println(result.Region.GeometryString())
}
os.Exit(0)
}
if result.YInverted {
result.Buffer.FlipVertical()
if result.Buffer != nil {
defer result.Buffer.Close()
if result.YInverted {
result.Buffer.FlipVertical()
}
}
if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil {
exitScreenshotError(" writing to stdout", err)
}
return
@@ -324,7 +379,7 @@ func runScreenshot(config screenshot.Config) {
}
filePath = filepath.Join(outputDir, filename)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format, result.CICP); err != nil {
exitScreenshotError(" writing file", err)
}
if !ssJSON {
@@ -333,7 +388,7 @@ func runScreenshot(config screenshot.Config) {
}
if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil {
exitScreenshotError(" copying to clipboard", err)
}
if !ssJSON && !config.SaveFile {
@@ -358,31 +413,30 @@ func runScreenshot(config screenshot.Config) {
if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
screenshot.SendNotification(screenshot.NotifyResult{
id := screenshot.SendNotification(screenshot.NotifyResult{
FilePath: filePath,
Clipboard: config.Clipboard,
ImageData: thumbData,
Width: thumbW,
Height: thumbH,
})
watchNotificationAction(id, filePath)
}
}
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32, cicp *screenshot.CICP) error {
var mimeType string
var data bytes.Buffer
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
switch format {
case screenshot.FormatJPEG:
mimeType = "image/jpeg"
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
if err := screenshot.EncodeBufferJPEG(&data, buf, pixelFormat, quality); err != nil {
return err
}
default:
mimeType = "image/png"
if err := screenshot.EncodePNG(&data, img); err != nil {
if err := screenshot.EncodeBufferPNG(&data, buf, pixelFormat, cicp); err != nil {
return err
}
}
@@ -390,14 +444,12 @@ func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, q
return clipboard.Copy(data.Bytes(), mimeType)
}
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32, cicp *screenshot.CICP) error {
switch format {
case screenshot.FormatJPEG:
return screenshot.EncodeJPEG(os.Stdout, img, quality)
return screenshot.EncodeBufferJPEG(os.Stdout, buf, pixelFormat, quality)
default:
return screenshot.EncodePNG(os.Stdout, img)
return screenshot.EncodeBufferPNG(os.Stdout, buf, pixelFormat, cicp)
}
}
@@ -424,9 +476,12 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
data := buf.Data()
rgb := make([]byte, dstW*dstH*3)
is10Bit := screenshot.PixelFormat(pixelFormat).Is10Bit()
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888),
uint32(screenshot.FormatABGR2101010), uint32(screenshot.FormatXBGR2101010):
swapRB = false
default:
swapRB = true
@@ -447,11 +502,19 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
if si+3 >= len(data) {
continue
}
if swapRB {
switch {
case is10Bit:
v := binary.LittleEndian.Uint32(data[si:])
c0, c1, c2 := uint8(v>>2), uint8(v>>12), uint8(v>>22)
if swapRB {
c0, c2 = c2, c0
}
rgb[di+0], rgb[di+1], rgb[di+2] = c0, c1, c2
case swapRB:
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
} else {
default:
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
+1 -1
View File
@@ -120,7 +120,7 @@ func showSession(id string) (sessionInfo, error) {
return sessionInfo{}, err
}
fields := map[string]string{}
for _, line := range strings.Split(string(out), "\n") {
for line := range strings.SplitSeq(string(out), "\n") {
idx := strings.IndexByte(line, '=')
if idx <= 0 {
continue
+53 -52
View File
@@ -356,22 +356,18 @@ func runSetup() error {
}
}()
ctx := context.Background()
var results []config.DeploymentResult
var err error
if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
} else if wmSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
if len(results) > 1 {
results = results[:1]
}
} else if terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:]
}
if wmSelected {
var result config.DeploymentResult
result, err = deployer.DeployCompositor(wm, terminal, useSystemd)
results = append(results, result)
}
if err == nil && terminalSelected {
var terminalResults []config.DeploymentResult
terminalResults, err = deployer.DeployTerminal(terminal)
results = append(results, terminalResults...)
}
close(logChan)
@@ -495,49 +491,54 @@ func promptSystemd() bool {
}
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
homeDir := os.Getenv("HOME")
willBackup := false
if wmSelected {
var configPaths []string
switch wm {
case deps.WindowManagerNiri:
configPaths = []string{filepath.Join(homeDir, ".config", "niri", "config.kdl")}
case deps.WindowManagerHyprland:
configPaths = []string{
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
}
case deps.WindowManagerMango:
configPaths = []string{
filepath.Join(homeDir, ".config", "mango", "config.conf"),
filepath.Join(homeDir, ".config", "mango", "mango.conf"),
}
}
for _, configPath := range configPaths {
if _, err := os.Stat(configPath); err == nil {
willBackup = true
break
}
}
if wmSelected && firstExistingPath(compositorPrimaryPaths(wm)) != "" {
return true
}
return terminalSelected && firstExistingPath([]string{terminalPrimaryPath(terminal)}) != ""
}
if terminalSelected {
var configPath string
switch terminal {
case deps.TerminalGhostty:
configPath = filepath.Join(homeDir, ".config", "ghostty", "config")
case deps.TerminalKitty:
configPath = filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
case deps.TerminalAlacritty:
configPath = filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
func compositorPrimaryPaths(wm deps.WindowManager) []string {
homeDir := os.Getenv("HOME")
switch wm {
case deps.WindowManagerNiri:
return []string{filepath.Join(homeDir, ".config", "niri", "config.kdl")}
case deps.WindowManagerHyprland:
return []string{
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
}
case deps.WindowManagerMango:
return []string{
filepath.Join(homeDir, ".config", "mango", "config.conf"),
filepath.Join(homeDir, ".config", "mango", "mango.conf"),
}
default:
return nil
}
}
func terminalPrimaryPath(terminal deps.Terminal) string {
homeDir := os.Getenv("HOME")
switch terminal {
case deps.TerminalGhostty:
return filepath.Join(homeDir, ".config", "ghostty", "config")
case deps.TerminalKitty:
return filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
case deps.TerminalAlacritty:
return filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
default:
return ""
}
}
if _, err := os.Stat(configPath); err == nil {
willBackup = true
func firstExistingPath(paths []string) string {
for _, path := range paths {
if path == "" {
continue
}
if _, err := os.Stat(path); err == nil {
return path
}
}
return willBackup
return ""
}
+148
View File
@@ -0,0 +1,148 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/spf13/cobra"
)
var setupHeadlessCmd = &cobra.Command{
Use: "headless",
Short: "Deploy DMS configurations without prompts",
Long: "Deploy compositor and terminal configurations non-interactively, for installers and scripts",
PersistentPreRunE: requireMutableSystemCommand,
RunE: func(cmd *cobra.Command, args []string) error {
compositor, _ := cmd.Flags().GetString("compositor")
terminal, _ := cmd.Flags().GetString("terminal")
noSystemd, _ := cmd.Flags().GetBool("no-systemd")
force, _ := cmd.Flags().GetBool("force")
skipExisting, _ := cmd.Flags().GetBool("skip-existing")
cmd.SilenceUsage = true
return runSetupHeadless(compositor, terminal, noSystemd, force, skipExisting)
},
}
func init() {
setupHeadlessCmd.Flags().String("compositor", "", "Compositor to configure: niri, hyprland, or mango")
setupHeadlessCmd.Flags().String("terminal", "", "Also deploy a terminal config: ghostty, kitty, or alacritty")
setupHeadlessCmd.Flags().Bool("no-systemd", false, "Deploy session config without systemd integration")
setupHeadlessCmd.Flags().Bool("force", false, "Overwrite existing configs (timestamped backups are created)")
setupHeadlessCmd.Flags().Bool("skip-existing", false, "Warn and skip instead of failing when a config already exists")
_ = setupHeadlessCmd.MarkFlagRequired("compositor")
setupHeadlessCmd.MarkFlagsMutuallyExclusive("force", "skip-existing")
}
func runSetupHeadless(compositor, terminal string, noSystemd, force, skipExisting bool) error {
wm, err := parseHeadlessCompositor(compositor)
if err != nil {
return err
}
term, termSelected, err := parseHeadlessTerminal(terminal)
if err != nil {
return err
}
deployCompositor := true
deployTerminal := termSelected
if !force {
if existing := firstExistingPath(compositorPrimaryPaths(wm)); existing != "" {
if !skipExisting {
return fmt.Errorf("%s config already exists: %s (use --force to overwrite with backup, or --skip-existing)", compositor, existing)
}
fmt.Printf("⚠ Skipping %s config, already exists: %s\n", compositor, existing)
deployCompositor = false
}
if deployTerminal {
if existing := firstExistingPath([]string{terminalPrimaryPath(term)}); existing != "" {
if !skipExisting {
return fmt.Errorf("%s config already exists: %s (use --force to overwrite with backup, or --skip-existing)", terminal, existing)
}
fmt.Printf("⚠ Skipping %s config, already exists: %s\n", terminal, existing)
deployTerminal = false
}
}
}
if !deployCompositor && !deployTerminal {
return nil
}
logChan := make(chan string, 100)
logDone := make(chan struct{})
go func() {
for msg := range logChan {
fmt.Println(" " + msg)
}
close(logDone)
}()
deployer := config.NewConfigDeployer(logChan)
var results []config.DeploymentResult
var deployErr error
if deployCompositor {
result, err := deployer.DeployCompositor(wm, term, headlessUseSystemd(wm, noSystemd))
results = append(results, result)
deployErr = err
}
if deployErr == nil && deployTerminal {
terminalResults, err := deployer.DeployTerminal(term)
results = append(results, terminalResults...)
deployErr = err
}
close(logChan)
<-logDone
for _, result := range results {
if !result.Deployed {
continue
}
fmt.Printf("✓ %s: %s\n", result.ConfigType, result.Path)
if result.BackupPath != "" {
fmt.Printf(" Backup: %s\n", result.BackupPath)
}
}
if deployErr != nil {
return fmt.Errorf("deployment failed: %w", deployErr)
}
return nil
}
func parseHeadlessCompositor(name string) (deps.WindowManager, error) {
switch name {
case "niri":
return deps.WindowManagerNiri, nil
case "hyprland":
return deps.WindowManagerHyprland, nil
case "mango":
return deps.WindowManagerMango, nil
default:
return 0, fmt.Errorf("unknown compositor %q (expected niri, hyprland, or mango)", name)
}
}
func parseHeadlessTerminal(name string) (deps.Terminal, bool, error) {
switch name {
case "":
return deps.TerminalGhostty, false, nil
case "ghostty":
return deps.TerminalGhostty, true, nil
case "kitty":
return deps.TerminalKitty, true, nil
case "alacritty":
return deps.TerminalAlacritty, true, nil
default:
return 0, false, fmt.Errorf("unknown terminal %q (expected ghostty, kitty, or alacritty)", name)
}
}
func headlessUseSystemd(wm deps.WindowManager, noSystemd bool) bool {
if noSystemd || wm == deps.WindowManagerMango {
return false
}
return !isVoidSetup()
}
+1 -1
View File
@@ -58,7 +58,7 @@ func init() {
systemUpdateCmd.Flags().BoolVar(&sysUpdateDry, "dry", false, "Simulate the upgrade without applying changes")
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip AUR updates")
systemUpdateCmd.Flags().StringSliceVar(&sysUpdateIgnore, "ignore", nil, "Skip specific packages (repeatable or comma-separated)")
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
+2 -1
View File
@@ -13,7 +13,7 @@ var Version = "dev"
func init() {
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
setupCmd.AddCommand(setupHeadlessCmd, setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd, pluginsLockCmd, pluginsRestoreCmd)
registryCmd.AddCommand(registryListCmd, registryAddCmd, registryRemoveCmd)
@@ -26,6 +26,7 @@ func init() {
}
func main() {
disableMemProfilingUnlessRequested()
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
+2 -1
View File
@@ -13,7 +13,7 @@ var Version = "dev"
func init() {
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
setupCmd.AddCommand(setupHeadlessCmd, setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd, pluginsLockCmd, pluginsRestoreCmd)
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(authCmd)
@@ -22,6 +22,7 @@ func init() {
}
func main() {
disableMemProfilingUnlessRequested()
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
+2 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
// maxIPCMessageSize allows room for a 50 MB clipboard entry plus JSON/base64
@@ -96,10 +97,7 @@ func tryServerRequest(req models.Request) (*models.Response[any], bool) {
}
func getServerSocketPath() string {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
runtimeDir = os.TempDir()
}
runtimeDir := utils.RuntimeDir()
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
return sessionSock
+27 -2
View File
@@ -14,6 +14,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/qsipc"
)
type ipcTargets map[string]map[string][]string
@@ -137,6 +138,9 @@ func runShellIPCCommand(args []string) {
if args[0] != "call" {
args = append([]string{"call"}, args...)
}
if tryDirectIPCCall(args) {
return
}
baseArgs, err := buildQsIPCBaseArgs()
if err != nil {
@@ -153,6 +157,27 @@ func runShellIPCCommand(args []string) {
}
}
// tryDirectIPCCall invokes the shell over its IPC socket without spawning qs.
// Any failure falls back to the qs child process path, which owns error output.
func tryDirectIPCCall(args []string) bool {
if len(args) < 3 {
return false
}
pid, ok := shellApp.SessionPID()
if !ok {
return false
}
result, isVoid, err := qsipc.Call(qsipc.SocketPathForPID(pid), args[1], args[2], args[3:])
if err != nil {
log.Debugf("Direct Quickshell IPC failed, falling back to qs: %v", err)
return false
}
if !isVoid {
fmt.Println(result)
}
return true
}
func printIPCHelp() {
fmt.Println("Usage: dms ipc call <target> <function> [args...]")
fmt.Println()
@@ -261,8 +286,8 @@ func ensureFontCache() {
}
func fontInCache(target, cache string) bool {
for _, line := range strings.Split(cache, "\n") {
for _, fam := range strings.Split(strings.TrimSpace(line), ",") {
for line := range strings.SplitSeq(cache, "\n") {
for fam := range strings.SplitSeq(strings.TrimSpace(line), ",") {
if strings.TrimSpace(fam) == target {
return true
}
+16 -9
View File
@@ -6,6 +6,7 @@ import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/gpu"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
@@ -13,15 +14,17 @@ import (
)
var shellApp = shellapp.New(shellapp.Config{
ID: "danklinux",
EnvPrefix: "DMS",
QSAppID: "com.danklinux.dms",
Version: Version,
Embedded: embeddedShell{},
Boot: bootBackend,
PreLaunch: preLaunch,
ExtraEnv: dmsExtraEnv,
OnUIExit: logStartupFailure,
ID: "danklinux",
EnvPrefix: "DMS",
QSAppID: "com.danklinux.dms",
Version: Version,
Embedded: embeddedShell{},
Boot: bootBackend,
PreLaunch: preLaunch,
ExtraEnv: dmsExtraEnv,
OnUIExit: logStartupFailure,
SessionRestartExitCode: dmsSessionRestartExitCode,
TryManagedRestart: trySystemdRestart,
})
type embeddedShell struct{}
@@ -78,5 +81,9 @@ func dmsExtraEnv(string) []string {
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
if _, set := os.LookupEnv("MALLOC_CONF"); !set {
env = append(env, "MALLOC_CONF=thp:never,narenas:4,dirty_decay_ms:3000")
}
env = append(env, gpu.EGLVendorEnv()...)
return env
}
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"fmt"
"os/exec"
"strings"
)
const dmsSystemdUnit = "dms.service"
const dmsSessionRestartExitCode = 75 // EX_TEMPFAIL
var runSystemctl = func(args ...string) error {
output, err := exec.Command("systemctl", args...).CombinedOutput()
if err == nil {
return nil
}
if detail := strings.TrimSpace(string(output)); detail != "" {
return fmt.Errorf("%s: %w", detail, err)
}
return err
}
func trySystemdRestart() (bool, error) {
if err := runSystemctl("--user", "is-active", "--quiet", dmsSystemdUnit); err != nil {
return false, nil
}
if err := runSystemctl("--user", "restart", dmsSystemdUnit); err != nil {
return true, fmt.Errorf("restarting %s: %w", dmsSystemdUnit, err)
}
return true, nil
}
+6 -6
View File
@@ -15,12 +15,12 @@ func getThemedASCII() string {
theme := tui.TerminalTheme()
logo := `
██████╗ █████╗ ███╗ ██╗██╗ ██╗
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
██║ ██║███████║██╔██╗ ██║█████╔╝
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
██████╔╝██║ ██║██║ ╚████║██║ ██╗
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
██████╗ ███╗ ███╗███████╗
██╔══██╗████╗ ████║██╔════╝
██║ ██║██╔████╔██║███████╗
██║ ██║██║╚██╔╝██║╚════██║
██████╔╝██║ ╚═╝ ██║███████║
╚═════╝ ╚═╝ ╚═╝╚══════╝`
style := lipgloss.NewStyle().
Foreground(lipgloss.Color(theme.Primary)).
+9
View File
@@ -1,10 +1,19 @@
package main
import (
"os"
"os/exec"
"runtime"
"strings"
)
func disableMemProfilingUnlessRequested() {
if os.Getenv("DMS_PPROF") != "" {
return
}
runtime.MemProfileRate = 0
}
// isReadOnlyCommand returns true if the CLI args indicate a command that is
// safe to run as root (e.g. shell completion, help).
func isReadOnlyCommand(args []string) bool {
+14 -15
View File
@@ -1,29 +1,31 @@
module github.com/AvengeMedia/DankMaterialShell/core
go 1.26.4
go 1.26.5
require (
github.com/AvengeMedia/dgop v0.2.4-0.20260808141551-f559f2f31d2a
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287
github.com/Nadim147c/material/v3 v3.1.1
github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/dlclark/regexp2/v2 v2.5.1
github.com/fsnotify/fsnotify v1.10.1
github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847
github.com/klauspost/compress v1.19.2
github.com/pilebones/go-udev v0.9.1
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.3.0
github.com/yuin/goldmark v1.8.4
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.5.0
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
golang.org/x/image v0.44.0
tailscale.com v1.100.0
tailscale.com v1.102.0
)
require (
@@ -38,7 +40,6 @@ require (
github.com/coder/websocket v1.8.15 // indirect
github.com/danielgtaylor/huma/v2 v2.39.1 // indirect
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect
github.com/dlclark/regexp2/v2 v2.5.1 // indirect
github.com/ebitengine/purego v0.10.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
@@ -70,41 +71,39 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 // indirect
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
)
require (
github.com/AvengeMedia/dankgo v0.0.0-20260812195758-5935f34d40ce
github.com/AvengeMedia/dankgo v1.6.1-0.20260908011626-07e1ef7caaea
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/ansi v0.11.8 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-alpha.4
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.1
github.com/mattn/go-isatty v0.0.24
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
github.com/mattn/go-runewidth v0.0.27 // indirect
github.com/mattn/go-runewidth v0.0.28 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xo/terminfo v1.0.0 // indirect
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0
golang.org/x/text v0.41.0
gopkg.in/yaml.v3 v3.0.1
)
+32 -39
View File
@@ -1,11 +1,13 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AvengeMedia/dankgo v0.0.0-20260812195758-5935f34d40ce h1:vNccNlfXze7Fmg9PXrsyxhfAMaifK04bsQHWUjVfe2w=
github.com/AvengeMedia/dankgo v0.0.0-20260812195758-5935f34d40ce/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
github.com/AvengeMedia/dgop v0.2.4-0.20260808141551-f559f2f31d2a h1:bGwK4G22ixHUs5NBeNy4JYMKUMMFoZtQz180OnbSFKE=
github.com/AvengeMedia/dgop v0.2.4-0.20260808141551-f559f2f31d2a/go.mod h1:2kt1u9YgHwi2npqB5ago8kwZdFrHlPGXv3PM/vwRHfY=
github.com/AvengeMedia/dankgo v1.6.1-0.20260908011626-07e1ef7caaea h1:A2IuXIzhTQwvBZiS8pxU5RmWSszMd16Oh11gUlJ9OeU=
github.com/AvengeMedia/dankgo v1.6.1-0.20260908011626-07e1ef7caaea/go.mod h1:w1W/vDrWJCLdjVI4E/mURiKUQLMCmiGiPLxZEeP5kp8=
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287 h1:wzQqP16zxfUcfFltJelKg6pTdRyiLY2l2NJLT0Par+Q=
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287/go.mod h1:lwzO5Y4BO5z0I7cLMp5A7sw/THliwGz1TNv6xoUehY4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nadim147c/material/v3 v3.1.1 h1:3ZuTPWBJWUpzzWW1rtqe28RMZXhA4f0/mg/Jk3JG94Y=
github.com/Nadim147c/material/v3 v3.1.1/go.mod h1:ThN+dRdC+CDN2qXSGMn32KqtcXwYtb/trDM82cdZI6g=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
@@ -14,10 +16,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -34,14 +34,12 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
@@ -63,12 +61,8 @@ github.com/danielgtaylor/huma/v2 v2.39.1 h1:0kwF4ltQoYZ+IU55VPy+BcGekzgF44R64daT
github.com/danielgtaylor/huma/v2 v2.39.1/go.mod h1:zcnQ38duIJ3VUHwFaBoZ6x8T+KN/mr33oyqxcj0HTug=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20=
github.com/dlclark/regexp2/v2 v2.5.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
@@ -89,8 +83,8 @@ github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ=
@@ -130,6 +124,8 @@ github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5G
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -147,8 +143,8 @@ github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsRe
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
@@ -168,8 +164,6 @@ github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -194,49 +188,47 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0 h1:CnIEL2n7Xql6Ux1k+Vu5S5ubDHCT/kxFgkKCY8FjefU=
github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM=
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk=
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
@@ -249,8 +241,10 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
@@ -260,8 +254,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
tailscale.com v1.102.0 h1:yAYrykMZWlgp7Qq2ECDrDnxeySWBhNXNMkXdDmVMyrE=
tailscale.com v1.102.0/go.mod h1:NdndXfkSklVPbRUZMAzzRuDNaR5B+YWGbAyE1HpSQZg=
+2 -7
View File
@@ -47,6 +47,7 @@ printf "%bInstalling Dankinstall %s for %s...%b\n" "$GREEN" "$LATEST_VERSION" "$
# Download and install
TEMP_DIR=$(mktemp -d)
trap 'cd / && rm -rf "$TEMP_DIR"' EXIT
cd "$TEMP_DIR" || exit 1
# Download the gzipped binary and its checksum
@@ -67,8 +68,6 @@ if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
printf "The downloaded file may be corrupted or tampered with\n"
cd - >/dev/null
rm -rf "$TEMP_DIR"
exit 1
fi
@@ -79,8 +78,4 @@ chmod +x installer
# Execute the installer
printf "%bRunning installer...%b\n" "$GREEN" "$NC"
./installer
# Cleanup
cd - >/dev/null
rm -rf "$TEMP_DIR"
./installer "$@"
+36 -123
View File
@@ -2,35 +2,26 @@ package clipboard
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
"golang.org/x/sys/unix"
)
const (
xkbKeymapFormatV1 = 1
keyStateReleased = 0
keyStatePressed = 1
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
shiftModMask = 1 << 0
ctrlModMask = 1 << 2
// evdev fallbacks for a standard pc105 map
fallbackCtrlKey = 29 // KEY_LEFTCTRL
fallbackShiftKey = 42 // KEY_LEFTSHIFT
fallbackVKey = 47 // KEY_V
)
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
// using the seat's own keymap, so keycodes stay valid for XWayland clients
// (a synthetic wtype-style keymap breaks X11 apps like Steam). withShift
// selects ctrl+shift+v for terminal targets.
func SendPasteKeystroke(withShift bool) error {
func SendPasteKeystroke(withShift bool) (err error) {
s, err := connectSession()
if err != nil {
return err
@@ -50,25 +41,25 @@ func SendPasteKeystroke(withShift bool) error {
}
defer keyboard.Release()
var keymap *wlclient.KeyboardKeymapEvent
var seatKeymap *wlclient.KeyboardKeymapEvent
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
if keymap == nil {
keymap = &e
if seatKeymap == nil {
seatKeymap = &e
}
})
s.display.Roundtrip()
if keymap == nil || keymap.Format != xkbKeymapFormatV1 {
if seatKeymap == nil || seatKeymap.Format != keymap.FormatXkbV1 {
return fmt.Errorf("no xkb keymap from seat")
}
defer unix.Close(keymap.Fd)
defer unix.Close(seatKeymap.Fd)
keymapText, err := readKeymap(keymap.Fd, keymap.Size)
keymapText, err := keymap.Read(seatKeymap.Fd, seatKeymap.Size)
if err != nil {
return fmt.Errorf("read keymap: %w", err)
}
keys := resolveKeycodes(keymapText)
keys := keymap.Parse(keymapText)
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
if err != nil {
@@ -76,131 +67,53 @@ func SendPasteKeystroke(withShift bool) error {
}
defer vk.Destroy()
if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil {
if err := vk.Keymap(keymap.FormatXkbV1, seatKeymap.Fd, seatKeymap.Size); err != nil {
return fmt.Errorf("set keymap: %w", err)
}
mods := uint32(ctrlModMask)
held := []uint32{keys.ctrl}
held := []uint32{keys.Keycode("Control_L")}
if withShift {
mods |= shiftModMask
held = append(held, keys.shift)
held = append(held, keys.Keycode("Shift_L"))
}
t := uint32(0)
press := func(key, state uint32) error {
var pressed []uint32
press := func(key uint32) error {
t++
return vk.Key(t, key, state)
if err := vk.Key(t, key, keyStatePressed); err != nil {
return err
}
pressed = append(pressed, key)
return nil
}
// all releases happen here so error paths can't leave keys held or
// modifiers latched for the compositor to interpret on destroy
defer func() {
for i := len(pressed) - 1; i >= 0; i-- {
t++
if e := vk.Key(t, pressed[i], keyStateReleased); e != nil && err == nil {
err = fmt.Errorf("key release: %w", e)
}
}
if e := vk.Modifiers(0, 0, 0, 0); e != nil && err == nil {
err = fmt.Errorf("clear modifiers: %w", e)
}
s.display.Roundtrip()
}()
for _, key := range held {
if err := press(key, keyStatePressed); err != nil {
if err := press(key); err != nil {
return fmt.Errorf("key press: %w", err)
}
}
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
return fmt.Errorf("set modifiers: %w", err)
}
if err := press(keys.v, keyStatePressed); err != nil {
if err := press(keys.Keycode("v")); err != nil {
return fmt.Errorf("key press: %w", err)
}
if err := press(keys.v, keyStateReleased); err != nil {
return fmt.Errorf("key release: %w", err)
}
for i := len(held) - 1; i >= 0; i-- {
if err := press(held[i], keyStateReleased); err != nil {
return fmt.Errorf("key release: %w", err)
}
}
if err := vk.Modifiers(0, 0, 0, 0); err != nil {
return fmt.Errorf("clear modifiers: %w", err)
}
s.display.Roundtrip()
return nil
}
func readKeymap(fd int, size uint32) (string, error) {
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
if err != nil {
return "", err
}
text := strings.TrimRight(string(data), "\x00")
return text, unix.Munmap(data)
}
type pasteKeycodes struct {
ctrl uint32
shift uint32
v uint32
}
var (
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
)
// xkbcommon may serialize keysyms as hex escapes instead of names
// (e.g. "0x76" for v, "0xffe3" for Control_L).
var keysymNames = map[uint32]string{
0x76: "v",
0xffe3: "Control_L",
0xffe1: "Shift_L",
}
func canonicalKeysym(sym string) string {
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
return sym
}
value, err := strconv.ParseUint(sym[2:], 16, 32)
if err != nil {
return sym
}
if name, ok := keysymNames[uint32(value)]; ok {
return name
}
return sym
}
// resolveKeycodes finds the evdev keycodes producing the keysyms we need in
// the seat keymap's first group, falling back to pc105 positions.
func resolveKeycodes(keymap string) pasteKeycodes {
codes := map[string]uint32{}
for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) {
if code, err := strconv.Atoi(m[2]); err == nil {
codes[m[1]] = uint32(code)
}
}
keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}
want := map[string]*uint32{
"Control_L": &keys.ctrl,
"Shift_L": &keys.shift,
"v": &keys.v,
}
for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) {
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
if group == nil {
continue
}
xkbCode, ok := codes[m[1]]
if !ok || xkbCode < 8 {
continue
}
level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
target, wanted := want[level1]
if !wanted {
continue
}
*target = xkbCode - 8
delete(want, level1)
if len(want) == 0 {
break
}
}
return keys
}
+10 -12
View File
@@ -4,6 +4,7 @@ import (
"os"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
"golang.org/x/sys/unix"
)
@@ -32,20 +33,20 @@ func TestLiveSeatKeymapResolution(t *testing.T) {
}
defer keyboard.Release()
var keymap *wlclient.KeyboardKeymapEvent
var seatKeymap *wlclient.KeyboardKeymapEvent
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
if keymap == nil {
keymap = &e
if seatKeymap == nil {
seatKeymap = &e
}
})
s.display.Roundtrip()
if keymap == nil {
if seatKeymap == nil {
t.Fatal("no keymap event")
}
defer unix.Close(keymap.Fd)
defer unix.Close(seatKeymap.Fd)
text, err := readKeymap(keymap.Fd, keymap.Size)
text, err := keymap.Read(seatKeymap.Fd, seatKeymap.Size)
if err != nil {
t.Fatalf("read keymap: %v", err)
}
@@ -56,10 +57,7 @@ func TestLiveSeatKeymapResolution(t *testing.T) {
}
}
keys := resolveKeycodes(text)
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v)
if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey {
t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap")
}
keys := keymap.Parse(text)
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", seatKeymap.Size,
keys.Keycode("Control_L"), keys.Keycode("Shift_L"), keys.Keycode("v"))
}
-80
View File
@@ -1,80 +0,0 @@
package clipboard
import "testing"
const qwertyKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
minimum = 8;
maximum = 708;
<ESC> = 9;
<AB04> = 55;
<LCTL> = 37;
<LFSH> = 50;
alias <AL01> = <AC01>;
indicator 1 = "Caps Lock";
};
xkb_types "(unnamed)" {
type "ALPHABETIC" {
modifiers = Shift+Lock;
map[Shift] = Level2;
level_name[Level1] = "Base";
};
};
xkb_symbols "(unnamed)" {
key <ESC> { [ Escape ] };
key <AB04> { type= "ALPHABETIC", [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
const hexKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB04> = 56;
<LCTL> = 38;
<LFSH> = 51;
};
xkb_symbols "(unnamed)" {
key <LCTL> { [ 0xffe3 ] };
key <LFSH> {
type= "PC_ALT_LEVEL2",
symbols[1]= [ 0xffe1, 0xfe08 ]
};
key <AB04> { [ 0x76, 0x56 ] };
};
};`
const dvorakKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB09> = 60;
<LCTL> = 37;
<LFSH> = 50;
};
xkb_symbols "(unnamed)" {
key <AB09> { [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
func TestResolveKeycodes(t *testing.T) {
tests := []struct {
name string
keymap string
want pasteKeycodes
}{
{"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}},
{"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}},
{"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}},
{"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := resolveKeycodes(tt.keymap)
if got != tt.want {
t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want)
}
})
}
}
+9 -1
View File
@@ -47,7 +47,15 @@ func Store(data []byte, mimeType string) error {
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
}
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) (err error) {
defer func() {
r := recover()
if r == nil {
return
}
err = fmt.Errorf("clipboard db panic: %v", r)
}()
if len(data) == 0 {
return nil
}
+98 -23
View File
@@ -8,17 +8,21 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_color_management"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
"github.com/AvengeMedia/dankgo/wayland/client"
)
type Config struct {
Format OutputFormat
CustomFormat string
Lowercase bool
Autocopy bool
Notify bool
Format OutputFormat
CustomFormat string
Lowercase bool
Autocopy bool
Notify bool
AllowMultiple bool
}
type Output struct {
@@ -34,18 +38,19 @@ type Output struct {
}
type LayerSurface struct {
output *Output
state *SurfaceState
wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport
wlPools [2]*client.ShmPool
wlBuffers [2]*client.Buffer
slotBusy [2]bool
needsRedraw bool
scopyBuffer *client.Buffer
configured bool
hidden bool
output *Output
state *SurfaceState
wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport
wlPools [2]*client.ShmPool
wlBuffers [2]*client.Buffer
slotBusy [2]bool
needsRedraw bool
framePending bool
scopyBuffer *client.Buffer
configured bool
hidden bool
}
type Picker struct {
@@ -60,9 +65,13 @@ type Picker struct {
seat *client.Seat
pointer *client.Pointer
keyboard *client.Keyboard
keymap *keymap.Keymap
layerShell *wlr_layer_shell.ZwlrLayerShellV1
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
viewporter *wp_viewporter.WpViewporter
colorMgr *wp_color_management.WpColorManagerV1
compositorVersion uint32
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
@@ -86,6 +95,14 @@ func New(config Config) *Picker {
}
func (p *Picker) Run() (*Color, error) {
if !p.config.AllowMultiple {
lock, err := utils.LockSelectionOverlay()
if err != nil {
return nil, err
}
defer lock.Release()
}
if err := p.connect(); err != nil {
return nil, fmt.Errorf("wayland connect: %w", err)
}
@@ -164,9 +181,10 @@ func (p *Picker) checkDone() {
}
}
// flushRedraws paints queued surfaces, at most once per compositor frame each.
func (p *Picker) flushRedraws() {
for _, ls := range p.surfaces {
if !ls.needsRedraw {
if !ls.needsRedraw || ls.framePending {
continue
}
p.redrawSurface(ls)
@@ -213,6 +231,7 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
compositor := client.NewCompositor(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
p.compositor = compositor
p.compositorVersion = e.Version
}
case client.ShmInterfaceName:
@@ -263,6 +282,12 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
p.viewporter = viewporter
}
case wp_color_management.WpColorManagerV1InterfaceName:
mgr := wp_color_management.NewWpColorManagerV1(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, 1, mgr); err == nil {
p.colorMgr = mgr
}
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
@@ -321,16 +346,38 @@ func (p *Picker) createSurfaces() error {
p.outputsMu.Unlock()
for _, output := range outputs {
refLum, pq := p.outputPQEncoding(output)
ls, err := p.createLayerSurface(output)
if err != nil {
return fmt.Errorf("output %s: %w", output.name, err)
}
if pq {
ls.state.SetPQEncoding(refLum)
}
p.surfaces = append(p.surfaces, ls)
}
return nil
}
func (p *Picker) outputPQEncoding(output *Output) (float64, bool) {
if p.colorMgr == nil {
return 0, false
}
desc, ok := wp_color_management.QueryOutputDescription(p.colorMgr, output.wlOutput, p.roundtrip)
if !ok {
return 0, false
}
pq := desc.TF == uint32(wp_color_management.WpColorManagerV1TransferFunctionSt2084Pq) &&
desc.Primaries == uint32(wp_color_management.WpColorManagerV1PrimariesBt2020)
if !pq {
return 0, false
}
return float64(desc.ReferenceLum), true
}
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
surface, err := p.compositor.CreateSurface()
if err != nil {
@@ -517,17 +564,18 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
func (p *Picker) redrawSurface(ls *LayerSurface) {
slot := ls.state.FrontIndex()
if ls.slotBusy[slot] {
if ls.slotBusy[slot] || ls.framePending {
ls.needsRedraw = true
return
}
var renderBuf *ShmBuffer
var damage []rect
switch {
case ls.hidden:
renderBuf = ls.state.RedrawScreenOnly()
renderBuf, damage = ls.state.RedrawScreenOnly()
default:
renderBuf = ls.state.Redraw()
renderBuf, damage = ls.state.Redraw()
}
if renderBuf == nil {
return
@@ -574,7 +622,25 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
_ = ls.wlSurface.SetBufferScale(bufferScale)
}
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
switch {
case p.compositorVersion < 4:
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
default:
for _, r := range damage {
r = r.clip(renderBuf.Width, renderBuf.Height)
if r.w <= 0 || r.h <= 0 {
continue
}
_ = ls.wlSurface.DamageBuffer(int32(r.x), int32(r.y), int32(r.w), int32(r.h))
}
}
if cb, err := ls.wlSurface.Frame(); err == nil {
ls.framePending = true
cb.SetDoneHandler(func(client.CallbackDoneEvent) {
_ = cb.Destroy()
ls.framePending = false
})
}
_ = ls.wlSurface.Commit()
ls.state.SwapBuffers()
@@ -674,9 +740,14 @@ func (p *Picker) setupPointerHandlers() {
}
func (p *Picker) setupKeyboardHandlers() {
p.keyboard.SetKeymapHandler(func(e client.KeyboardKeymapEvent) {
p.keymap = keymap.FromEvent(e)
})
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
sym := p.keymap.Keysym(e.Key)
for _, ls := range p.surfaces {
ls.state.OnKey(e.Key, e.State)
ls.state.OnKey(sym, e.State)
}
})
}
@@ -726,6 +797,10 @@ func (p *Picker) cleanup() {
p.viewporter.Destroy()
}
if p.colorMgr != nil {
p.colorMgr.Destroy()
}
if p.screencopy != nil {
p.screencopy.Destroy()
}
+126 -39
View File
@@ -25,6 +25,8 @@ type SurfaceState struct {
screenBuf *ShmBuffer
screenFormat PixelFormat
pqEncoded bool
refLum float64
yInverted bool
logicalW int
@@ -32,6 +34,10 @@ type SurfaceState struct {
renderBufs [2]*ShmBuffer
front int
primed [2]bool
drawn [2][]rect
shown []rect
fullDamage bool
scale int32
scaleX float64
@@ -56,6 +62,13 @@ func NewSurfaceState(format OutputFormat, lowercase bool) *SurfaceState {
}
}
func (s *SurfaceState) SetPQEncoding(refLum float64) {
s.mu.Lock()
defer s.mu.Unlock()
s.pqEncoded = true
s.refLum = refLum
}
func (s *SurfaceState) SetScale(scale int32) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -152,6 +165,17 @@ func (s *SurfaceState) OnScreencopyReady() {
}
}
// Magnifier rendering and pixel picking work on 8-bit pixels only
if s.screenFormat.Is10Bit() {
switch {
case s.pqEncoded:
s.screenBuf.ConvertPQ2020ToSRGB(s.refLum)
default:
s.screenBuf.Convert10To8()
}
s.screenFormat = s.screenBuf.Format
}
s.recomputeScale()
s.ensureRenderBuffers()
s.readyForDisplay = true
@@ -192,6 +216,10 @@ func (s *SurfaceState) ensureRenderBuffers() {
if s.screenBuf == nil {
return
}
s.primed = [2]bool{}
s.drawn = [2][]rect{}
s.shown = nil
s.fullDamage = true
width := s.screenBuf.Width
height := s.screenBuf.Height
@@ -238,7 +266,7 @@ func (s *SurfaceState) OnPointerButton(button, state uint32) {
}
}
func (s *SurfaceState) OnKey(key, state uint32) {
func (s *SurfaceState) OnKey(sym string, state uint32) {
if state != 1 {
return
}
@@ -246,10 +274,10 @@ func (s *SurfaceState) OnKey(key, state uint32) {
s.mu.Lock()
defer s.mu.Unlock()
switch key {
case 1: // KEY_ESC
switch sym {
case "Escape":
s.cancelled = true
case 28: // KEY_ENTER
case "Return", "KP_Enter":
if s.readyForDisplay && s.screenBuf != nil {
s.colorPicked = true
}
@@ -286,21 +314,70 @@ func (s *SurfaceState) SwapBuffers() {
s.mu.Unlock()
}
func (s *SurfaceState) Redraw() *ShmBuffer {
// rect is a buffer-pixel area, clipped on use.
type rect struct{ x, y, w, h int }
func (r rect) clip(w, h int) rect {
x1, y1 := max(r.x, 0), max(r.y, 0)
x2, y2 := min(r.x+r.w, w), min(r.y+r.h, h)
return rect{x1, y1, max(x2-x1, 0), max(y2-y1, 0)}
}
func magnifierRect(cx, cy int) rect {
const reach = magnifierRadius + 2
return rect{cx - reach, cy - reach, 2*reach + 1, 2*reach + 1}
}
// restore puts screen pixels back under the front buffer's previous overlay and
// records cur as its new footprint. It returns what differs from the frame on screen.
func (s *SurfaceState) restore(dst *ShmBuffer, cur []rect) []rect {
i := s.front
switch {
case !s.primed[i]:
dst.CopyFrom(s.screenBuf)
s.primed[i] = true
default:
for _, r := range s.drawn[i] {
s.copyRect(dst, r)
}
}
s.drawn[i] = cur
damage := append(append([]rect(nil), s.shown...), cur...)
if s.fullDamage {
s.fullDamage = false
damage = []rect{{0, 0, dst.Width, dst.Height}}
}
s.shown = cur
return damage
}
func (s *SurfaceState) copyRect(dst *ShmBuffer, r rect) {
r = r.clip(min(dst.Width, s.screenBuf.Width), min(dst.Height, s.screenBuf.Height))
if r.w <= 0 || r.h <= 0 {
return
}
src := s.screenBuf
for y := r.y; y < r.y+r.h; y++ {
copy(dst.Data()[y*dst.Stride+r.x*4:][:r.w*4], src.Data()[y*src.Stride+r.x*4:][:r.w*4])
}
}
// Redraw draws the magnifier and preview at the pointer into the front buffer,
// touching only the previous and new overlay areas; the rects are the damage.
func (s *SurfaceState) Redraw() (*ShmBuffer, []rect) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.readyForDisplay || s.screenBuf == nil {
return nil
return nil, nil
}
dst := s.renderBufs[s.front]
if dst == nil {
return nil
return nil, nil
}
dst.CopyFrom(s.screenBuf)
px := int(math.Round(float64(s.pointerX) * s.scaleX))
py := int(math.Round(float64(s.pointerY) * s.scaleY))
@@ -313,6 +390,8 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
}
picked := GetPixelColorWithFormat(s.screenBuf, px, sampleY, s.screenFormat)
text := formatColorForPreview(picked, s.displayFormat, s.lowercase)
damage := s.restore(dst, []rect{magnifierRect(px, py), previewRect(dst.Width, dst.Height, px, py, text)})
drawMagnifierWithInversion(
dst.Data(), dst.Stride, dst.Width, dst.Height,
@@ -322,26 +401,25 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
return dst
return dst, damage
}
// RedrawScreenOnly renders just the screenshot without any overlay (magnifier, preview).
// Used for when pointer leaves the surface.
func (s *SurfaceState) RedrawScreenOnly() *ShmBuffer {
func (s *SurfaceState) RedrawScreenOnly() (*ShmBuffer, []rect) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.readyForDisplay || s.screenBuf == nil {
return nil
return nil, nil
}
dst := s.renderBufs[s.front]
if dst == nil {
return nil
return nil, nil
}
dst.CopyFrom(s.screenBuf)
return dst
return dst, s.restore(dst, nil)
}
func (s *SurfaceState) PickColor() (Color, bool) {
@@ -422,6 +500,8 @@ func blendColors(bg, fg Color, alpha float64) Color {
}
}
const magnifierRadius = 80
func drawMagnifierWithInversion(
dst []byte, dstStride, dstW, dstH int,
src []byte, srcStride, srcW, srcH int,
@@ -435,7 +515,7 @@ func drawMagnifierWithInversion(
}
const (
outerRadius = 80
outerRadius = magnifierRadius
borderThickness = 4
aaWidth = 1.5
zoom = 8.0
@@ -530,7 +610,7 @@ func drawMagnifierWithInversion(
}
srcOff := sy*srcStride + sx*4
if srcOff+4 <= len(src) {
finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff]}
} else {
continue
}
@@ -543,12 +623,11 @@ func drawMagnifierWithInversion(
}
}
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format)
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius)
}
func drawMagnifierCrosshair(
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
format PixelFormat,
) {
if width <= 0 || height <= 0 {
return
@@ -1046,30 +1125,28 @@ var fontGlyphs = map[rune][fontH]uint8{
},
}
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
text := formatColorForPreview(c, format, lowercase)
const (
previewPaddingX = 8
previewPaddingY = 4
previewSpace = 2
previewOffset = 88
)
// previewRect places the color label beside the magnifier, flipping to the left
// and clamping so it stays on the buffer.
func previewRect(width, height, cx, cy int, text string) rect {
if len(text) == 0 {
return
return rect{}
}
textW := len(text)*(fontW+previewSpace) - previewSpace
boxW := textW + previewPaddingX*2
boxH := fontH + previewPaddingY*2
const (
paddingX = 8
paddingY = 4
space = 2
offset = 88
)
textW := len(text)*(fontW+space) - space
textH := fontH
boxW := textW + paddingX*2
boxH := textH + paddingY*2
x := cx + offset
x := cx + previewOffset
y := cy - boxH/2
if x+boxW >= width {
x = cx - boxW - offset
x = cx - boxW - previewOffset
}
if x < 0 {
x = 0
@@ -1080,8 +1157,18 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
if y+boxH >= height {
y = height - boxH
}
return rect{x, y, boxW, boxH}
}
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat)
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
text := formatColorForPreview(c, format, lowercase)
if len(text) == 0 {
return
}
box := previewRect(width, height, cx, cy, text)
x, y := box.x, box.y
drawFilledRect(data, stride, width, height, x, y, box.w, box.h, c, pixelFormat)
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
var fg Color
@@ -1090,7 +1177,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
} else {
fg = Color{R: 255, G: 255, B: 255, A: 255}
}
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat)
drawText(data, stride, width, height, x+previewPaddingX, y+previewPaddingY, text, fg, pixelFormat)
}
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
+113 -31
View File
@@ -45,14 +45,12 @@ func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
}
for range goroutines / 2 {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for range iterations {
scale := s.Scale()
assert.GreaterOrEqual(t, scale, int32(1))
}
}()
})
}
wg.Wait()
@@ -76,15 +74,13 @@ func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
}
for range goroutines / 2 {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for range iterations {
w, h := s.LogicalSize()
_ = w
_ = h
}
}()
})
}
wg.Wait()
@@ -98,35 +94,29 @@ func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
const iterations = 100
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for range iterations {
s.OnPointerButton(0x110, 1)
}
}()
})
}
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for range iterations {
s.OnKey(1, 1)
s.OnKey("Escape", 1)
}
}()
})
}
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for range iterations {
picked, cancelled := s.IsDone()
_ = picked
_ = cancelled
}
}()
})
}
wg.Wait()
@@ -140,13 +130,11 @@ func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
const iterations = 100
for range goroutines {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for range iterations {
_ = s.IsReady()
}
}()
})
}
wg.Wait()
@@ -160,13 +148,11 @@ func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
const iterations = 100
for range goroutines {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for range iterations {
s.SwapBuffers()
}
}()
})
}
wg.Wait()
@@ -210,13 +196,13 @@ func TestSurfaceState_PickColorNilBuffer(t *testing.T) {
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.Redraw()
buf, _ := s.Redraw()
assert.Nil(t, buf)
}
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.RedrawScreenOnly()
buf, _ := s.RedrawScreenOnly()
assert.Nil(t, buf)
}
@@ -312,3 +298,99 @@ func TestBlendColors(t *testing.T) {
result = blendColors(bg, fg, 2.0)
assert.Equal(t, fg.R, result.R)
}
func newReadyState(t *testing.T, w, h int) *SurfaceState {
t.Helper()
s := NewSurfaceState(FormatHex, false)
if err := s.OnScreencopyBuffer(FormatXRGB8888, w, h, w*4+16); err != nil {
t.Fatal(err)
}
t.Cleanup(s.Destroy)
data := s.ScreenBuffer().Data()
for i := range data {
data[i] = byte(i*31 + i/7)
}
if err := s.OnLayerConfigure(w, h); err != nil {
t.Fatal(err)
}
s.OnScreencopyReady()
return s
}
func TestSurfaceState_RedrawIncremental(t *testing.T) {
const w, h = 400, 300
s := newReadyState(t, w, h)
screen := s.ScreenBuffer()
reference := func(px, py int, overlay bool) []byte {
ref, err := CreateShmBuffer(w, h, screen.Stride)
if err != nil {
t.Fatal(err)
}
defer ref.Close()
ref.CopyFrom(screen)
if overlay {
c := GetPixelColorWithFormat(screen, px, py, s.screenFormat)
drawMagnifierWithInversion(ref.Data(), ref.Stride, w, h, screen.Data(), screen.Stride, w, h, px, py, c, false, s.screenFormat)
drawColorPreview(ref.Data(), ref.Stride, w, h, px, py, c, FormatHex, false, s.screenFormat)
}
return append([]byte(nil), ref.Data()...)
}
inDamage := func(x, y int, damage []rect) bool {
for _, r := range damage {
if x >= r.x && x < r.x+r.w && y >= r.y && y < r.y+r.h {
return true
}
}
return false
}
var onScreen []byte
positions := [][2]int{{10, 10}, {200, 150}, {205, 152}, {399, 299}, {0, 299}, {-1, -1}, {120, 40}, {125, 40}, {300, 290}}
for i, pos := range positions {
hidden := pos[0] < 0
var buf *ShmBuffer
var damage []rect
px, py := pos[0], pos[1]
switch {
case hidden:
buf, damage = s.RedrawScreenOnly()
default:
s.OnPointerMotion(float64(px), float64(py))
buf, damage = s.Redraw()
}
if buf == nil {
t.Fatalf("step %d: no buffer", i)
}
want := reference(px, py, !hidden)
if !bytesEqual(buf.Data(), want) {
t.Fatalf("step %d: incremental frame differs from full render", i)
}
if onScreen != nil {
for y := range h {
for x := range w {
off := y*buf.Stride + x*4
if bytesEqual(want[off:off+4], onScreen[off:off+4]) || inDamage(x, y, damage) {
continue
}
t.Fatalf("step %d: pixel %d,%d changed outside damage %v", i, x, y, damage)
}
}
}
onScreen = want
s.SwapBuffers()
}
}
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
+94 -48
View File
@@ -38,25 +38,6 @@ func (cd *ConfigDeployer) log(message string) {
}
}
// DeployConfigurations deploys all necessary configurations based on the chosen window manager
func (cd *ConfigDeployer) DeployConfigurations(ctx context.Context, wm deps.WindowManager) ([]DeploymentResult, error) {
return cd.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
}
// DeployConfigurationsWithTerminal deploys all necessary configurations based on chosen window manager and terminal
func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]DeploymentResult, error) {
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
}
// DeployConfigurationsWithSystemd deploys configurations with systemd option
func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
}
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
}
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
}
@@ -65,7 +46,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
func (cd *ConfigDeployer) deployConfigurationsInternal(_ context.Context, wm deps.WindowManager, terminal deps.Terminal, _ []deps.Dependency, replaceConfigs map[string]bool, _ map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult
// Primary config file paths used to detect fresh installs.
@@ -174,6 +155,32 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
return results, nil
}
func (cd *ConfigDeployer) DeployCompositor(wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
switch wm {
case deps.WindowManagerNiri:
return cd.deployNiriConfig(terminal, useSystemd)
case deps.WindowManagerHyprland:
return cd.deployHyprlandConfig(terminal, useSystemd)
case deps.WindowManagerMango:
return cd.deployMangoConfig(terminal, useSystemd)
default:
return DeploymentResult{}, fmt.Errorf("unsupported window manager")
}
}
func (cd *ConfigDeployer) DeployTerminal(terminal deps.Terminal) ([]DeploymentResult, error) {
switch terminal {
case deps.TerminalGhostty:
return cd.deployGhosttyConfig()
case deps.TerminalKitty:
return cd.deployKittyConfig()
case deps.TerminalAlacritty:
return cd.deployAlacrittyConfig()
default:
return nil, fmt.Errorf("unsupported terminal")
}
}
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Niri",
@@ -286,7 +293,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
return nil
}
func (cd *ConfigDeployer) deployMangoConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
func (cd *ConfigDeployer) deployMangoConfig(terminal deps.Terminal, _ bool) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Mango",
Path: filepath.Join(os.Getenv("HOME"), ".config", "mango", "config.conf"),
@@ -572,7 +579,13 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
var existingOutputs []string
for _, output := range outputRegex.FindAllString(existingConfig, -1) {
if strings.Contains(NiriConfig, output) {
continue
}
existingOutputs = append(existingOutputs, output)
}
if len(existingOutputs) == 0 {
return newConfig, nil
@@ -648,9 +661,12 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
if existingData != "" {
existingConfig = existingData
cd.log(fmt.Sprintf("Found existing Hyprland configuration at %s", existingPath))
result.BackupPath = filepath.Join(backupDir, filepath.Base(existingPath))
if err := backupHyprlandConfigFile(existingPath, result.BackupPath, []byte(existingData), strings.EqualFold(filepath.Ext(existingPath), ".conf")); err != nil {
}
// A legacy hyprland.conf is only backed up once hyprland.lua is in place; Hyprland
// watches the live config and regenerates a stub one if it disappears first.
if existingData != "" && !strings.EqualFold(filepath.Ext(existingPath), ".conf") {
result.BackupPath, err = backupHyprlandConfigFile(filepath.Join(backupDir, filepath.Base(existingPath)), []byte(existingData))
if err != nil {
result.Error = fmt.Errorf("failed to create backup: %w", err)
return result, result.Error
}
@@ -690,11 +706,23 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
return result, result.Error
}
movedLegacy, err := backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir)
if useSystemd {
if targetPath, err := EnsureHyprlandSessionTarget(); err != nil {
cd.log(fmt.Sprintf("Warning: failed to write hyprland-session.target: %v", err))
} else {
cd.log(fmt.Sprintf("Ensured hyprland session target at %s", targetPath))
}
}
movedLegacy, mainConfBackup, err := backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir)
if err != nil {
result.Error = fmt.Errorf("failed to back up legacy hyprlang configs: %w", err)
return result, result.Error
}
if mainConfBackup != "" && result.BackupPath == "" {
result.BackupPath = mainConfBackup
cd.log(fmt.Sprintf("Backed up existing config to %s", mainConfBackup))
}
if movedLegacy > 0 {
if result.BackupPath == "" {
result.BackupPath = backupDir
@@ -716,42 +744,53 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
return result, nil
}
func backupHyprlandConfigFile(src, dst string, data []byte, removeSource bool) error {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
func uniqueBackupPath(dst string) string {
if _, err := os.Lstat(dst); err != nil {
return dst
}
if err := os.WriteFile(dst, data, 0o644); err != nil {
return err
}
if removeSource {
if err := os.Remove(src); err != nil && !os.IsNotExist(err) {
return err
for i := 1; i < 1000; i++ {
candidate := fmt.Sprintf("%s.%d", dst, i)
if _, err := os.Lstat(candidate); err != nil {
return candidate
}
}
return nil
return fmt.Sprintf("%s.%d", dst, time.Now().UnixNano())
}
func backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir string) (int, error) {
legacyPaths := []string{filepath.Join(configDir, "hyprland.conf")}
func backupHyprlandConfigFile(dst string, data []byte) (string, error) {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return "", err
}
dst = uniqueBackupPath(dst)
if err := os.WriteFile(dst, data, 0o644); err != nil {
return "", err
}
return dst, nil
}
func backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir string) (int, string, error) {
mainConf := filepath.Join(configDir, "hyprland.conf")
legacyPaths := []string{mainConf}
dmsConfPaths, err := filepath.Glob(filepath.Join(dmsDir, "*.conf"))
if err != nil {
return 0, err
return 0, "", err
}
legacyPaths = append(legacyPaths, dmsConfPaths...)
backupPaths, err := adjacentHyprlandBackupFiles(configDir, dmsDir)
if err != nil {
return 0, err
return 0, "", err
}
legacyPaths = append(legacyPaths, backupPaths...)
moved := 0
mainConfBackup := ""
for _, src := range legacyPaths {
info, err := os.Lstat(src)
if os.IsNotExist(err) {
continue
}
if err != nil {
return moved, err
return moved, mainConfBackup, err
}
if info.IsDir() {
continue
@@ -761,21 +800,28 @@ func backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir string) (int, er
if err != nil {
rel = filepath.Base(src)
}
dst := filepath.Join(backupDir, rel)
if err := moveHyprlandConfigFile(src, dst); err != nil {
return moved, err
dst, err := moveHyprlandConfigFile(src, filepath.Join(backupDir, rel))
if err != nil {
return moved, mainConfBackup, err
}
if src == mainConf {
mainConfBackup = dst
}
moved++
}
return moved, nil
return moved, mainConfBackup, nil
}
func moveHyprlandConfigFile(src, dst string) error {
func moveHyprlandConfigFile(src, dst string) (string, error) {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
return "", err
}
return os.Rename(src, dst)
dst = uniqueBackupPath(dst)
if err := os.Rename(src, dst); err != nil {
return "", err
}
return dst, nil
}
func adjacentHyprlandBackupFiles(configDir, dmsDir string) ([]string, error) {
+70 -1
View File
@@ -557,7 +557,7 @@ func TestKittyConfigStructure(t *testing.T) {
func TestKittyThemeConfigStructure(t *testing.T) {
assert.Contains(t, KittyThemeConfig, "foreground #e0e2e8")
assert.Contains(t, KittyThemeConfig, "background #101418")
assert.Contains(t, KittyThemeConfig, "cursor #e0e2e8")
assert.Contains(t, KittyThemeConfig, "cursor #9dcbfb")
assert.Contains(t, KittyThemeConfig, "color0 #101418")
assert.Contains(t, KittyThemeConfig, "color15 #ffffff")
}
@@ -890,3 +890,72 @@ func TestShouldReplaceConfigDeployIfMissing(t *testing.T) {
}
})
}
func TestBackupLegacyHyprlandConfFilesKeepsExistingBackups(t *testing.T) {
td := t.TempDir()
configDir := filepath.Join(td, ".config", "hypr")
dmsDir := filepath.Join(configDir, "dms")
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
userConfig := "# the config the user actually wrote\nmonitor = DP-1, 1920x1080@144, 0x0, 1\n"
backupDir := filepath.Join(configDir, hyprlandBackupDirName, "2026-01-02_03-04-05")
require.NoError(t, os.MkdirAll(backupDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(backupDir, "hyprland.conf"), []byte(userConfig), 0o644))
autogenerated := "autogenerated = 1\n$mainMod = SUPER\n"
require.NoError(t, os.WriteFile(filepath.Join(configDir, "hyprland.conf"), []byte(autogenerated), 0o644))
moved, mainConfBackup, err := backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir)
require.NoError(t, err)
assert.Equal(t, 1, moved)
assert.Equal(t, filepath.Join(backupDir, "hyprland.conf.1"), mainConfBackup)
kept, err := os.ReadFile(filepath.Join(backupDir, "hyprland.conf"))
require.NoError(t, err)
assert.Equal(t, userConfig, string(kept))
stray, err := os.ReadFile(mainConfBackup)
require.NoError(t, err)
assert.Equal(t, autogenerated, string(stray))
}
func TestBackupHyprlandConfigFileKeepsExistingBackups(t *testing.T) {
backupDir := t.TempDir()
dst := filepath.Join(backupDir, "hyprland.lua")
require.NoError(t, os.WriteFile(dst, []byte("-- first\n"), 0o644))
written, err := backupHyprlandConfigFile(dst, []byte("-- second\n"))
require.NoError(t, err)
assert.Equal(t, dst+".1", written)
first, err := os.ReadFile(dst)
require.NoError(t, err)
assert.Equal(t, "-- first\n", string(first))
}
func TestDeployHyprlandConfigWritesLuaBeforeRemovingLegacyConf(t *testing.T) {
td := t.TempDir()
t.Setenv("HOME", td)
configDir := filepath.Join(td, ".config", "hypr")
require.NoError(t, os.MkdirAll(configDir, 0o755))
confPath := filepath.Join(configDir, "hyprland.conf")
userConfig := "# user config\nmonitor = DP-1, 1920x1080@144, 0x0, 1\n"
require.NoError(t, os.WriteFile(confPath, []byte(userConfig), 0o644))
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
go func() {
for range logChan {
}
}()
defer close(logChan)
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
require.NoError(t, err)
assert.NoFileExists(t, confPath)
backup, err := os.ReadFile(result.BackupPath)
require.NoError(t, err)
assert.Equal(t, userConfig, string(backup))
assert.Equal(t, filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf"), result.BackupPath)
}
@@ -1,5 +1,5 @@
cursor #e0e2e8
cursor_text_color #c2c7cf
cursor #9dcbfb
cursor_text_color #003354
foreground #e0e2e8
background #101418
+6 -6
View File
@@ -212,12 +212,12 @@ binds {
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
XF86Launch1 hotkey-overlay-title="Screenshot: Region" { spawn "dms" "screenshot"; }
Ctrl+XF86Launch1 hotkey-overlay-title="Screenshot: Full Screen" { spawn "dms" "screenshot" "full"; }
Alt+XF86Launch1 hotkey-overlay-title="Screenshot: Window" { spawn "dms" "screenshot" "window"; }
Print hotkey-overlay-title="Screenshot: Region" { spawn "dms" "screenshot"; }
Ctrl+Print hotkey-overlay-title="Screenshot: Full Screen" { spawn "dms" "screenshot" "full"; }
Alt+Print hotkey-overlay-title="Screenshot: Window" { spawn "dms" "screenshot" "window"; }
// === Display Profiles ===
Mod+P hotkey-overlay-title="Cycle Display Profile" {
spawn "dms" "ipc" "outputs" "cycleProfile";
+2 -2
View File
@@ -179,8 +179,8 @@ func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
if err != nil {
rel = filepath.Base(src)
}
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, rel)
if err := moveHyprlandConfigFile(src, dst); err != nil {
dst, err := moveHyprlandConfigFile(src, filepath.Join(configDir, hyprlandBackupDirName, ts, rel))
if err != nil {
if logFn != nil {
logFn("Could not move stray Hyprland conf file %s: %v", src, err)
}
@@ -0,0 +1,56 @@
package config
import (
"os"
"path/filepath"
"slices"
)
// Mirrors Hyprland's shipped target. Before=graphical-session.target would cycle with the add-wants implicit After=dms.service.
const hyprlandSessionTargetUnit = `[Unit]
Description=Hyprland Session Target
BindsTo=graphical-session.target
Wants=graphical-session-pre.target
After=graphical-session-pre.target
PropagatesStopTo=graphical-session.target
`
var staleHyprlandSessionTargetUnits = []string{
`[Unit]
Description=Hyprland Session Target
Requires=graphical-session.target
After=graphical-session.target
`,
`[Unit]
Description=Hyprland Session Target
BindsTo=graphical-session.target
Before=graphical-session.target
Wants=graphical-session-pre.target
After=graphical-session-pre.target
`,
}
// Only a missing or dankinstall-written unit is replaced; hand-written units are left alone.
func EnsureHyprlandSessionTarget() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
targetPath := filepath.Join(homeDir, ".config", "systemd", "user", "hyprland-session.target")
existing, err := os.ReadFile(targetPath)
if err == nil {
content := string(existing)
if content == hyprlandSessionTargetUnit || !slices.Contains(staleHyprlandSessionTargetUnits, content) {
return targetPath, nil
}
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
return "", err
}
if err := os.WriteFile(targetPath, []byte(hyprlandSessionTargetUnit), 0o644); err != nil {
return "", err
}
return targetPath, nil
}
@@ -0,0 +1,75 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func targetPathFor(t *testing.T, home string) string {
t.Helper()
return filepath.Join(home, ".config", "systemd", "user", "hyprland-session.target")
}
func TestEnsureHyprlandSessionTargetWritesMissing(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
path, err := EnsureHyprlandSessionTarget()
if err != nil {
t.Fatal(err)
}
if path != targetPathFor(t, home) {
t.Fatalf("unexpected path %s", path)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != hyprlandSessionTargetUnit {
t.Fatalf("unexpected content:\n%s", data)
}
}
func TestEnsureHyprlandSessionTargetUpgradesStale(t *testing.T) {
for _, stale := range staleHyprlandSessionTargetUnits {
home := t.TempDir()
t.Setenv("HOME", home)
path := targetPathFor(t, home)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(stale), 0o644); err != nil {
t.Fatal(err)
}
if _, err := EnsureHyprlandSessionTarget(); err != nil {
t.Fatal(err)
}
data, _ := os.ReadFile(path)
if string(data) != hyprlandSessionTargetUnit {
t.Fatalf("stale unit not upgraded:\n%s", data)
}
}
}
func TestEnsureHyprlandSessionTargetKeepsCustom(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
path := targetPathFor(t, home)
custom := "[Unit]\nDescription=mine\n"
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(custom), 0o644); err != nil {
t.Fatal(err)
}
if _, err := EnsureHyprlandSessionTarget(); err != nil {
t.Fatal(err)
}
data, _ := os.ReadFile(path)
if string(data) != custom {
t.Fatalf("custom unit was overwritten:\n%s", data)
}
}
+128 -49
View File
@@ -3,6 +3,7 @@ package dank16
import (
"fmt"
"math"
"strconv"
"github.com/lucasb-eyer/go-colorful"
)
@@ -11,27 +12,38 @@ type RGB struct {
R, G, B float64
}
type HSL struct {
H, S, L float64
}
type HSV struct {
H, S, V float64
}
type ColorInfo struct {
Hex string `json:"hex"`
HexStripped string `json:"hex_stripped"`
R int `json:"r"`
G int `json:"g"`
B int `json:"b"`
}
type VariantColorValue struct {
Hex string `json:"hex"`
HexStripped string `json:"hex_stripped"`
Hex string `json:"hex"`
HexStripped string `json:"hex_stripped"`
HexAlpha string `json:"hex_alpha"`
HexAlphaStripped string `json:"hex_alpha_stripped"`
AlphaHex string `json:"alpha_hex"`
AlphaHexStripped string `json:"alpha_hex_stripped"`
RGB string `json:"rgb"`
RGBA string `json:"rgba"`
HSL string `json:"hsl"`
HSLA string `json:"hsla"`
R string `json:"red"`
G string `json:"green"`
B string `json:"blue"`
A string `json:"alpha"`
H string `json:"hue"`
S string `json:"saturation"`
L string `json:"lightness"`
}
type VariantColorInfo struct {
Dark VariantColorValue `json:"dark"`
Light VariantColorValue `json:"light"`
Default VariantColorValue `json:"default"`
Dark ColorInfo `json:"dark"`
Light ColorInfo `json:"light"`
Default ColorInfo `json:"default"`
}
type Palette struct {
@@ -74,16 +86,41 @@ type VariantPalette struct {
func NewColorInfo(hex string) ColorInfo {
rgb := HexToRGB(hex)
hsl := RGBToHSL(rgb)
var r, g, b, a, h, s, l string
r = strconv.FormatInt(int64(math.Round(rgb.R*255)), 10)
g = strconv.FormatInt(int64(math.Round(rgb.G*255)), 10)
b = strconv.FormatInt(int64(math.Round(rgb.B*255)), 10)
a = "1.0"
h = strconv.FormatFloat(math.Round(hsl.H*3600)/10, 'f', 1, 64)
s = fmt.Sprintf("%.1f%%", math.Round(hsl.S*1000.0)/10.0)
l = fmt.Sprintf("%.1f%%", math.Round(hsl.L*1000.0)/10.0)
stripped := hex
if len(hex) > 0 && hex[0] == '#' {
stripped = hex[1:]
}
return ColorInfo{
Hex: hex,
HexStripped: stripped,
R: int(math.Round(rgb.R * 255)),
G: int(math.Round(rgb.G * 255)),
B: int(math.Round(rgb.B * 255)),
Hex: hex,
HexStripped: stripped,
HexAlpha: hex + "ff",
HexAlphaStripped: stripped + "ff",
AlphaHex: "#ff" + stripped,
AlphaHexStripped: "ff" + stripped,
RGB: fmt.Sprintf("rgb(%s, %s, %s)", r, g, b),
RGBA: fmt.Sprintf("rgba(%s, %s, %s, %s)", r, g, b, a),
HSL: fmt.Sprintf("hsl(%s, %s, %s)", h, s, l),
HSLA: fmt.Sprintf("hsla(%s, %s, %s, %s)", h, s, l, a),
R: r,
G: g,
B: b,
A: a,
H: h,
S: s,
L: l,
}
}
@@ -138,6 +175,35 @@ func RGBToHSV(rgb RGB) HSV {
return HSV{H: h, S: s, V: max}
}
func RGBToHSL(rgb RGB) HSL {
max := math.Max(math.Max(rgb.R, rgb.G), rgb.B)
min := math.Min(math.Min(rgb.R, rgb.G), rgb.B)
delta := max - min
var h, s, l float64
l = (max + min) / 2
if delta == 0 {
return HSL{H: 0, S: 0, L: l}
}
switch max {
case rgb.R:
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
case rgb.G:
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
default:
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
}
if h < 0 {
h += 1.0
}
s = delta / (1 - math.Abs((2*l)-1))
return HSL{H: h, S: s, L: l}
}
func HSVToRGB(hsv HSV) RGB {
h := hsv.H * 6.0
c := hsv.V * hsv.S
@@ -389,6 +455,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
type PaletteOptions struct {
IsLight bool
Background string
Container string
UseDPS bool
}
@@ -454,8 +521,18 @@ func DeriveContainer(primary string, isLight bool) string {
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
}
// DeriveContainer only approximates the Material tone pairing, so a caller that
// has the real primary_container should pass it: color5 and color6 are the
// primary/container pair and their contrast with each other is the invariant.
func resolveContainer(primaryColor string, opts PaletteOptions) string {
if opts.Container != "" {
return opts.Container
}
return DeriveContainer(primaryColor, opts.IsLight)
}
func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
baseColor := DeriveContainer(primaryColor, opts.IsLight)
baseColor := resolveContainer(primaryColor, opts)
rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb)
@@ -465,13 +542,20 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
var palette Palette
var normalTextTarget, secondaryTarget float64
if opts.UseDPS {
// Delta Phi Star is not symmetric across polarity: on a light background the
// same Lc buys far less real contrast, so the light targets are the numbers
// that land on the same measured WCAG ratios the dark ones already deliver.
var normalTextTarget, accentTarget float64
switch {
case opts.UseDPS && opts.IsLight:
normalTextTarget = 70.0
accentTarget = 58.0
case opts.UseDPS:
normalTextTarget = 40.0
secondaryTarget = 35.0
} else {
accentTarget = 24.5
default:
normalTextTarget = 4.5
secondaryTarget = 3.0
accentTarget = 2.1
}
var bgColor string
@@ -491,8 +575,6 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
greenH := blendHue(0.33, ph.H, 0.10)
yellowH := blendHue(0.14, ph.H, 0.04)
accentTarget := secondaryTarget * 0.7
if opts.IsLight {
redS := math.Min(baseSat*1.2, 1.0)
redV := baseVal * 0.95
@@ -504,15 +586,13 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
yellowS := math.Min(baseSat*1.5, 1.0)
yellowV := math.Min(baseVal*1.2, 1.0)
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
blueS := math.Min(ph.S*1.05, 1.0)
blueV := math.Min(ph.V*1.05, 1.0)
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
// Color5 matches primary_container exactly (light container in light mode)
container5 := DeriveContainer(primaryColor, true)
palette.Color5 = NewColorInfo(container5)
palette.Color5 = NewColorInfo(baseColor)
palette.Color6 = NewColorInfo(primaryColor)
@@ -538,8 +618,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
brightBlueV := math.Min(ph.V*1.15, 1.0)
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
lightContainer := DeriveContainer(primaryColor, true)
palette.Color13 = NewColorInfo(lightContainer)
color13S := ph.S * 0.7
color13V := math.Min(ph.V*1.3, 1.0)
palette.Color13 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})), bgColor, accentTarget, opts))
brightCyanS := ph.S * 0.5
brightCyanV := math.Min(ph.V*1.3, 1.0)
@@ -566,9 +647,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
blueV := ph.V * 0.95
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
// Color5 matches primary_container exactly (dark container in dark mode)
darkContainer := DeriveContainer(primaryColor, false)
palette.Color5 = NewColorInfo(darkContainer)
palette.Color5 = NewColorInfo(baseColor)
palette.Color6 = NewColorInfo(primaryColor)
@@ -615,32 +694,32 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
}
type VariantOptions struct {
PrimaryDark string
PrimaryLight string
Background string
UseDPS bool
IsLightMode bool
PrimaryDark string
PrimaryLight string
BackgroundDark string
BackgroundLight string
ContainerDark string
ContainerLight string
UseDPS bool
IsLightMode bool
}
func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
darkVal := VariantColorValue{Hex: dark.Hex, HexStripped: dark.HexStripped}
lightVal := VariantColorValue{Hex: light.Hex, HexStripped: light.HexStripped}
defaultVal := darkVal
defaultVal := dark
if isLightMode {
defaultVal = lightVal
defaultVal = light
}
return VariantColorInfo{
Dark: darkVal,
Light: lightVal,
Dark: dark,
Light: light,
Default: defaultVal,
}
}
func GenerateVariantPalette(opts VariantOptions) VariantPalette {
darkOpts := PaletteOptions{IsLight: false, Background: opts.Background, UseDPS: opts.UseDPS}
lightOpts := PaletteOptions{IsLight: true, Background: opts.Background, UseDPS: opts.UseDPS}
darkOpts := PaletteOptions{IsLight: false, Background: opts.BackgroundDark, Container: opts.ContainerDark, UseDPS: opts.UseDPS}
lightOpts := PaletteOptions{IsLight: true, Background: opts.BackgroundLight, Container: opts.ContainerLight, UseDPS: opts.UseDPS}
dark := GeneratePalette(opts.PrimaryDark, darkOpts)
light := GeneratePalette(opts.PrimaryLight, lightOpts)
+243 -20
View File
@@ -1,6 +1,8 @@
package dank16
import (
"bytes"
"encoding/json"
"math"
"testing"
)
@@ -91,6 +93,54 @@ func TestRGBToHex(t *testing.T) {
}
}
func TestRGBToHSL(t *testing.T) {
tests := []struct {
name string
input RGB
expected HSL
}{
{
name: "black",
input: RGB{R: 0.0, G: 0.0, B: 0.0},
expected: HSL{H: 0.0, S: 0.0, L: 0.0},
},
{
name: "white",
input: RGB{R: 1.0, G: 1.0, B: 1.0},
expected: HSL{H: 0.0, S: 0.0, L: 1.0},
},
{
name: "red",
input: RGB{R: 1.0, G: 0.0, B: 0.0},
expected: HSL{H: 0.0, S: 1.0, L: 0.5},
},
{
name: "green",
input: RGB{R: 0.0, G: 1.0, B: 0.0},
expected: HSL{H: 120.0 / 360.0, S: 1.0, L: 0.5},
},
{
name: "blue",
input: RGB{R: 0.0, G: 0.0, B: 1.0},
expected: HSL{H: 240.0 / 360.0, S: 1.0, L: 0.5},
},
{
name: "magenta",
input: RGB{R: 1.0, G: 0.0, B: 1.0},
expected: HSL{H: 300 / 360.0, S: 1.0, L: 0.5},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := RGBToHSL(tt.input)
if !floatEqual(result.H, tt.expected.H) || !floatEqual(result.S, tt.expected.S) || !floatEqual(result.L, tt.expected.L) {
t.Errorf("RGBToHSL(%v) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestRGBToHSV(t *testing.T) {
tests := []struct {
name string
@@ -309,31 +359,41 @@ func TestEnsureContrast(t *testing.T) {
func TestGeneratePalette(t *testing.T) {
tests := []struct {
name string
base string
opts PaletteOptions
name string
base string
backgroundHex string
backgroundHSL string
opts PaletteOptions
}{
{
name: "dark theme default",
base: "#625690",
opts: PaletteOptions{IsLight: false},
name: "dark theme default",
base: "#625690",
backgroundHex: "#1a1a1a",
backgroundHSL: "hsl(0.0, 0.0%, 10.2%)",
opts: PaletteOptions{IsLight: false},
},
{
name: "light theme default",
base: "#625690",
opts: PaletteOptions{IsLight: true},
name: "light theme default",
base: "#625690",
backgroundHex: "#f8f8f8",
backgroundHSL: "hsl(0.0, 0.0%, 97.3%)",
opts: PaletteOptions{IsLight: true},
},
{
name: "light theme with custom background",
base: "#625690",
name: "light theme with custom background",
base: "#625690",
backgroundHex: "#fafafa",
backgroundHSL: "hsl(0.0, 0.0%, 98.0%)",
opts: PaletteOptions{
IsLight: true,
Background: "#fafafa",
},
},
{
name: "dark theme with custom background",
base: "#625690",
name: "dark theme with custom background",
base: "#625690",
backgroundHex: "#0a0a0a",
backgroundHSL: "hsl(0.0, 0.0%, 3.9%)",
opts: PaletteOptions{
IsLight: false,
Background: "#0a0a0a",
@@ -358,12 +418,12 @@ func TestGeneratePalette(t *testing.T) {
}
}
if tt.opts.Background != "" && result.Color0.Hex != tt.opts.Background {
t.Errorf("Background color = %s, expected %s", result.Color0.Hex, tt.opts.Background)
} else if !tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result.Color0.Hex)
} else if tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
if result.Color0.Hex != tt.backgroundHex {
t.Errorf("Background color hex = %s, expected %s", result.Color0.Hex, tt.backgroundHex)
}
if result.Color0.HSL != tt.backgroundHSL {
t.Errorf("Background color hsl = %s, expected %s", result.Color0.HSL, tt.backgroundHSL)
}
// Color15 is now derived from primary, so just verify it's a valid color
@@ -384,6 +444,59 @@ func TestGeneratePalette(t *testing.T) {
}
}
func TestFormatOutput(t *testing.T) {
tests := []struct {
in string
out []byte
}{
{
in: "#000000",
out: []byte(`{"hex":"#000000","hex_stripped":"000000","hex_alpha":"#000000ff","hex_alpha_stripped":"000000ff","alpha_hex":"#ff000000","alpha_hex_stripped":"ff000000","rgb":"rgb(0, 0, 0)","rgba":"rgba(0, 0, 0, 1.0)","hsl":"hsl(0.0, 0.0%, 0.0%)","hsla":"hsla(0.0, 0.0%, 0.0%, 1.0)","red":"0","green":"0","blue":"0","alpha":"1.0","hue":"0.0","saturation":"0.0%","lightness":"0.0%"}`),
},
{
in: "#ffffff",
out: []byte(`{"hex":"#ffffff","hex_stripped":"ffffff","hex_alpha":"#ffffffff","hex_alpha_stripped":"ffffffff","alpha_hex":"#ffffffff","alpha_hex_stripped":"ffffffff","rgb":"rgb(255, 255, 255)","rgba":"rgba(255, 255, 255, 1.0)","hsl":"hsl(0.0, 0.0%, 100.0%)","hsla":"hsla(0.0, 0.0%, 100.0%, 1.0)","red":"255","green":"255","blue":"255","alpha":"1.0","hue":"0.0","saturation":"0.0%","lightness":"100.0%"}`),
},
{
in: "#ff0000",
out: []byte(`{"hex":"#ff0000","hex_stripped":"ff0000","hex_alpha":"#ff0000ff","hex_alpha_stripped":"ff0000ff","alpha_hex":"#ffff0000","alpha_hex_stripped":"ffff0000","rgb":"rgb(255, 0, 0)","rgba":"rgba(255, 0, 0, 1.0)","hsl":"hsl(0.0, 100.0%, 50.0%)","hsla":"hsla(0.0, 100.0%, 50.0%, 1.0)","red":"255","green":"0","blue":"0","alpha":"1.0","hue":"0.0","saturation":"100.0%","lightness":"50.0%"}`),
},
{
in: "#00ff00",
out: []byte(`{"hex":"#00ff00","hex_stripped":"00ff00","hex_alpha":"#00ff00ff","hex_alpha_stripped":"00ff00ff","alpha_hex":"#ff00ff00","alpha_hex_stripped":"ff00ff00","rgb":"rgb(0, 255, 0)","rgba":"rgba(0, 255, 0, 1.0)","hsl":"hsl(120.0, 100.0%, 50.0%)","hsla":"hsla(120.0, 100.0%, 50.0%, 1.0)","red":"0","green":"255","blue":"0","alpha":"1.0","hue":"120.0","saturation":"100.0%","lightness":"50.0%"}`),
},
{
in: "#0000ff",
out: []byte(`{"hex":"#0000ff","hex_stripped":"0000ff","hex_alpha":"#0000ffff","hex_alpha_stripped":"0000ffff","alpha_hex":"#ff0000ff","alpha_hex_stripped":"ff0000ff","rgb":"rgb(0, 0, 255)","rgba":"rgba(0, 0, 255, 1.0)","hsl":"hsl(240.0, 100.0%, 50.0%)","hsla":"hsla(240.0, 100.0%, 50.0%, 1.0)","red":"0","green":"0","blue":"255","alpha":"1.0","hue":"240.0","saturation":"100.0%","lightness":"50.0%"}`),
},
{
in: "#625690",
out: []byte(`{"hex":"#625690","hex_stripped":"625690","hex_alpha":"#625690ff","hex_alpha_stripped":"625690ff","alpha_hex":"#ff625690","alpha_hex_stripped":"ff625690","rgb":"rgb(98, 86, 144)","rgba":"rgba(98, 86, 144, 1.0)","hsl":"hsl(252.4, 25.2%, 45.1%)","hsla":"hsla(252.4, 25.2%, 45.1%, 1.0)","red":"98","green":"86","blue":"144","alpha":"1.0","hue":"252.4","saturation":"25.2%","lightness":"45.1%"}`),
},
{
in: "#ff00ff",
out: []byte(`{"hex":"#ff00ff","hex_stripped":"ff00ff","hex_alpha":"#ff00ffff","hex_alpha_stripped":"ff00ffff","alpha_hex":"#ffff00ff","alpha_hex_stripped":"ffff00ff","rgb":"rgb(255, 0, 255)","rgba":"rgba(255, 0, 255, 1.0)","hsl":"hsl(300.0, 100.0%, 50.0%)","hsla":"hsla(300.0, 100.0%, 50.0%, 1.0)","red":"255","green":"0","blue":"255","alpha":"1.0","hue":"300.0","saturation":"100.0%","lightness":"50.0%"}`),
},
{
in: "#888888",
out: []byte(`{"hex":"#888888","hex_stripped":"888888","hex_alpha":"#888888ff","hex_alpha_stripped":"888888ff","alpha_hex":"#ff888888","alpha_hex_stripped":"ff888888","rgb":"rgb(136, 136, 136)","rgba":"rgba(136, 136, 136, 1.0)","hsl":"hsl(0.0, 0.0%, 53.3%)","hsla":"hsla(0.0, 0.0%, 53.3%, 1.0)","red":"136","green":"136","blue":"136","alpha":"1.0","hue":"0.0","saturation":"0.0%","lightness":"53.3%"}`),
},
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
ci := NewColorInfo(tt.in)
out, err := json.Marshal(ci)
if err != nil {
t.Error("Failed encoding", err)
}
if !bytes.Equal(out, tt.out) {
t.Errorf("Color output mismatch, got = %s, expected %s", out, tt.out)
}
})
}
}
func TestRoundTripConversion(t *testing.T) {
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
@@ -588,7 +701,8 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
bgColor := result.Color0.Hex
for i := 1; i < 8; i++ {
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
// Color5 is the container and Color6 the exact primary: they pair
// with each other, not with the background
if i == 5 || i == 6 {
continue
}
@@ -749,3 +863,112 @@ func TestGeneratePaletteLightColor8StaysLight(t *testing.T) {
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
}
func TestGeneratePaletteLightYellowMeetsNormalTextContrast(t *testing.T) {
primaries := []string{"#63568f", "#7d5800", "#116d2b", "#1c5070", "#8b3f68"}
for _, primary := range primaries {
t.Run(primary, func(t *testing.T) {
palette := GeneratePalette(primary, PaletteOptions{IsLight: true, Background: "#f5fafc", UseDPS: true})
ratio := ContrastRatio(palette.Color3.Hex, palette.Color0.Hex)
if ratio < 4.5 {
t.Errorf("light Color3 %s vs bg %s ratio %.2f, expected >= 4.5 (yellow must clear the normal-text target)",
palette.Color3.Hex, palette.Color0.Hex, ratio)
}
})
}
}
func TestGenerateVariantPaletteUsesPerVariantBackground(t *testing.T) {
const darkBg = "#0f1417"
const lightBg = "#f5fafc"
variants := GenerateVariantPalette(VariantOptions{
PrimaryDark: "#8ecff2",
PrimaryLight: "#1c5070",
BackgroundDark: darkBg,
BackgroundLight: lightBg,
UseDPS: true,
})
if variants.Color0.Dark.Hex != darkBg {
t.Errorf("dark Color0 = %s, expected the dark background %s", variants.Color0.Dark.Hex, darkBg)
}
if variants.Color0.Light.Hex != lightBg {
t.Errorf("light Color0 = %s, expected the light background %s", variants.Color0.Light.Hex, lightBg)
}
lightDim := variants.Color8.Light.Hex
if lstar := getLstar(lightDim); lstar < 60.0 {
t.Errorf("light Color8 %s has L* %.2f, expected >= 60 (derived from the dark surface it collapses to near-black)",
lightDim, lstar)
}
if ratio := ContrastRatio(lightDim, lightBg); ratio > 3.0 {
t.Errorf("light Color8 %s vs bg %s ratio %.2f, expected <= 3.0 (bright black stays dim)", lightDim, lightBg, ratio)
}
darkDim := variants.Color8.Dark.Hex
if ratio := ContrastRatio(darkDim, darkBg); ratio > 3.0 {
t.Errorf("dark Color8 %s vs bg %s ratio %.2f, expected <= 3.0", darkDim, darkBg, ratio)
}
}
func TestGeneratePaletteColor5PairsWithColor6(t *testing.T) {
tests := []struct {
name string
primary string
container string
bg string
isLight bool
}{
{"violet dark", "#cdbdff", "#4b3e76", "#141318", false},
{"violet light", "#63568f", "#e8deff", "#fdf7ff", true},
{"green dark", "#9ed49d", "#1f5027", "#101510", false},
{"green light", "#38693c", "#b9f0b8", "#f7fbf2", true},
{"amber light", "#805610", "#ffddb3", "#fff8f4", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
palette := GeneratePalette(tt.primary, PaletteOptions{
IsLight: tt.isLight,
Background: tt.bg,
Container: tt.container,
UseDPS: true,
})
if palette.Color5.Hex != tt.container {
t.Errorf("Color5 = %s, expected the supplied container %s", palette.Color5.Hex, tt.container)
}
if palette.Color6.Hex != tt.primary {
t.Errorf("Color6 = %s, expected the primary %s", palette.Color6.Hex, tt.primary)
}
pair := ContrastRatio(palette.Color5.Hex, palette.Color6.Hex)
if pair < 4.5 {
t.Errorf("Color5 %s vs Color6 %s ratio %.2f, expected >= 4.5 (the container/primary pair carries the prompt themes that use ANSI 5 on 6)",
palette.Color5.Hex, palette.Color6.Hex, pair)
}
if palette.Color5.Hex == palette.Color13.Hex {
t.Errorf("Color5 and Color13 are both %s, expected a distinct bright variant", palette.Color5.Hex)
}
if sep := ContrastRatio(palette.Color5.Hex, palette.Color13.Hex); sep < 2.0 {
t.Errorf("Color5 %s vs Color13 %s ratio %.2f, expected >= 2.0 (normal and bright must be tellable apart)",
palette.Color5.Hex, palette.Color13.Hex, sep)
}
})
}
}
func TestGeneratePaletteFallsBackToDerivedContainer(t *testing.T) {
const primary = "#cdbdff"
palette := GeneratePalette(primary, PaletteOptions{Background: "#141318", UseDPS: true})
want := DeriveContainer(primary, false)
if palette.Color5.Hex != want {
t.Errorf("Color5 = %s with no container supplied, expected the derived %s", palette.Color5.Hex, want)
}
}
+1 -1
View File
@@ -172,7 +172,7 @@ func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err
if err != nil {
return nil, nil, err
}
for _, line := range strings.Split(string(data), "\n") {
for line := range strings.SplitSeq(string(data), "\n") {
line = strings.TrimSpace(line)
var pkg string
var target *[]string
+5 -27
View File
@@ -13,7 +13,9 @@ import (
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
)
@@ -615,24 +617,8 @@ func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error
}
func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
homeDir, err := os.UserHomeDir()
targetPath, err := config.EnsureHyprlandSessionTarget()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
if err := os.MkdirAll(targetDir, 0o755); err != nil {
return fmt.Errorf("failed to create systemd user directory: %w", err)
}
targetPath := filepath.Join(targetDir, "hyprland-session.target")
content := `[Unit]
Description=Hyprland Session Target
Requires=graphical-session.target
After=graphical-session.target
`
if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil {
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
}
@@ -661,17 +647,10 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
CommandInfo: fmt.Sprintf("Downloading dms-%s.gz", arch),
}
// Get latest release version
latestVersionCmd := exec.CommandContext(ctx, "bash", "-c",
`curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'`)
versionOutput, err := latestVersionCmd.Output()
version, err := netfetch.LatestReleaseTag(ctx)
if err != nil {
return fmt.Errorf("failed to get latest DMS version: %w", err)
}
version := strings.TrimSpace(string(versionOutput))
if version == "" {
return fmt.Errorf("could not determine latest DMS version")
}
homeDir, err := os.UserHomeDir()
if err != nil {
@@ -687,8 +666,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
downloadURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch)
gzPath := filepath.Join(tmpDir, "dms.gz")
downloadCmd := exec.CommandContext(ctx, "curl", "-L", downloadURL, "-o", gzPath)
if err := downloadCmd.Run(); err != nil {
if err := netfetch.ToFile(ctx, downloadURL, netfetch.Options{Timeout: 5 * time.Minute}, gzPath); err != nil {
return fmt.Errorf("failed to download DMS binary: %w", err)
}
+17 -6
View File
@@ -3,6 +3,7 @@ package distros
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
@@ -267,12 +268,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: "Installing development dependencies...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
LogOutput: "Installing additional development tools",
}
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err)
}
@@ -505,12 +506,22 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
Progress: 0.18,
Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
CommandInfo: fmt.Sprintf("install keyring for %s", pkg.RepoURL),
}
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
keyring, err := fetchDearmoredKey(ctx, baseURL+"/Release.key")
if err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
}
tmpKeyring, err := writeTempKeyring(keyring)
if err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
}
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("install -m 0644 %s %s", tmpKeyring, keyringPath))
err = d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20)
os.Remove(tmpKeyring)
if err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
}
+3 -3
View File
@@ -124,7 +124,7 @@ func debianAPTSuites(sources string) []string {
stanza = nil
}
for _, raw := range strings.Split(sources, "\n") {
for raw := range strings.SplitSeq(sources, "\n") {
line := strings.TrimSpace(raw)
if line == "" {
flushStanza()
@@ -164,8 +164,8 @@ func normalizeDebianSuite(suite string) string {
suite = strings.ToLower(strings.TrimSpace(suite))
suite = strings.Trim(suite, "/")
for _, suffix := range []string{"-security", "-updates", "-backports", "-proposed-updates"} {
if strings.HasSuffix(suite, suffix) {
return strings.TrimSuffix(suite, suffix)
if before, ok := strings.CutSuffix(suite, suffix); ok {
return before
}
}
return suite
+1 -1
View File
@@ -457,7 +457,7 @@ func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []st
// enableTerraRepo registers the persistent Terra repo (via terra-release) so
// `mangowm` resolves in the DNF phase. $releasever is single-quoted so dnf, not
// the shell, expands it.
func (f *FedoraDistribution) enableTerraRepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
func (f *FedoraDistribution) enableTerraRepo(ctx context.Context, sudoPassword string, _ chan<- InstallProgressMsg) error {
// Skip if Terra is already configured
if exec.CommandContext(ctx, "sh", "-c",
"rpm -q terra-release >/dev/null 2>&1 || test -f /etc/yum.repos.d/terra.repo").Run() == nil {
+1
View File
@@ -101,6 +101,7 @@ type Distribution interface {
// Package Mapping
GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping
GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping
// Prerequisites
InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error
+1 -1
View File
@@ -531,7 +531,7 @@ func isOpenSUSEInstallMediaURI(uri string) bool {
func parseZypperInstallMediaAliases(output string) []string {
var aliases []string
for _, line := range strings.Split(output, "\n") {
for line := range strings.SplitSeq(output, "\n") {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "|") {
continue
+131
View File
@@ -0,0 +1,131 @@
package distros
import (
"context"
"encoding/base64"
"fmt"
"os"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
)
const (
crc24Init = 0xB704CE
crc24Poly = 0x1864CFB
)
func crc24(data []byte) uint32 {
crc := uint32(crc24Init)
for _, b := range data {
crc ^= uint32(b) << 16
for range 8 {
crc <<= 1
if crc&0x1000000 != 0 {
crc ^= crc24Poly
}
}
}
return crc & 0xFFFFFF
}
func dearmorPGP(armored []byte) ([]byte, error) {
lines := strings.Split(strings.ReplaceAll(string(armored), "\r\n", "\n"), "\n")
start := -1
for i, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "-----BEGIN PGP ") {
start = i + 1
break
}
}
if start < 0 {
return nil, fmt.Errorf("no PGP armor header found")
}
// Armor headers run until the first blank line; a headerless block has none.
for start < len(lines) && strings.Contains(lines[start], ": ") {
start++
}
for start < len(lines) && strings.TrimSpace(lines[start]) == "" {
start++
}
var payload strings.Builder
checksum := ""
ended := false
for _, line := range lines[start:] {
trimmed := strings.TrimSpace(line)
switch {
case trimmed == "":
continue
case strings.HasPrefix(trimmed, "-----END PGP "):
ended = true
case strings.HasPrefix(trimmed, "="):
checksum = trimmed[1:]
default:
payload.WriteString(trimmed)
}
if ended {
break
}
}
if !ended {
return nil, fmt.Errorf("unterminated PGP armor block")
}
decoded, err := base64.StdEncoding.DecodeString(payload.String())
if err != nil {
return nil, fmt.Errorf("invalid base64 in PGP armor: %w", err)
}
if len(decoded) == 0 {
return nil, fmt.Errorf("PGP armor block is empty")
}
if checksum == "" {
return decoded, nil
}
want, err := base64.StdEncoding.DecodeString(checksum)
if err != nil || len(want) != 3 {
return nil, fmt.Errorf("invalid PGP armor checksum %q", checksum)
}
got := crc24(decoded)
if uint32(want[0])<<16|uint32(want[1])<<8|uint32(want[2]) != got {
return nil, fmt.Errorf("PGP armor checksum mismatch")
}
return decoded, nil
}
func fetchDearmoredKey(ctx context.Context, url string) ([]byte, error) {
armored, err := netfetch.Bytes(ctx, url, netfetch.Options{Timeout: 30 * time.Second})
if err != nil {
return nil, fmt.Errorf("failed to fetch %s: %w", url, err)
}
keyring, err := dearmorPGP(armored)
if err != nil {
return nil, fmt.Errorf("failed to dearmor key from %s: %w", url, err)
}
return keyring, nil
}
// privesc.ExecCommand owns stdin for the sudo password, so the key cannot be piped in.
func writeTempKeyring(keyring []byte) (string, error) {
f, err := os.CreateTemp("", "dms-repo-key-*.gpg")
if err != nil {
return "", fmt.Errorf("failed to create temp keyring: %w", err)
}
defer f.Close()
if _, err := f.Write(keyring); err != nil {
os.Remove(f.Name())
return "", fmt.Errorf("failed to write temp keyring: %w", err)
}
if err := f.Chmod(0o644); err != nil {
os.Remove(f.Name())
return "", fmt.Errorf("failed to chmod temp keyring: %w", err)
}
return f.Name(), nil
}
+66
View File
@@ -0,0 +1,66 @@
package distros
import (
"bytes"
"os"
"strings"
"testing"
)
// Golden is `gpg --batch --dearmor` output for the fixture.
func TestDearmorPGPMatchesGPGGolden(t *testing.T) {
armored, err := os.ReadFile("testdata/obs-release.asc")
if err != nil {
t.Fatalf("read fixture: %v", err)
}
want, err := os.ReadFile("testdata/obs-release.gpg")
if err != nil {
t.Fatalf("read golden: %v", err)
}
got, err := dearmorPGP(armored)
if err != nil {
t.Fatalf("dearmorPGP: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatalf("got %d bytes, want %d", len(got), len(want))
}
}
func TestDearmorPGPAcceptsCRLF(t *testing.T) {
armored, err := os.ReadFile("testdata/obs-release.asc")
if err != nil {
t.Fatalf("read fixture: %v", err)
}
want, err := dearmorPGP(armored)
if err != nil {
t.Fatalf("dearmorPGP: %v", err)
}
crlf := []byte(strings.ReplaceAll(string(armored), "\n", "\r\n"))
got, err := dearmorPGP(crlf)
if err != nil {
t.Fatalf("dearmorPGP on CRLF input: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatal("CRLF input decoded differently")
}
}
func TestDearmorPGPRejects(t *testing.T) {
tests := map[string]string{
"no header": "mQINBAAA\n",
"unterminated": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBAAA\n",
"bad base64": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n!!!!\n-----END PGP PUBLIC KEY BLOCK-----\n",
"empty payload": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n-----END PGP PUBLIC KEY BLOCK-----\n",
"checksum mismatch": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBAAA\n=AAAA\n-----END PGP PUBLIC KEY BLOCK-----\n",
}
for name, input := range tests {
t.Run(name, func(t *testing.T) {
if _, err := dearmorPGP([]byte(input)); err == nil {
t.Fatal("expected an error")
}
})
}
}
+41
View File
@@ -0,0 +1,41 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGkdCo0BEADPLTDkieySQmsnUYbj6VyDve3OVY23AfUukdNoqe/FwOPRm9eW
hduEMo28W5mQZp2TCOWGc7E4V63l3DWjrAzDlYtgDmupZ0QiTLbz0BUWHPsZHRfF
+xVh/pbfcixMtSk/InAe4Dv0p7jc+LjM1u8E204FWsJAPc7DRurwowXS6nEBZlMu
cjTzX0jehRQ2QShtib0iexd5YpfvW4ZGkQ6uSBnOAn5e/jwiSUbLZZhYR7RHdxhG
XR08PVP0SBUQvsTo2lC7Fqi8O3mEZm5NHeuj7dulNJxNu4LbdGYftev7GzJ5yT8O
dNem+rYspFpLaBbdOieRVFCcyVf+jGNHJiWnBkxUNGNSZr+M+26v455/v83mdQW4
1pGBhFyFVirH8wxLmJm7P7CYEBes9HujwaLHBnSGg9VAzXfWb2g5FSdDb3drG8FH
g40HEMl1Kuwnm0o3HH9k94C2cBaS3mji9aE21RgoxGoRZ4vEZEQpoWZ8CVFrKHhU
tU1rbXLWsuG37MRZJYYRb2gCM/zgoEmgBFvoHeVqqd1i8T56j11UUCEfK68iCVVY
GlHfXxEzhBO54qGpfbh3sC9ce5koicyXUdhkBz0Ob+APJ7TLFUpLarxNb/rrJ7YC
0RnVgqtPyS78rMcpOAJTEyZgl9Blad5escDB7394Snkgu/Ghsol8DS+sRwARAQAB
tEJob21lOkF2ZW5nZU1lZGlhIE9CUyBQcm9qZWN0IDxob21lOkF2ZW5nZU1lZGlh
QGJ1aWxkLm9wZW5zdXNlLm9yZz6JAlQEEwEIAD4WIQQgyj5YBMp1AJs2caQoR3hI
O8hhFQUCaR0KjQIbAwUJBB6wAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAo
R3hIO8hhFVgaEACxFCL5YwVJLlQvMjJuvdQt9D1T+vE9uvaYP8SONQOxhpRpOS0U
t+rHJXy73uy4cUieT0o1AS17/8C9b0ur+VlNdjgLf1fqPbAmcvEAB6UQ8yQzLC7j
/8XNJuc/aHcSX3fWFM7YwhVc8dXPE+eRUufxctFXqD8Otg3SDkmeObJE0HlR5m08
2BH8TaPE4HkkJJ4kZV23wnbNs0x4OdoOFOCT7CnCr9k0rgBNMP422S3vJ7zjf08t
GX0YGdNWyB0SSUt7OR372WPabImAeOUTB3f05vfd9fLWBeEPLt/I8wQj1pbo9SSF
upNXsoHcAnTl9gpBVxCZYJXVF+uefxY5d68ZZEYBVEAfzwwdxrvjosYqh0Whcv3V
DNlg2kQ2y9MCptv4a6qNTkVUTKAYv5SV0feV7b/OGUpAqu6dvjkzncMhSDZCmRUr
zjzG2/USv56ef1Hnayuw0YNyrM3IQuEz6jEKEwjofENIk7W2rcKQRZB8gp/BNI1M
MtRxC7YufcIRkyBANCtdM95Wi1tnV64LrPk3vnreG1HVII237xOBtiCn8XgBtHyA
NfJwFctjpaZCRcL6FgWO6uUHOfkZjLeLPehbNIFx/DCqphLWkusuWfH6jiDl0Y49
fpx4oJ6CfIfzsow2t1CL+JCF3D/OX5G20IlMhCMwdXoQ2H8Z/MNiHFJMKYkCMwQT
AQgAHRYhBMw1zD015aNkPlRaQ88Lkoze1k87BQJpHQqQAAoJEM8Lkoze1k87wTQP
/0b1PY/4uBWc0Uf5CK2t4lBUw+D9sL2wsLkCrSMS4mPlcutF+qh3xnO+d74I4Y8R
dg4RYlwHlRGFa2wMKp9q2HEeMxnqG4mRQBo3nCXvQDhPTerJG0mhnDzLVvS+4MaY
vcuxPntHvMygQDrguIChsdehU43FqlexjNKSBlAUBqDIkmgvTlcQjx62/m9m89Sr
Jg1QGqGoAYSaAPK3gDBpnBxYMpLMMCUtiIsHCbZFwxvOGBvTd+HWqaRpB56hm1sf
OFYXjoXl8ydkIh6HuAtP8NHHkHt3exL+dE5mL//KtmeLtm0aDklrd4Ol5ZUo25jQ
sOM0Tc/7AF0X3EyV3wTuz/MsyPFDMU6yJfrrlvpKIRhRtkDjkt8Ut9IOaCJ/iUoO
Fap45D4euq5M326HhkFfPKfnwO7MHWi82CNaQx05l7Lam+NniEOaBNMW0oAAS9JE
D6uoYgJSdCUHmhP8mt4f4C3j+VnQB+LsfPhQpqeEiqQtEar+arJ2KUgaOP/g3Z89
zaOiZN2nbpbK0wPmJQasJeyIGv1lQccjxD9ZxUd8iG8L4CJjnmXkUf1IrRyUZOoh
PK6GJB0YOGpiNnxG4bhiOjgdUMd9J5nBfjWTGNTjI+RzogfFbka+utDWuHR6ASCt
lNMZpysq5D+jaPXphRaKzHqaXrjtxWYiQN6JhGH8fgOF
=kr/Y
-----END PGP PUBLIC KEY BLOCK-----
Binary file not shown.
+4
View File
@@ -26,6 +26,8 @@ const (
ErrTypeSecretPromptTimeout
ErrTypeSecretAgentFailed
ErrTypeGeneric
ErrTypeInvalidDuration
ErrTypeInvalidContrast
)
func NewCustomError(errType ErrorType, message string) error {
@@ -52,8 +54,10 @@ var (
ErrNoUpdateNeeded = NewCustomError(ErrTypeNoUpdateNeeded, "no update needed")
ErrInvalidTemperature = NewCustomError(ErrTypeInvalidTemperature, "temperature must be between 1000 and 10000")
ErrInvalidGamma = NewCustomError(ErrTypeInvalidGamma, "gamma must be between 0 and 10")
ErrInvalidContrast = NewCustomError(ErrTypeInvalidContrast, "contrast must be between 0.5 and 2")
ErrInvalidLocation = NewCustomError(ErrTypeInvalidLocation, "invalid latitude/longitude")
ErrInvalidManualTimes = NewCustomError(ErrTypeInvalidManualTimes, "both sunrise and sunset must be set or neither")
ErrInvalidDuration = NewCustomError(ErrTypeInvalidDuration, "duration must be between 0 and 6 hours")
ErrNoWaylandDisplay = NewCustomError(ErrTypeNoWaylandDisplay, "no wayland display available")
ErrNoGammaControl = NewCustomError(ErrTypeNoGammaControl, "compositor does not support gamma control")
ErrNotInitialized = NewCustomError(ErrTypeNotInitialized, "manager not initialized")
+3 -10
View File
@@ -47,7 +47,7 @@ type GeoClueClient struct {
}
func newGeoClueClient() (*GeoClueClient, error) {
dbusConn, err := dbus.ConnectSystemBus()
dbusConn, err := dbus.SystemBus()
if err != nil {
return nil, fmt.Errorf("system bus connection failed: %w", err)
}
@@ -64,7 +64,6 @@ func newGeoClueClient() (*GeoClueClient, error) {
}
if err := c.setupClient(); err != nil {
dbusConn.Close()
return nil, err
}
@@ -90,10 +89,6 @@ func (c *GeoClueClient) Close() {
c.subscribers.Delete(key)
return true
})
if c.dbusConn != nil {
c.dbusConn.Close()
}
}
func (c *GeoClueClient) Subscribe(id string) chan Location {
@@ -138,9 +133,7 @@ func (c *GeoClueClient) startSignalPump() error {
return err
}
c.sigWG.Add(1)
go func() {
defer c.sigWG.Done()
c.sigWG.Go(func() {
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
clientObj.Call(dbusGeoClueClientTimeStart, 0)
@@ -161,7 +154,7 @@ func (c *GeoClueClient) startSignalPump() error {
c.handleSignal(sig)
}
}
}()
})
return nil
}
+169
View File
@@ -0,0 +1,169 @@
package gpu
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/linux_dmabuf"
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
"golang.org/x/sys/unix"
)
var eglVendorDirs = []string{"/etc/glvnd/egl_vendor.d", "/usr/share/glvnd/egl_vendor.d"}
const (
dmabufFeedbackMinVersion = 4
// v6 stops sending main_device
dmabufMainDeviceMaxVersion = 5
compositorProbeTimeout = 2 * time.Second
sysDevCharDir = "/sys/dev/char"
)
func EGLVendorEnv() []string {
if os.Getenv("__EGL_VENDOR_LIBRARY_FILENAMES") != "" || os.Getenv("__EGL_VENDOR_LIBRARY_DIRS") != "" {
return nil
}
mesa, nvidia := findEGLVendorICDs(eglVendorDirs)
if mesa == "" || nvidia == "" {
return nil
}
driver, err := compositorRenderDriver()
if err != nil {
log.Debugf("egl: keeping glvnd vendor defaults: %v", err)
return nil
}
if driver == "nvidia" {
return nil
}
log.Infof("egl: compositor renders on %s, loading mesa vendor only", driver)
return []string{"__EGL_VENDOR_LIBRARY_FILENAMES=" + mesa}
}
type eglVendorICD struct {
ICD struct {
LibraryPath string `json:"library_path"`
} `json:"ICD"`
}
func findEGLVendorICDs(dirs []string) (mesa, nvidia string) {
for _, dir := range dirs {
entries, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
continue
}
for _, path := range entries {
switch eglVendorLibrary(path) {
case "libEGL_mesa":
if mesa == "" {
mesa = path
}
case "libEGL_nvidia":
if nvidia == "" {
nvidia = path
}
}
}
}
return mesa, nvidia
}
func eglVendorLibrary(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
var icd eglVendorICD
if err := json.Unmarshal(data, &icd); err != nil {
return ""
}
base := filepath.Base(icd.ICD.LibraryPath)
name, _, _ := strings.Cut(base, ".so")
return name
}
func compositorRenderDriver() (string, error) {
dev, err := compositorMainDevice()
if err != nil {
return "", err
}
return drmDeviceDriver(sysDevCharDir, dev)
}
func drmDeviceDriver(charDir string, dev uint64) (string, error) {
link := filepath.Join(charDir, fmt.Sprintf("%d:%d", unix.Major(dev), unix.Minor(dev)), "device", "driver")
target, err := os.Readlink(link)
if err != nil {
return "", err
}
return filepath.Base(target), nil
}
func compositorMainDevice() (uint64, error) {
display, err := wlclient.Connect("")
if err != nil {
return 0, err
}
ctx := display.Context()
defer ctx.Close()
if err := ctx.SetReadDeadline(time.Now().Add(compositorProbeTimeout)); err != nil {
return 0, err
}
registry, err := display.GetRegistry()
if err != nil {
return 0, err
}
var dmabuf *linux_dmabuf.ZwpLinuxDmabufV1
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface != linux_dmabuf.ZwpLinuxDmabufV1InterfaceName || e.Version < dmabufFeedbackMinVersion {
return
}
candidate := linux_dmabuf.NewZwpLinuxDmabufV1(ctx)
if err := registry.Bind(e.Name, e.Interface, min(e.Version, dmabufMainDeviceMaxVersion), candidate); err != nil {
return
}
dmabuf = candidate
})
if err := display.Roundtrip(); err != nil {
return 0, err
}
if dmabuf == nil {
return 0, errors.New("compositor lacks linux-dmabuf feedback")
}
defer dmabuf.Destroy()
feedback, err := dmabuf.GetDefaultFeedback()
if err != nil {
return 0, err
}
defer feedback.Destroy()
var device []byte
done := false
feedback.SetMainDeviceHandler(func(e linux_dmabuf.ZwpLinuxDmabufFeedbackV1MainDeviceEvent) {
device = e.Device
})
feedback.SetDoneHandler(func(linux_dmabuf.ZwpLinuxDmabufFeedbackV1DoneEvent) {
done = true
})
for !done {
if err := ctx.Dispatch(); err != nil {
return 0, err
}
}
return mainDeviceFromEvent(device)
}
func mainDeviceFromEvent(device []byte) (uint64, error) {
if len(device) != 8 {
return 0, fmt.Errorf("main_device has %d bytes, want 8", len(device))
}
return binary.NativeEndian.Uint64(device), nil
}
+79
View File
@@ -0,0 +1,79 @@
package gpu
import (
"encoding/binary"
"os"
"path/filepath"
"testing"
)
func writeICD(t *testing.T, dir, name, library string) string {
t.Helper()
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(`{"file_format_version":"1.0.0","ICD":{"library_path":"`+library+`"}}`), 0o644); err != nil {
t.Fatal(err)
}
return path
}
func TestFindEGLVendorICDs(t *testing.T) {
etc := t.TempDir()
usr := t.TempDir()
writeICD(t, usr, "10_nvidia.json", "libEGL_nvidia.so.0")
mesaPath := writeICD(t, usr, "50_mesa.json", "libEGL_mesa.so.0")
nvidiaPath := writeICD(t, etc, "10_nvidia.json", "/usr/lib/libEGL_nvidia.so.0")
if err := os.WriteFile(filepath.Join(etc, "broken.json"), []byte("{"), 0o644); err != nil {
t.Fatal(err)
}
mesa, nvidia := findEGLVendorICDs([]string{etc, usr})
if mesa != mesaPath {
t.Errorf("mesa = %q, want %q", mesa, mesaPath)
}
if nvidia != nvidiaPath {
t.Errorf("nvidia = %q, want %q", nvidia, nvidiaPath)
}
mesa, nvidia = findEGLVendorICDs([]string{filepath.Join(etc, "missing")})
if mesa != "" || nvidia != "" {
t.Errorf("missing dir: got %q %q, want empty", mesa, nvidia)
}
}
func TestDrmDeviceDriver(t *testing.T) {
root := t.TempDir()
charDir := filepath.Join(root, "char")
deviceDir := filepath.Join(charDir, "226:128", "device")
if err := os.MkdirAll(deviceDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink("../../../../bus/pci/drivers/amdgpu", filepath.Join(deviceDir, "driver")); err != nil {
t.Fatal(err)
}
driver, err := drmDeviceDriver(charDir, 226<<8|128)
if err != nil {
t.Fatal(err)
}
if driver != "amdgpu" {
t.Errorf("driver = %q, want amdgpu", driver)
}
if _, err := drmDeviceDriver(charDir, 226<<8|129); err == nil {
t.Error("unknown device: want error")
}
}
func TestMainDeviceFromEvent(t *testing.T) {
raw := make([]byte, 8)
binary.NativeEndian.PutUint64(raw, 226<<8|128)
dev, err := mainDeviceFromEvent(raw)
if err != nil {
t.Fatal(err)
}
if dev != 226<<8|128 {
t.Errorf("dev = %d", dev)
}
if _, err := mainDeviceFromEvent(raw[:4]); err == nil {
t.Error("short array: want error")
}
}
+110 -9
View File
@@ -34,8 +34,12 @@ var orderedConfigNames = []string{"niri", "hyprland", "ghostty", "kitty", "alacr
// Config holds all CLI parameters for unattended installation.
type Config struct {
Compositor string // "niri" or "hyprland"
Terminal string // "ghostty", "kitty", or "alacritty"
Compositor string // "niri", "hyprland", or "mango"
Terminal string // "ghostty", "kitty", or "alacritty"
PrivescTool string // "sudo", "doas", or "run0"; empty means autodetect
GitAll bool // git variant for every dep that supports it
GitDeps []string // deps to force the git variant of
AllFeatures bool // enable all optional deps
IncludeDeps []string
ExcludeDeps []string
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
@@ -43,6 +47,7 @@ type Config struct {
Yes bool
DankSearch bool // install danksearch and enable its user service
DankCalendar bool // install dankcalendar
DmsGreeter bool // install dms-greeter
}
// Runner orchestrates unattended (headless) installation.
@@ -68,6 +73,10 @@ func (r *Runner) GetLogChan() <-chan string {
func (r *Runner) Run() error {
r.log("Starting headless installation")
if err := r.applyPrivescTool(); err != nil {
return err
}
// 1. Parse compositor and terminal selections
wm, err := r.parseWindowManager()
if err != nil {
@@ -112,14 +121,16 @@ func (r *Runner) Run() error {
return fmt.Errorf("dependency detection failed: %w", err)
}
reinstallItems, err := r.applyGitVariants(dependencies, gitVariantChecker(distro, wm, dependencies))
if err != nil {
return err
}
// 5. Apply include/exclude filters and build the disabled-items map.
// Headless mode does not currently collect any explicit reinstall selections,
// so keep reinstallItems nil instead of constructing an always-empty map.
disabledItems, err := r.buildDisabledItems(dependencies)
if err != nil {
return err
}
var reinstallItems map[string]bool
// Print dependency summary
fmt.Fprintln(os.Stdout, "\nDependencies:")
@@ -129,6 +140,9 @@ func (r *Runner) Run() error {
if disabledItems[dep.Name] {
marker = " SKIP "
status = "(disabled)"
} else if reinstallItems[dep.Name] {
marker = " RE "
status = "(will reinstall)"
} else {
switch dep.Status {
case deps.StatusInstalled:
@@ -283,15 +297,96 @@ func (r *Runner) Run() error {
return nil
}
func (r *Runner) applyPrivescTool() error {
if r.cfg.PrivescTool == "" {
return nil
}
switch tool := privesc.Tool(strings.ToLower(r.cfg.PrivescTool)); tool {
case privesc.ToolSudo, privesc.ToolDoas, privesc.ToolRun0:
if err := privesc.SetTool(tool); err != nil {
return fmt.Errorf("--privesc: %w", err)
}
return nil
default:
return fmt.Errorf("invalid --privesc value %q: must be 'sudo', 'doas', or 'run0'", r.cfg.PrivescTool)
}
}
// CanToggle is not enough: some distros discard the variant (hyprland on Arch maps to stable either way)
func gitVariantChecker(distro distros.Distribution, wm deps.WindowManager, dependencies []deps.Dependency) func(name string) bool {
stable := distro.GetPackageMappingWithVariants(wm, map[string]deps.PackageVariant{})
allGit := make(map[string]deps.PackageVariant, len(dependencies))
for _, dep := range dependencies {
allGit[dep.Name] = deps.VariantGit
}
git := distro.GetPackageMappingWithVariants(wm, allGit)
return func(name string) bool {
mapping, ok := git[name]
return ok && mapping != stable[name]
}
}
func (r *Runner) applyGitVariants(dependencies []deps.Dependency, hasGitVariant func(name string) bool) (map[string]bool, error) {
reinstallItems := make(map[string]bool)
// InstallPackages skips StatusInstalled deps unless marked for reinstall
markGit := func(dep *deps.Dependency) {
if dep.Variant == deps.VariantStable && dep.Status == deps.StatusInstalled {
reinstallItems[dep.Name] = true
}
dep.Variant = deps.VariantGit
}
for _, name := range r.cfg.GitDeps {
name = strings.ToLower(strings.TrimSpace(name))
if name == "" {
continue
}
dep := findDepFold(dependencies, name)
if dep == nil {
return nil, fmt.Errorf("--git-deps: unknown dependency %q", name)
}
if !dep.CanToggle || !hasGitVariant(dep.Name) {
return nil, fmt.Errorf("--git-deps: %q does not have a git variant on this distribution", dep.Name)
}
markGit(dep)
}
if !r.cfg.GitAll {
return reinstallItems, nil
}
for i := range dependencies {
if dependencies[i].CanToggle && hasGitVariant(dependencies[i].Name) {
markGit(&dependencies[i])
}
}
return reinstallItems, nil
}
func findDepFold(dependencies []deps.Dependency, lowerName string) *deps.Dependency {
if lowerName == "dms" {
lowerName = "dms (dankmaterialshell)"
}
for i := range dependencies {
if strings.ToLower(dependencies[i].Name) == lowerName {
return &dependencies[i]
}
}
return nil
}
// buildDisabledItems computes the set of dependencies that should be skipped
// during installation. Optional components are opt-in (disabled by default),
// then re-enabled by the dedicated flags and --include-deps.
// then re-enabled by --all-features, the dedicated flags, and --include-deps.
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
disabledItems := make(map[string]bool)
for i := range dependencies {
if !dependencies[i].Required {
disabledItems[dependencies[i].Name] = true
if !r.cfg.AllFeatures {
for i := range dependencies {
if !dependencies[i].Required {
disabledItems[dependencies[i].Name] = true
}
}
}
@@ -308,6 +403,12 @@ func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]
}
delete(disabledItems, "dankcalendar")
}
if r.cfg.DmsGreeter {
if !r.depExists(dependencies, "dms-greeter") {
return nil, fmt.Errorf("--dms-greeter: not available on this distribution")
}
delete(disabledItems, "dms-greeter")
}
// Process --include-deps (enable items that are disabled by default)
for _, name := range r.cfg.IncludeDeps {
+179 -1
View File
@@ -146,7 +146,7 @@ func TestLog(t *testing.T) {
r := NewRunner(Config{})
// log should not block even if channel is full
for i := 0; i < 1100; i++ {
for range 1100 {
r.log("message")
}
// If we reach here without hanging, the non-blocking send works
@@ -357,6 +357,8 @@ func TestBuildDisabledItems(t *testing.T) {
excludeDeps []string
dankSearch bool
dankCalendar bool
dmsGreeter bool
allFeatures bool
deps []deps.Dependency // nil means use the shared fixture
wantErr bool
errContains string // substring expected in error message
@@ -442,6 +444,31 @@ func TestBuildDisabledItems(t *testing.T) {
wantErr: true,
errContains: "--dankcalendar",
},
{
name: "dms-greeter flag enables it",
dmsGreeter: true,
wantEnabled: []string{"dms-greeter"},
wantDisabled: []string{"danksearch", "dankcalendar"},
},
{
name: "dms-greeter flag when unavailable errors",
dmsGreeter: true,
deps: []deps.Dependency{{Name: "niri", Status: deps.StatusInstalled, Required: true}},
wantErr: true,
errContains: "--dms-greeter",
},
{
name: "all-features enables every optional dep",
allFeatures: true,
wantEnabled: []string{"dms-greeter", "danksearch", "dankcalendar", "niri", "ghostty", "waybar"},
},
{
name: "all-features with exclude still disables the excluded dep",
allFeatures: true,
excludeDeps: []string{"dankcalendar"},
wantEnabled: []string{"dms-greeter", "danksearch"},
wantDisabled: []string{"dankcalendar"},
},
}
for _, tt := range tests {
@@ -451,6 +478,8 @@ func TestBuildDisabledItems(t *testing.T) {
ExcludeDeps: tt.excludeDeps,
DankSearch: tt.dankSearch,
DankCalendar: tt.dankCalendar,
DmsGreeter: tt.dmsGreeter,
AllFeatures: tt.allFeatures,
})
d := tt.deps
if d == nil {
@@ -491,3 +520,152 @@ func TestBuildDisabledItems(t *testing.T) {
})
}
}
func TestApplyGitVariants(t *testing.T) {
freshDeps := func() []deps.Dependency {
return []deps.Dependency{
{Name: "niri", Status: deps.StatusInstalled, Variant: deps.VariantStable, CanToggle: true},
{Name: "quickshell", Status: deps.StatusMissing, Variant: deps.VariantStable, CanToggle: true},
{Name: "hyprland", Status: deps.StatusInstalled, Variant: deps.VariantGit, CanToggle: true},
{Name: "dms (DankMaterialShell)", Status: deps.StatusMissing, Variant: deps.VariantStable, CanToggle: true},
{Name: "ghostty", Status: deps.StatusInstalled, Variant: deps.VariantStable, CanToggle: false},
}
}
tests := []struct {
name string
gitAll bool
gitDeps []string
noGitVariant []string // deps whose git and stable packages resolve identically
wantErr bool
errContains string
wantGit []string
wantStable []string
wantReinstall []string
}{
{
name: "no git flags leaves variants untouched",
wantGit: []string{"hyprland"},
wantStable: []string{"niri", "quickshell", "dms (DankMaterialShell)", "ghostty"},
},
{
name: "git-deps flips only the named deps",
gitDeps: []string{"niri", "quickshell"},
wantGit: []string{"niri", "quickshell", "hyprland"},
wantStable: []string{"dms (DankMaterialShell)", "ghostty"},
wantReinstall: []string{"niri"},
},
{
name: "git-deps matches names case-insensitively and accepts the dms alias",
gitDeps: []string{"NIRI", "dms"},
wantGit: []string{"niri", "dms (DankMaterialShell)", "hyprland"},
wantStable: []string{"quickshell", "ghostty"},
wantReinstall: []string{"niri"},
},
{
name: "empty and whitespace entries are skipped",
gitDeps: []string{"", " "},
wantGit: []string{"hyprland"},
wantStable: []string{"niri", "quickshell", "dms (DankMaterialShell)", "ghostty"},
},
{
name: "unknown dep errors",
gitDeps: []string{"nonexistent"},
wantErr: true,
errContains: "--git-deps",
},
{
name: "dep without git variant errors",
gitDeps: []string{"ghostty"},
wantErr: true,
errContains: "does not have a git variant",
},
{
name: "git flips every toggleable dep",
gitAll: true,
wantGit: []string{"niri", "quickshell", "hyprland", "dms (DankMaterialShell)"},
wantStable: []string{"ghostty"},
wantReinstall: []string{"niri"},
},
{
name: "git-deps errors when the distro has no distinct git package",
gitDeps: []string{"niri"},
noGitVariant: []string{"niri"},
wantErr: true,
errContains: "does not have a git variant",
},
{
name: "git skips deps without a distinct git package",
gitAll: true,
noGitVariant: []string{"niri"},
wantGit: []string{"quickshell", "hyprland", "dms (DankMaterialShell)"},
wantStable: []string{"niri", "ghostty"},
wantReinstall: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewRunner(Config{GitAll: tt.gitAll, GitDeps: tt.gitDeps})
d := freshDeps()
hasGitVariant := func(name string) bool {
for _, n := range tt.noGitVariant {
if n == name {
return false
}
}
return true
}
reinstall, err := r.applyGitVariants(d, hasGitVariant)
if (err != nil) != tt.wantErr {
t.Fatalf("applyGitVariants() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
}
return
}
variants := make(map[string]deps.PackageVariant)
for _, dep := range d {
variants[dep.Name] = dep.Variant
}
for _, name := range tt.wantGit {
if variants[name] != deps.VariantGit {
t.Errorf("expected %q to be VariantGit", name)
}
}
for _, name := range tt.wantStable {
if variants[name] != deps.VariantStable {
t.Errorf("expected %q to stay VariantStable", name)
}
}
if len(reinstall) != len(tt.wantReinstall) {
t.Errorf("reinstall = %v, want %v", reinstall, tt.wantReinstall)
}
for _, name := range tt.wantReinstall {
if !reinstall[name] {
t.Errorf("expected %q to be marked for reinstall", name)
}
}
})
}
}
func TestApplyPrivescTool(t *testing.T) {
if err := NewRunner(Config{}).applyPrivescTool(); err != nil {
t.Errorf("empty PrivescTool should be a no-op, got error: %v", err)
}
err := NewRunner(Config{PrivescTool: "pkexec"}).applyPrivescTool()
if err == nil {
t.Fatal("expected error for unsupported privesc tool, got nil")
}
if !strings.Contains(err.Error(), "--privesc") {
t.Errorf("error %q does not mention --privesc", err.Error())
}
}
+118
View File
@@ -0,0 +1,118 @@
Copyright (C) 2017 Alec Thomas
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
// formatters/svg/font_liberation_mono.go
Digitized data copyright (c) 2010 Google Corporation
with Reserved Font Arimo, Tinos and Cousine.
Copyright (c) 2012 Red Hat, Inc.
with Reserved Font Name Liberation.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+8
View File
@@ -0,0 +1,8 @@
// Package highlight replaces chroma's lexers and styles packages, whose
// package init parses every embedded XML definition (~10ms on every dms
// invocation). The copies under lexers/ and styles/ are chroma's own files,
// refreshed by go generate; only lexers/lexers.go and styles/styles.go are
// ours and build the registries on first use.
package highlight
//go:generate sh generate.sh
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Refreshes the chroma lexer and style copies from the module version in go.mod.
set -e
d=$(go list -m -f '{{.Dir}}' github.com/alecthomas/chroma/v2)
rm -f lexers/embedded/*.xml styles/*.xml
find lexers -maxdepth 1 -name '*.go' ! -name lexers.go -delete
install -m 0644 "$d"/lexers/embedded/*.xml lexers/embedded/
install -m 0644 "$d"/styles/*.xml styles/
install -m 0644 "$d"/COPYING COPYING
for f in "$d"/lexers/*.go; do
case $(basename "$f") in
lexers.go | dns.go | mysql.go | zed.go | *_test.go) continue ;;
esac
install -m 0644 "$f" lexers/
done
+29
View File
@@ -0,0 +1,29 @@
package highlight
import (
"bytes"
"strings"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight/styles"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/yuin/goldmark"
)
func TestMarkdown(t *testing.T) {
md := goldmark.New(goldmark.WithExtensions(Markdown{Style: styles.Get("monokai"), Formatter: html.New(html.WithClasses(false))}))
cases := []struct{ in, want string }{
{"```go\nfunc main() {}\n```\n", `style="color:`},
{"```nope\na < b\n```\n", `<pre><code class="language-nope">a &lt; b`},
{"```\nplain\n```\n", `<pre><code>plain`},
}
for _, tc := range cases {
var out bytes.Buffer
if err := md.Convert([]byte(tc.in), &out); err != nil {
t.Fatal(err)
}
if !strings.Contains(out.String(), tc.want) {
t.Errorf("%q rendered %q, want %q", tc.in, out.String(), tc.want)
}
}
}
+275
View File
@@ -0,0 +1,275 @@
package lexers
import (
. "github.com/alecthomas/chroma/v2" // nolint
)
// Matcher token stub for docs, or
// Named matcher: @name, or
// Path matcher: /foo, or
// Wildcard path matcher: *
// nolint: gosec
var caddyfileMatcherTokenRegexp = `(\[\<matcher\>\]|@[^\s]+|/[^\s]+|\*)`
// Comment at start of line, or
// Comment preceded by whitespace
var caddyfileCommentRegexp = `(^|\s+)#.*\n`
// caddyfileCommon are the rules common to both of the lexer variants
func caddyfileCommonRules() Rules {
return Rules{
"site_block_common": {
Include("site_body"),
// Any other directive
{Pattern: `[^\s#]+`, Type: Keyword, Mutator: Push("directive")},
Include("base"),
},
"site_body": {
// Import keyword
{Pattern: `\b(import|invoke)\b( [^\s#]+)`, Type: ByGroups(Keyword, Text), Mutator: Push("subdirective")},
// Matcher definition
{Pattern: `@[^\s]+(?=\s)`, Type: NameDecorator, Mutator: Push("matcher")},
// Matcher token stub for docs
{Pattern: `\[\<matcher\>\]`, Type: NameDecorator, Mutator: Push("matcher")},
// These cannot have matchers but may have things that look like
// matchers in their arguments, so we just parse as a subdirective.
{Pattern: `\b(try_files|tls|log|bind)\b`, Type: Keyword, Mutator: Push("subdirective")},
// These are special, they can nest more directives
{Pattern: `\b(handle_errors|handle_path|handle_response|replace_status|handle|route)\b`, Type: Keyword, Mutator: Push("nested_directive")},
// uri directive has special syntax
{Pattern: `\b(uri)\b`, Type: Keyword, Mutator: Push("uri_directive")},
},
"matcher": {
{Pattern: `\{`, Type: Punctuation, Mutator: Push("block")},
// Not can be one-liner
{Pattern: `not`, Type: Keyword, Mutator: Push("deep_not_matcher")},
// Heredoc for CEL expression
Include("heredoc"),
// Backtick for CEL expression
{Pattern: "`", Type: StringBacktick, Mutator: Push("backticks")},
// Any other same-line matcher
{Pattern: `[^\s#]+`, Type: Keyword, Mutator: Push("arguments")},
// Terminators
{Pattern: `\s*\n`, Type: Text, Mutator: Pop(1)},
{Pattern: `\}`, Type: Punctuation, Mutator: Pop(1)},
Include("base"),
},
"block": {
{Pattern: `\}`, Type: Punctuation, Mutator: Pop(2)},
// Using double quotes doesn't stop at spaces
{Pattern: `"`, Type: StringDouble, Mutator: Push("double_quotes")},
// Using backticks doesn't stop at spaces
{Pattern: "`", Type: StringBacktick, Mutator: Push("backticks")},
// Not can be one-liner
{Pattern: `not`, Type: Keyword, Mutator: Push("not_matcher")},
// Directives & matcher definitions
Include("site_body"),
// Any directive
{Pattern: `[^\s#]+`, Type: Keyword, Mutator: Push("subdirective")},
Include("base"),
},
"nested_block": {
{Pattern: `\}`, Type: Punctuation, Mutator: Pop(2)},
// Using double quotes doesn't stop at spaces
{Pattern: `"`, Type: StringDouble, Mutator: Push("double_quotes")},
// Using backticks doesn't stop at spaces
{Pattern: "`", Type: StringBacktick, Mutator: Push("backticks")},
// Not can be one-liner
{Pattern: `not`, Type: Keyword, Mutator: Push("not_matcher")},
// Directives & matcher definitions
Include("site_body"),
// Any other subdirective
{Pattern: `[^\s#]+`, Type: Keyword, Mutator: Push("directive")},
Include("base"),
},
"not_matcher": {
{Pattern: `\}`, Type: Punctuation, Mutator: Pop(2)},
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("block")},
{Pattern: `[^\s#]+`, Type: Keyword, Mutator: Push("arguments")},
{Pattern: `\s+`, Type: Text, Mutator: nil},
},
"deep_not_matcher": {
{Pattern: `\}`, Type: Punctuation, Mutator: Pop(2)},
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("block")},
{Pattern: `[^\s#]+`, Type: Keyword, Mutator: Push("deep_subdirective")},
{Pattern: `\s+`, Type: Text, Mutator: nil},
},
"directive": {
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("block")},
{Pattern: caddyfileMatcherTokenRegexp, Type: NameDecorator, Mutator: Push("arguments")},
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: Pop(1)},
{Pattern: `\s*\n`, Type: Text, Mutator: Pop(1)},
Include("base"),
},
"nested_directive": {
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("nested_block")},
{Pattern: caddyfileMatcherTokenRegexp, Type: NameDecorator, Mutator: Push("nested_arguments")},
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: Pop(1)},
{Pattern: `\s*\n`, Type: Text, Mutator: Pop(1)},
Include("base"),
},
"subdirective": {
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("block")},
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: Pop(1)},
{Pattern: `\s*\n`, Type: Text, Mutator: Pop(1)},
Include("base"),
},
"arguments": {
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("block")},
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: Pop(2)},
{Pattern: `\\\n`, Type: Text, Mutator: nil}, // Skip escaped newlines
{Pattern: `\s*\n`, Type: Text, Mutator: Pop(2)},
Include("base"),
},
"nested_arguments": {
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("nested_block")},
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: Pop(2)},
{Pattern: `\\\n`, Type: Text, Mutator: nil}, // Skip escaped newlines
{Pattern: `\s*\n`, Type: Text, Mutator: Pop(2)},
Include("base"),
},
"deep_subdirective": {
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("block")},
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: Pop(3)},
{Pattern: `\s*\n`, Type: Text, Mutator: Pop(3)},
Include("base"),
},
"uri_directive": {
{Pattern: `\{(?=\s)`, Type: Punctuation, Mutator: Push("block")},
{Pattern: caddyfileMatcherTokenRegexp, Type: NameDecorator, Mutator: nil},
{Pattern: `(strip_prefix|strip_suffix|replace|path_regexp)`, Type: NameConstant, Mutator: Push("arguments")},
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: Pop(1)},
{Pattern: `\s*\n`, Type: Text, Mutator: Pop(1)},
Include("base"),
},
"double_quotes": {
Include("placeholder"),
{Pattern: `\\"`, Type: StringDouble, Mutator: nil},
{Pattern: `[^"]`, Type: StringDouble, Mutator: nil},
{Pattern: `"`, Type: StringDouble, Mutator: Pop(1)},
},
"backticks": {
Include("placeholder"),
{Pattern: "\\\\`", Type: StringBacktick, Mutator: nil},
{Pattern: "[^`]", Type: StringBacktick, Mutator: nil},
{Pattern: "`", Type: StringBacktick, Mutator: Pop(1)},
},
"optional": {
// Docs syntax for showing optional parts with [ ]
{Pattern: `\[`, Type: Punctuation, Mutator: Push("optional")},
Include("name_constants"),
{Pattern: `\|`, Type: Punctuation, Mutator: nil},
{Pattern: `[^\[\]\|]+`, Type: String, Mutator: nil},
{Pattern: `\]`, Type: Punctuation, Mutator: Pop(1)},
},
"heredoc": {
{Pattern: `(<<([a-zA-Z0-9_-]+))(\n(.*|\n)*)(\s*)(\2)`, Type: ByGroups(StringHeredoc, nil, String, String, String, StringHeredoc), Mutator: nil},
},
"name_constants": {
{Pattern: `\b(most_recently_modified|largest_size|smallest_size|first_exist|internal|disable_redirects|ignore_loaded_certs|disable_certs|private_ranges|first|last|before|after|on|off)\b(\||(?=\]|\s|$))`, Type: ByGroups(NameConstant, Punctuation), Mutator: nil},
},
"placeholder": {
// Placeholder with dots, colon for default value, brackets for args[0:]
{Pattern: `\{[\w+.\[\]\:\$-]+\}`, Type: StringEscape, Mutator: nil},
// Handle opening brackets with no matching closing one
{Pattern: `\{[^\}\s]*\b`, Type: String, Mutator: nil},
},
"base": {
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: nil},
{Pattern: `\[\<matcher\>\]`, Type: NameDecorator, Mutator: nil},
Include("name_constants"),
Include("heredoc"),
{Pattern: `(https?://)?([a-z0-9.-]+)(:)([0-9]+)([^\s]*)`, Type: ByGroups(Name, Name, Punctuation, NumberInteger, Name), Mutator: nil},
{Pattern: `\[`, Type: Punctuation, Mutator: Push("optional")},
{Pattern: "`", Type: StringBacktick, Mutator: Push("backticks")},
{Pattern: `"`, Type: StringDouble, Mutator: Push("double_quotes")},
Include("placeholder"),
{Pattern: `[a-z-]+/[a-z-+]+`, Type: String, Mutator: nil},
{Pattern: `[0-9]+([smhdk]|ns|us|µs|ms)?\b`, Type: NumberInteger, Mutator: nil},
{Pattern: `[^\s\n#\{]+`, Type: String, Mutator: nil},
{Pattern: `/[^\s#]*`, Type: Name, Mutator: nil},
{Pattern: `\s+`, Type: Text, Mutator: nil},
},
}
}
// Caddyfile lexer.
var Caddyfile = Register(MustNewLexer(
&Config{
Name: "Caddyfile",
Aliases: []string{"caddyfile", "caddy"},
Filenames: []string{"Caddyfile*"},
MimeTypes: []string{},
},
caddyfileRules,
))
func caddyfileRules() Rules {
return Rules{
"root": {
{Pattern: caddyfileCommentRegexp, Type: CommentSingle, Mutator: nil},
// Global options block
{Pattern: `^\s*(\{)\s*$`, Type: ByGroups(Punctuation), Mutator: Push("globals")},
// Top level import
{Pattern: `(import)(\s+)([^\s]+)`, Type: ByGroups(Keyword, Text, NameVariableMagic), Mutator: nil},
// Snippets
{Pattern: `(&?\([^\s#]+\))(\s*)(\{)`, Type: ByGroups(NameVariableAnonymous, Text, Punctuation), Mutator: Push("snippet")},
// Site label
{Pattern: `[^#{(\s,]+`, Type: GenericHeading, Mutator: Push("label")},
// Site label with placeholder
{Pattern: `\{[\w+.\[\]\:\$-]+\}`, Type: StringEscape, Mutator: Push("label")},
{Pattern: `\s+`, Type: Text, Mutator: nil},
},
"globals": {
{Pattern: `\}`, Type: Punctuation, Mutator: Pop(1)},
// Global options are parsed as subdirectives (no matcher)
{Pattern: `[^\s#]+`, Type: Keyword, Mutator: Push("subdirective")},
Include("base"),
},
"snippet": {
{Pattern: `\}`, Type: Punctuation, Mutator: Pop(1)},
Include("site_body"),
// Any other directive
{Pattern: `[^\s#]+`, Type: Keyword, Mutator: Push("directive")},
Include("base"),
},
"label": {
// Allow multiple labels, comma separated, newlines after
// a comma means another label is coming
{Pattern: `,\s*\n?`, Type: Text, Mutator: nil},
{Pattern: ` `, Type: Text, Mutator: nil},
// Site label with placeholder
Include("placeholder"),
// Site label
{Pattern: `[^#{(\s,]+`, Type: GenericHeading, Mutator: nil},
// Comment after non-block label (hack because comments end in \n)
{Pattern: `#.*\n`, Type: CommentSingle, Mutator: Push("site_block")},
// Note: if \n, we'll never pop out of the site_block, it's valid
{Pattern: `\{(?=\s)|\n`, Type: Punctuation, Mutator: Push("site_block")},
},
"site_block": {
{Pattern: `\}`, Type: Punctuation, Mutator: Pop(2)},
Include("site_block_common"),
},
}.Merge(caddyfileCommonRules())
}
// Caddyfile directive-only lexer.
var CaddyfileDirectives = Register(MustNewLexer(
&Config{
Name: "Caddyfile Directives",
Aliases: []string{"caddyfile-directives", "caddyfile-d", "caddy-d"},
Filenames: []string{},
MimeTypes: []string{},
},
caddyfileDirectivesRules,
))
func caddyfileDirectivesRules() Rules {
return Rules{
// Same as "site_block" in Caddyfile
"root": {
Include("site_block_common"),
},
}.Merge(caddyfileCommonRules())
}
+243
View File
@@ -0,0 +1,243 @@
package lexers
import (
. "github.com/alecthomas/chroma/v2" // nolint
)
var (
clBuiltinFunctions = []string{
"<", "<=", "=", ">", ">=", "-", "/", "/=", "*", "+", "1-", "1+",
"abort", "abs", "acons", "acos", "acosh", "add-method", "adjoin",
"adjustable-array-p", "adjust-array", "allocate-instance",
"alpha-char-p", "alphanumericp", "append", "apply", "apropos",
"apropos-list", "aref", "arithmetic-error-operands",
"arithmetic-error-operation", "array-dimension", "array-dimensions",
"array-displacement", "array-element-type", "array-has-fill-pointer-p",
"array-in-bounds-p", "arrayp", "array-rank", "array-row-major-index",
"array-total-size", "ash", "asin", "asinh", "assoc", "assoc-if",
"assoc-if-not", "atan", "atanh", "atom", "bit", "bit-and", "bit-andc1",
"bit-andc2", "bit-eqv", "bit-ior", "bit-nand", "bit-nor", "bit-not",
"bit-orc1", "bit-orc2", "bit-vector-p", "bit-xor", "boole",
"both-case-p", "boundp", "break", "broadcast-stream-streams",
"butlast", "byte", "byte-position", "byte-size", "caaaar", "caaadr",
"caaar", "caadar", "caaddr", "caadr", "caar", "cadaar", "cadadr",
"cadar", "caddar", "cadddr", "caddr", "cadr", "call-next-method", "car",
"cdaaar", "cdaadr", "cdaar", "cdadar", "cdaddr", "cdadr", "cdar",
"cddaar", "cddadr", "cddar", "cdddar", "cddddr", "cdddr", "cddr", "cdr",
"ceiling", "cell-error-name", "cerror", "change-class", "char", "char<",
"char<=", "char=", "char>", "char>=", "char/=", "character",
"characterp", "char-code", "char-downcase", "char-equal",
"char-greaterp", "char-int", "char-lessp", "char-name",
"char-not-equal", "char-not-greaterp", "char-not-lessp", "char-upcase",
"cis", "class-name", "class-of", "clear-input", "clear-output",
"close", "clrhash", "code-char", "coerce", "compile",
"compiled-function-p", "compile-file", "compile-file-pathname",
"compiler-macro-function", "complement", "complex", "complexp",
"compute-applicable-methods", "compute-restarts", "concatenate",
"concatenated-stream-streams", "conjugate", "cons", "consp",
"constantly", "constantp", "continue", "copy-alist", "copy-list",
"copy-pprint-dispatch", "copy-readtable", "copy-seq", "copy-structure",
"copy-symbol", "copy-tree", "cos", "cosh", "count", "count-if",
"count-if-not", "decode-float", "decode-universal-time", "delete",
"delete-duplicates", "delete-file", "delete-if", "delete-if-not",
"delete-package", "denominator", "deposit-field", "describe",
"describe-object", "digit-char", "digit-char-p", "directory",
"directory-namestring", "disassemble", "documentation", "dpb",
"dribble", "echo-stream-input-stream", "echo-stream-output-stream",
"ed", "eighth", "elt", "encode-universal-time", "endp",
"enough-namestring", "ensure-directories-exist",
"ensure-generic-function", "eq", "eql", "equal", "equalp", "error",
"eval", "evenp", "every", "exp", "export", "expt", "fboundp",
"fceiling", "fdefinition", "ffloor", "fifth", "file-author",
"file-error-pathname", "file-length", "file-namestring",
"file-position", "file-string-length", "file-write-date",
"fill", "fill-pointer", "find", "find-all-symbols", "find-class",
"find-if", "find-if-not", "find-method", "find-package", "find-restart",
"find-symbol", "finish-output", "first", "float", "float-digits",
"floatp", "float-precision", "float-radix", "float-sign", "floor",
"fmakunbound", "force-output", "format", "fourth", "fresh-line",
"fround", "ftruncate", "funcall", "function-keywords",
"function-lambda-expression", "functionp", "gcd", "gensym", "gentemp",
"get", "get-decoded-time", "get-dispatch-macro-character", "getf",
"gethash", "get-internal-real-time", "get-internal-run-time",
"get-macro-character", "get-output-stream-string", "get-properties",
"get-setf-expansion", "get-universal-time", "graphic-char-p",
"hash-table-count", "hash-table-p", "hash-table-rehash-size",
"hash-table-rehash-threshold", "hash-table-size", "hash-table-test",
"host-namestring", "identity", "imagpart", "import",
"initialize-instance", "input-stream-p", "inspect",
"integer-decode-float", "integer-length", "integerp",
"interactive-stream-p", "intern", "intersection",
"invalid-method-error", "invoke-debugger", "invoke-restart",
"invoke-restart-interactively", "isqrt", "keywordp", "last", "lcm",
"ldb", "ldb-test", "ldiff", "length", "lisp-implementation-type",
"lisp-implementation-version", "list", "list*", "list-all-packages",
"listen", "list-length", "listp", "load",
"load-logical-pathname-translations", "log", "logand", "logandc1",
"logandc2", "logbitp", "logcount", "logeqv", "logical-pathname",
"logical-pathname-translations", "logior", "lognand", "lognor",
"lognot", "logorc1", "logorc2", "logtest", "logxor", "long-site-name",
"lower-case-p", "machine-instance", "machine-type", "machine-version",
"macroexpand", "macroexpand-1", "macro-function", "make-array",
"make-broadcast-stream", "make-concatenated-stream", "make-condition",
"make-dispatch-macro-character", "make-echo-stream", "make-hash-table",
"make-instance", "make-instances-obsolete", "make-list",
"make-load-form", "make-load-form-saving-slots", "make-package",
"make-pathname", "make-random-state", "make-sequence", "make-string",
"make-string-input-stream", "make-string-output-stream", "make-symbol",
"make-synonym-stream", "make-two-way-stream", "makunbound", "map",
"mapc", "mapcan", "mapcar", "mapcon", "maphash", "map-into", "mapl",
"maplist", "mask-field", "max", "member", "member-if", "member-if-not",
"merge", "merge-pathnames", "method-combination-error",
"method-qualifiers", "min", "minusp", "mismatch", "mod",
"muffle-warning", "name-char", "namestring", "nbutlast", "nconc",
"next-method-p", "nintersection", "ninth", "no-applicable-method",
"no-next-method", "not", "notany", "notevery", "nreconc", "nreverse",
"nset-difference", "nset-exclusive-or", "nstring-capitalize",
"nstring-downcase", "nstring-upcase", "nsublis", "nsubst", "nsubst-if",
"nsubst-if-not", "nsubstitute", "nsubstitute-if", "nsubstitute-if-not",
"nth", "nthcdr", "null", "numberp", "numerator", "nunion", "oddp",
"open", "open-stream-p", "output-stream-p", "package-error-package",
"package-name", "package-nicknames", "packagep",
"package-shadowing-symbols", "package-used-by-list", "package-use-list",
"pairlis", "parse-integer", "parse-namestring", "pathname",
"pathname-device", "pathname-directory", "pathname-host",
"pathname-match-p", "pathname-name", "pathnamep", "pathname-type",
"pathname-version", "peek-char", "phase", "plusp", "position",
"position-if", "position-if-not", "pprint", "pprint-dispatch",
"pprint-fill", "pprint-indent", "pprint-linear", "pprint-newline",
"pprint-tab", "pprint-tabular", "prin1", "prin1-to-string", "princ",
"princ-to-string", "print", "print-object", "probe-file", "proclaim",
"provide", "random", "random-state-p", "rassoc", "rassoc-if",
"rassoc-if-not", "rational", "rationalize", "rationalp", "read",
"read-byte", "read-char", "read-char-no-hang", "read-delimited-list",
"read-from-string", "read-line", "read-preserving-whitespace",
"read-sequence", "readtable-case", "readtablep", "realp", "realpart",
"reduce", "reinitialize-instance", "rem", "remhash", "remove",
"remove-duplicates", "remove-if", "remove-if-not", "remove-method",
"remprop", "rename-file", "rename-package", "replace", "require",
"rest", "restart-name", "revappend", "reverse", "room", "round",
"row-major-aref", "rplaca", "rplacd", "sbit", "scale-float", "schar",
"search", "second", "set", "set-difference",
"set-dispatch-macro-character", "set-exclusive-or",
"set-macro-character", "set-pprint-dispatch", "set-syntax-from-char",
"seventh", "shadow", "shadowing-import", "shared-initialize",
"short-site-name", "signal", "signum", "simple-bit-vector-p",
"simple-condition-format-arguments", "simple-condition-format-control",
"simple-string-p", "simple-vector-p", "sin", "sinh", "sixth", "sleep",
"slot-boundp", "slot-exists-p", "slot-makunbound", "slot-missing",
"slot-unbound", "slot-value", "software-type", "software-version",
"some", "sort", "special-operator-p", "sqrt", "stable-sort",
"standard-char-p", "store-value", "stream-element-type",
"stream-error-stream", "stream-external-format", "streamp", "string",
"string<", "string<=", "string=", "string>", "string>=", "string/=",
"string-capitalize", "string-downcase", "string-equal",
"string-greaterp", "string-left-trim", "string-lessp",
"string-not-equal", "string-not-greaterp", "string-not-lessp",
"stringp", "string-right-trim", "string-trim", "string-upcase",
"sublis", "subseq", "subsetp", "subst", "subst-if", "subst-if-not",
"substitute", "substitute-if", "substitute-if-not", "subtypep", "svref",
"sxhash", "symbol-function", "symbol-name", "symbolp", "symbol-package",
"symbol-plist", "symbol-value", "synonym-stream-symbol", "syntax:",
"tailp", "tan", "tanh", "tenth", "terpri", "third",
"translate-logical-pathname", "translate-pathname", "tree-equal",
"truename", "truncate", "two-way-stream-input-stream",
"two-way-stream-output-stream", "type-error-datum",
"type-error-expected-type", "type-of", "typep", "unbound-slot-instance",
"unexport", "unintern", "union", "unread-char", "unuse-package",
"update-instance-for-different-class",
"update-instance-for-redefined-class", "upgraded-array-element-type",
"upgraded-complex-part-type", "upper-case-p", "use-package",
"user-homedir-pathname", "use-value", "values", "values-list", "vector",
"vectorp", "vector-pop", "vector-push", "vector-push-extend", "warn",
"wild-pathname-p", "write", "write-byte", "write-char", "write-line",
"write-sequence", "write-string", "write-to-string", "yes-or-no-p",
"y-or-n-p", "zerop",
}
clSpecialForms = []string{
"block", "catch", "declare", "eval-when", "flet", "function", "go", "if",
"labels", "lambda", "let", "let*", "load-time-value", "locally", "macrolet",
"multiple-value-call", "multiple-value-prog1", "progn", "progv", "quote",
"return-from", "setq", "symbol-macrolet", "tagbody", "the", "throw",
"unwind-protect",
}
clMacros = []string{
"and", "assert", "call-method", "case", "ccase", "check-type", "cond",
"ctypecase", "decf", "declaim", "defclass", "defconstant", "defgeneric",
"define-compiler-macro", "define-condition", "define-method-combination",
"define-modify-macro", "define-setf-expander", "define-symbol-macro",
"defmacro", "defmethod", "defpackage", "defparameter", "defsetf",
"defstruct", "deftype", "defun", "defvar", "destructuring-bind", "do",
"do*", "do-all-symbols", "do-external-symbols", "dolist", "do-symbols",
"dotimes", "ecase", "etypecase", "formatter", "handler-bind",
"handler-case", "ignore-errors", "incf", "in-package", "lambda", "loop",
"loop-finish", "make-method", "multiple-value-bind", "multiple-value-list",
"multiple-value-setq", "nth-value", "or", "pop",
"pprint-exit-if-list-exhausted", "pprint-logical-block", "pprint-pop",
"print-unreadable-object", "prog", "prog*", "prog1", "prog2", "psetf",
"psetq", "push", "pushnew", "remf", "restart-bind", "restart-case",
"return", "rotatef", "setf", "shiftf", "step", "time", "trace", "typecase",
"unless", "untrace", "when", "with-accessors", "with-compilation-unit",
"with-condition-restarts", "with-hash-table-iterator",
"with-input-from-string", "with-open-file", "with-open-stream",
"with-output-to-string", "with-package-iterator", "with-simple-restart",
"with-slots", "with-standard-io-syntax",
}
clLambdaListKeywords = []string{
"&allow-other-keys", "&aux", "&body", "&environment", "&key", "&optional",
"&rest", "&whole",
}
clDeclarations = []string{
"dynamic-extent", "ignore", "optimize", "ftype", "inline", "special",
"ignorable", "notinline", "type",
}
clBuiltinTypes = []string{
"atom", "boolean", "base-char", "base-string", "bignum", "bit",
"compiled-function", "extended-char", "fixnum", "keyword", "nil",
"signed-byte", "short-float", "single-float", "double-float", "long-float",
"simple-array", "simple-base-string", "simple-bit-vector", "simple-string",
"simple-vector", "standard-char", "unsigned-byte",
// Condition Types
"arithmetic-error", "cell-error", "condition", "control-error",
"division-by-zero", "end-of-file", "error", "file-error",
"floating-point-inexact", "floating-point-overflow",
"floating-point-underflow", "floating-point-invalid-operation",
"parse-error", "package-error", "print-not-readable", "program-error",
"reader-error", "serious-condition", "simple-condition", "simple-error",
"simple-type-error", "simple-warning", "stream-error", "storage-condition",
"style-warning", "type-error", "unbound-variable", "unbound-slot",
"undefined-function", "warning",
}
clBuiltinClasses = []string{
"array", "broadcast-stream", "bit-vector", "built-in-class", "character",
"class", "complex", "concatenated-stream", "cons", "echo-stream",
"file-stream", "float", "function", "generic-function", "hash-table",
"integer", "list", "logical-pathname", "method-combination", "method",
"null", "number", "package", "pathname", "ratio", "rational", "readtable",
"real", "random-state", "restart", "sequence", "standard-class",
"standard-generic-function", "standard-method", "standard-object",
"string-stream", "stream", "string", "structure-class", "structure-object",
"symbol", "synonym-stream", "t", "two-way-stream", "vector",
}
)
// Common Lisp lexer.
var CommonLisp = Register(TypeRemappingLexer(MustNewXMLLexer(
embedded,
"embedded/common_lisp.xml",
), TypeMapping{
{NameVariable, NameFunction, clBuiltinFunctions},
{NameVariable, Keyword, clSpecialForms},
{NameVariable, NameBuiltin, clMacros},
{NameVariable, Keyword, clLambdaListKeywords},
{NameVariable, Keyword, clDeclarations},
{NameVariable, KeywordType, clBuiltinTypes},
{NameVariable, NameClass, clBuiltinClasses},
}))
+533
View File
@@ -0,0 +1,533 @@
package lexers
import (
. "github.com/alecthomas/chroma/v2" // nolint
)
var (
emacsMacros = []string{
"atomic-change-group", "case", "block", "cl-block", "cl-callf", "cl-callf2",
"cl-case", "cl-decf", "cl-declaim", "cl-declare",
"cl-define-compiler-macro", "cl-defmacro", "cl-defstruct",
"cl-defsubst", "cl-deftype", "cl-defun", "cl-destructuring-bind",
"cl-do", "cl-do*", "cl-do-all-symbols", "cl-do-symbols", "cl-dolist",
"cl-dotimes", "cl-ecase", "cl-etypecase", "eval-when", "cl-eval-when", "cl-flet",
"cl-flet*", "cl-function", "cl-incf", "cl-labels", "cl-letf",
"cl-letf*", "cl-load-time-value", "cl-locally", "cl-loop",
"cl-macrolet", "cl-multiple-value-bind", "cl-multiple-value-setq",
"cl-progv", "cl-psetf", "cl-psetq", "cl-pushnew", "cl-remf",
"cl-return", "cl-return-from", "cl-rotatef", "cl-shiftf",
"cl-symbol-macrolet", "cl-tagbody", "cl-the", "cl-typecase",
"combine-after-change-calls", "condition-case-unless-debug", "decf",
"declaim", "declare", "declare-function", "def-edebug-spec",
"defadvice", "defclass", "defcustom", "defface", "defgeneric",
"defgroup", "define-advice", "define-alternatives",
"define-compiler-macro", "define-derived-mode", "define-generic-mode",
"define-global-minor-mode", "define-globalized-minor-mode",
"define-minor-mode", "define-modify-macro",
"define-obsolete-face-alias", "define-obsolete-function-alias",
"define-obsolete-variable-alias", "define-setf-expander",
"define-skeleton", "defmacro", "defmethod", "defsetf", "defstruct",
"defsubst", "deftheme", "deftype", "defun", "defvar-local",
"delay-mode-hooks", "destructuring-bind", "do", "do*",
"do-all-symbols", "do-symbols", "dolist", "dont-compile", "dotimes",
"dotimes-with-progress-reporter", "ecase", "ert-deftest", "etypecase",
"eval-and-compile", "eval-when-compile", "flet", "ignore-errors",
"incf", "labels", "lambda", "letrec", "lexical-let", "lexical-let*",
"loop", "multiple-value-bind", "multiple-value-setq", "noreturn",
"oref", "oref-default", "oset", "oset-default", "pcase",
"pcase-defmacro", "pcase-dolist", "pcase-exhaustive", "pcase-let",
"pcase-let*", "pop", "psetf", "psetq", "push", "pushnew", "remf",
"return", "rotatef", "rx", "save-match-data", "save-selected-window",
"save-window-excursion", "setf", "setq-local", "shiftf",
"track-mouse", "typecase", "unless", "use-package", "when",
"while-no-input", "with-case-table", "with-category-table",
"with-coding-priority", "with-current-buffer", "with-demoted-errors",
"with-eval-after-load", "with-file-modes", "with-local-quit",
"with-output-to-string", "with-output-to-temp-buffer",
"with-parsed-tramp-file-name", "with-selected-frame",
"with-selected-window", "with-silent-modifications", "with-slots",
"with-syntax-table", "with-temp-buffer", "with-temp-file",
"with-temp-message", "with-timeout", "with-tramp-connection-property",
"with-tramp-file-property", "with-tramp-progress-reporter",
"with-wrapper-hook", "load-time-value", "locally", "macrolet", "progv",
"return-from",
}
emacsSpecialForms = []string{
"and", "catch", "cond", "condition-case", "defconst", "defvar",
"function", "if", "interactive", "let", "let*", "or", "prog1",
"prog2", "progn", "quote", "save-current-buffer", "save-excursion",
"save-restriction", "setq", "setq-default", "subr-arity",
"unwind-protect", "while",
}
emacsBuiltinFunction = []string{
"%", "*", "+", "-", "/", "/=", "1+", "1-", "<", "<=", "=", ">", ">=",
"Snarf-documentation", "abort-recursive-edit", "abs",
"accept-process-output", "access-file", "accessible-keymaps", "acos",
"active-minibuffer-window", "add-face-text-property",
"add-name-to-file", "add-text-properties", "all-completions",
"append", "apply", "apropos-internal", "aref", "arrayp", "aset",
"ash", "asin", "assoc", "assoc-string", "assq", "atan", "atom",
"autoload", "autoload-do-load", "backtrace", "backtrace--locals",
"backtrace-debug", "backtrace-eval", "backtrace-frame",
"backward-char", "backward-prefix-chars", "barf-if-buffer-read-only",
"base64-decode-region", "base64-decode-string",
"base64-encode-region", "base64-encode-string", "beginning-of-line",
"bidi-find-overridden-directionality", "bidi-resolved-levels",
"bitmap-spec-p", "bobp", "bolp", "bool-vector",
"bool-vector-count-consecutive", "bool-vector-count-population",
"bool-vector-exclusive-or", "bool-vector-intersection",
"bool-vector-not", "bool-vector-p", "bool-vector-set-difference",
"bool-vector-subsetp", "bool-vector-union", "boundp",
"buffer-base-buffer", "buffer-chars-modified-tick",
"buffer-enable-undo", "buffer-file-name", "buffer-has-markers-at",
"buffer-list", "buffer-live-p", "buffer-local-value",
"buffer-local-variables", "buffer-modified-p", "buffer-modified-tick",
"buffer-name", "buffer-size", "buffer-string", "buffer-substring",
"buffer-substring-no-properties", "buffer-swap-text", "bufferp",
"bury-buffer-internal", "byte-code", "byte-code-function-p",
"byte-to-position", "byte-to-string", "byteorder",
"call-interactively", "call-last-kbd-macro", "call-process",
"call-process-region", "cancel-kbd-macro-events", "capitalize",
"capitalize-region", "capitalize-word", "car", "car-less-than-car",
"car-safe", "case-table-p", "category-docstring",
"category-set-mnemonics", "category-table", "category-table-p",
"ccl-execute", "ccl-execute-on-string", "ccl-program-p", "cdr",
"cdr-safe", "ceiling", "char-after", "char-before",
"char-category-set", "char-charset", "char-equal", "char-or-string-p",
"char-resolve-modifiers", "char-syntax", "char-table-extra-slot",
"char-table-p", "char-table-parent", "char-table-range",
"char-table-subtype", "char-to-string", "char-width", "characterp",
"charset-after", "charset-id-internal", "charset-plist",
"charset-priority-list", "charsetp", "check-coding-system",
"check-coding-systems-region", "clear-buffer-auto-save-failure",
"clear-charset-maps", "clear-face-cache", "clear-font-cache",
"clear-image-cache", "clear-string", "clear-this-command-keys",
"close-font", "clrhash", "coding-system-aliases",
"coding-system-base", "coding-system-eol-type", "coding-system-p",
"coding-system-plist", "coding-system-priority-list",
"coding-system-put", "color-distance", "color-gray-p",
"color-supported-p", "combine-after-change-execute",
"command-error-default-function", "command-remapping", "commandp",
"compare-buffer-substrings", "compare-strings",
"compare-window-configurations", "completing-read",
"compose-region-internal", "compose-string-internal",
"composition-get-gstring", "compute-motion", "concat", "cons",
"consp", "constrain-to-field", "continue-process",
"controlling-tty-p", "coordinates-in-window-p", "copy-alist",
"copy-category-table", "copy-file", "copy-hash-table", "copy-keymap",
"copy-marker", "copy-sequence", "copy-syntax-table", "copysign",
"cos", "current-active-maps", "current-bidi-paragraph-direction",
"current-buffer", "current-case-table", "current-column",
"current-global-map", "current-idle-time", "current-indentation",
"current-input-mode", "current-local-map", "current-message",
"current-minor-mode-maps", "current-time", "current-time-string",
"current-time-zone", "current-window-configuration",
"cygwin-convert-file-name-from-windows",
"cygwin-convert-file-name-to-windows", "daemon-initialized",
"daemonp", "dbus--init-bus", "dbus-get-unique-name",
"dbus-message-internal", "debug-timer-check", "declare-equiv-charset",
"decode-big5-char", "decode-char", "decode-coding-region",
"decode-coding-string", "decode-sjis-char", "decode-time",
"default-boundp", "default-file-modes", "default-printer-name",
"default-toplevel-value", "default-value", "define-category",
"define-charset-alias", "define-charset-internal",
"define-coding-system-alias", "define-coding-system-internal",
"define-fringe-bitmap", "define-hash-table-test", "define-key",
"define-prefix-command", "delete",
"delete-all-overlays", "delete-and-extract-region", "delete-char",
"delete-directory-internal", "delete-field", "delete-file",
"delete-frame", "delete-other-windows-internal", "delete-overlay",
"delete-process", "delete-region", "delete-terminal",
"delete-window-internal", "delq", "describe-buffer-bindings",
"describe-vector", "destroy-fringe-bitmap", "detect-coding-region",
"detect-coding-string", "ding", "directory-file-name",
"directory-files", "directory-files-and-attributes", "discard-input",
"display-supports-face-attributes-p", "do-auto-save", "documentation",
"documentation-property", "downcase", "downcase-region",
"downcase-word", "draw-string", "dump-colors", "dump-emacs",
"dump-face", "dump-frame-glyph-matrix", "dump-glyph-matrix",
"dump-glyph-row", "dump-redisplay-history", "dump-tool-bar-row",
"elt", "emacs-pid", "encode-big5-char", "encode-char",
"encode-coding-region", "encode-coding-string", "encode-sjis-char",
"encode-time", "end-kbd-macro", "end-of-line", "eobp", "eolp", "eq",
"eql", "equal", "equal-including-properties", "erase-buffer",
"error-message-string", "eval", "eval-buffer", "eval-region",
"event-convert-list", "execute-kbd-macro", "exit-recursive-edit",
"exp", "expand-file-name", "expt", "external-debugging-output",
"face-attribute-relative-p", "face-attributes-as-vector", "face-font",
"fboundp", "fceiling", "fetch-bytecode", "ffloor",
"field-beginning", "field-end", "field-string",
"field-string-no-properties", "file-accessible-directory-p",
"file-acl", "file-attributes", "file-attributes-lessp",
"file-directory-p", "file-executable-p", "file-exists-p",
"file-locked-p", "file-modes", "file-name-absolute-p",
"file-name-all-completions", "file-name-as-directory",
"file-name-completion", "file-name-directory",
"file-name-nondirectory", "file-newer-than-file-p", "file-readable-p",
"file-regular-p", "file-selinux-context", "file-symlink-p",
"file-system-info", "file-system-info", "file-writable-p",
"fillarray", "find-charset-region", "find-charset-string",
"find-coding-systems-region-internal", "find-composition-internal",
"find-file-name-handler", "find-font", "find-operation-coding-system",
"float", "float-time", "floatp", "floor", "fmakunbound",
"following-char", "font-at", "font-drive-otf", "font-face-attributes",
"font-family-list", "font-get", "font-get-glyphs",
"font-get-system-font", "font-get-system-normal-font", "font-info",
"font-match-p", "font-otf-alternates", "font-put",
"font-shape-gstring", "font-spec", "font-variation-glyphs",
"font-xlfd-name", "fontp", "fontset-font", "fontset-info",
"fontset-list", "fontset-list-all", "force-mode-line-update",
"force-window-update", "format", "format-mode-line",
"format-network-address", "format-time-string", "forward-char",
"forward-comment", "forward-line", "forward-word",
"frame-border-width", "frame-bottom-divider-width",
"frame-can-run-window-configuration-change-hook", "frame-char-height",
"frame-char-width", "frame-face-alist", "frame-first-window",
"frame-focus", "frame-font-cache", "frame-fringe-width", "frame-list",
"frame-live-p", "frame-or-buffer-changed-p", "frame-parameter",
"frame-parameters", "frame-pixel-height", "frame-pixel-width",
"frame-pointer-visible-p", "frame-right-divider-width",
"frame-root-window", "frame-scroll-bar-height",
"frame-scroll-bar-width", "frame-selected-window", "frame-terminal",
"frame-text-cols", "frame-text-height", "frame-text-lines",
"frame-text-width", "frame-total-cols", "frame-total-lines",
"frame-visible-p", "framep", "frexp", "fringe-bitmaps-at-pos",
"fround", "fset", "ftruncate", "funcall", "funcall-interactively",
"function-equal", "functionp", "gap-position", "gap-size",
"garbage-collect", "gc-status", "generate-new-buffer-name", "get",
"get-buffer", "get-buffer-create", "get-buffer-process",
"get-buffer-window", "get-byte", "get-char-property",
"get-char-property-and-overlay", "get-file-buffer", "get-file-char",
"get-internal-run-time", "get-load-suffixes", "get-pos-property",
"get-process", "get-screen-color", "get-text-property",
"get-unicode-property-internal", "get-unused-category",
"get-unused-iso-final-char", "getenv-internal", "gethash",
"gfile-add-watch", "gfile-rm-watch", "global-key-binding",
"gnutls-available-p", "gnutls-boot", "gnutls-bye", "gnutls-deinit",
"gnutls-error-fatalp", "gnutls-error-string", "gnutls-errorp",
"gnutls-get-initstage", "gnutls-peer-status",
"gnutls-peer-status-warning-describe", "goto-char", "gpm-mouse-start",
"gpm-mouse-stop", "group-gid", "group-real-gid",
"handle-save-session", "handle-switch-frame", "hash-table-count",
"hash-table-p", "hash-table-rehash-size",
"hash-table-rehash-threshold", "hash-table-size", "hash-table-test",
"hash-table-weakness", "iconify-frame", "identity", "image-flush",
"image-mask-p", "image-metadata", "image-size", "imagemagick-types",
"imagep", "indent-to", "indirect-function", "indirect-variable",
"init-image-library", "inotify-add-watch", "inotify-rm-watch",
"input-pending-p", "insert", "insert-and-inherit",
"insert-before-markers", "insert-before-markers-and-inherit",
"insert-buffer-substring", "insert-byte", "insert-char",
"insert-file-contents", "insert-startup-screen", "int86",
"integer-or-marker-p", "integerp", "interactive-form", "intern",
"intern-soft", "internal--track-mouse", "internal-char-font",
"internal-complete-buffer", "internal-copy-lisp-face",
"internal-default-process-filter",
"internal-default-process-sentinel", "internal-describe-syntax-value",
"internal-event-symbol-parse-modifiers",
"internal-face-x-get-resource", "internal-get-lisp-face-attribute",
"internal-lisp-face-attribute-values", "internal-lisp-face-empty-p",
"internal-lisp-face-equal-p", "internal-lisp-face-p",
"internal-make-lisp-face", "internal-make-var-non-special",
"internal-merge-in-global-face",
"internal-set-alternative-font-family-alist",
"internal-set-alternative-font-registry-alist",
"internal-set-font-selection-order",
"internal-set-lisp-face-attribute",
"internal-set-lisp-face-attribute-from-resource",
"internal-show-cursor", "internal-show-cursor-p", "interrupt-process",
"invisible-p", "invocation-directory", "invocation-name", "isnan",
"iso-charset", "key-binding", "key-description",
"keyboard-coding-system", "keymap-parent", "keymap-prompt", "keymapp",
"keywordp", "kill-all-local-variables", "kill-buffer", "kill-emacs",
"kill-local-variable", "kill-process", "last-nonminibuffer-frame",
"lax-plist-get", "lax-plist-put", "ldexp", "length",
"libxml-parse-html-region", "libxml-parse-xml-region",
"line-beginning-position", "line-end-position", "line-pixel-height",
"list", "list-fonts", "list-system-processes", "listp", "load",
"load-average", "local-key-binding", "local-variable-if-set-p",
"local-variable-p", "locale-info", "locate-file-internal",
"lock-buffer", "log", "logand", "logb", "logior", "lognot", "logxor",
"looking-at", "lookup-image", "lookup-image-map", "lookup-key",
"lower-frame", "lsh", "macroexpand", "make-bool-vector",
"make-byte-code", "make-category-set", "make-category-table",
"make-char", "make-char-table", "make-directory-internal",
"make-frame-invisible", "make-frame-visible", "make-hash-table",
"make-indirect-buffer", "make-keymap", "make-list",
"make-local-variable", "make-marker", "make-network-process",
"make-overlay", "make-serial-process", "make-sparse-keymap",
"make-string", "make-symbol", "make-symbolic-link", "make-temp-name",
"make-terminal-frame", "make-variable-buffer-local",
"make-variable-frame-local", "make-vector", "makunbound",
"map-char-table", "map-charset-chars", "map-keymap",
"map-keymap-internal", "mapatoms", "mapc", "mapcar", "mapconcat",
"maphash", "mark-marker", "marker-buffer", "marker-insertion-type",
"marker-position", "markerp", "match-beginning", "match-data",
"match-end", "matching-paren", "max", "max-char", "md5", "member",
"memory-info", "memory-limit", "memory-use-counts", "memq", "memql",
"menu-bar-menu-at-x-y", "menu-or-popup-active-p",
"menu-or-popup-active-p", "merge-face-attribute", "message",
"message-box", "message-or-box", "min",
"minibuffer-completion-contents", "minibuffer-contents",
"minibuffer-contents-no-properties", "minibuffer-depth",
"minibuffer-prompt", "minibuffer-prompt-end",
"minibuffer-selected-window", "minibuffer-window", "minibufferp",
"minor-mode-key-binding", "mod", "modify-category-entry",
"modify-frame-parameters", "modify-syntax-entry",
"mouse-pixel-position", "mouse-position", "move-overlay",
"move-point-visually", "move-to-column", "move-to-window-line",
"msdos-downcase-filename", "msdos-long-file-names", "msdos-memget",
"msdos-memput", "msdos-mouse-disable", "msdos-mouse-enable",
"msdos-mouse-init", "msdos-mouse-p", "msdos-remember-default-colors",
"msdos-set-keyboard", "msdos-set-mouse-buttons",
"multibyte-char-to-unibyte", "multibyte-string-p", "narrow-to-region",
"natnump", "nconc", "network-interface-info",
"network-interface-list", "new-fontset", "newline-cache-check",
"next-char-property-change", "next-frame", "next-overlay-change",
"next-property-change", "next-read-file-uses-dialog-p",
"next-single-char-property-change", "next-single-property-change",
"next-window", "nlistp", "nreverse", "nth", "nthcdr", "null",
"number-or-marker-p", "number-to-string", "numberp",
"open-dribble-file", "open-font", "open-termscript",
"optimize-char-table", "other-buffer", "other-window-for-scrolling",
"overlay-buffer", "overlay-end", "overlay-get", "overlay-lists",
"overlay-properties", "overlay-put", "overlay-recenter",
"overlay-start", "overlayp", "overlays-at", "overlays-in",
"parse-partial-sexp", "play-sound-internal", "plist-get",
"plist-member", "plist-put", "point", "point-marker", "point-max",
"point-max-marker", "point-min", "point-min-marker",
"pos-visible-in-window-p", "position-bytes", "posix-looking-at",
"posix-search-backward", "posix-search-forward", "posix-string-match",
"posn-at-point", "posn-at-x-y", "preceding-char",
"prefix-numeric-value", "previous-char-property-change",
"previous-frame", "previous-overlay-change",
"previous-property-change", "previous-single-char-property-change",
"previous-single-property-change", "previous-window", "prin1",
"prin1-to-string", "princ", "print", "process-attributes",
"process-buffer", "process-coding-system", "process-command",
"process-connection", "process-contact", "process-datagram-address",
"process-exit-status", "process-filter", "process-filter-multibyte-p",
"process-id", "process-inherit-coding-system-flag", "process-list",
"process-mark", "process-name", "process-plist",
"process-query-on-exit-flag", "process-running-child-p",
"process-send-eof", "process-send-region", "process-send-string",
"process-sentinel", "process-status", "process-tty-name",
"process-type", "processp", "profiler-cpu-log",
"profiler-cpu-running-p", "profiler-cpu-start", "profiler-cpu-stop",
"profiler-memory-log", "profiler-memory-running-p",
"profiler-memory-start", "profiler-memory-stop", "propertize",
"purecopy", "put", "put-text-property",
"put-unicode-property-internal", "puthash", "query-font",
"query-fontset", "quit-process", "raise-frame", "random", "rassoc",
"rassq", "re-search-backward", "re-search-forward", "read",
"read-buffer", "read-char", "read-char-exclusive",
"read-coding-system", "read-command", "read-event",
"read-from-minibuffer", "read-from-string", "read-function",
"read-key-sequence", "read-key-sequence-vector",
"read-no-blanks-input", "read-non-nil-coding-system", "read-string",
"read-variable", "recent-auto-save-p", "recent-doskeys",
"recent-keys", "recenter", "recursion-depth", "recursive-edit",
"redirect-debugging-output", "redirect-frame-focus", "redisplay",
"redraw-display", "redraw-frame", "regexp-quote", "region-beginning",
"region-end", "register-ccl-program", "register-code-conversion-map",
"remhash", "remove-list-of-text-properties", "remove-text-properties",
"rename-buffer", "rename-file", "replace-match",
"reset-this-command-lengths", "resize-mini-window-internal",
"restore-buffer-modified-p", "resume-tty", "reverse", "round",
"run-hook-with-args", "run-hook-with-args-until-failure",
"run-hook-with-args-until-success", "run-hook-wrapped", "run-hooks",
"run-window-configuration-change-hook", "run-window-scroll-functions",
"safe-length", "scan-lists", "scan-sexps", "scroll-down",
"scroll-left", "scroll-other-window", "scroll-right", "scroll-up",
"search-backward", "search-forward", "secure-hash", "select-frame",
"select-window", "selected-frame", "selected-window",
"self-insert-command", "send-string-to-terminal", "sequencep",
"serial-process-configure", "set", "set-buffer",
"set-buffer-auto-saved", "set-buffer-major-mode",
"set-buffer-modified-p", "set-buffer-multibyte", "set-case-table",
"set-category-table", "set-char-table-extra-slot",
"set-char-table-parent", "set-char-table-range", "set-charset-plist",
"set-charset-priority", "set-coding-system-priority",
"set-cursor-size", "set-default", "set-default-file-modes",
"set-default-toplevel-value", "set-file-acl", "set-file-modes",
"set-file-selinux-context", "set-file-times", "set-fontset-font",
"set-frame-height", "set-frame-position", "set-frame-selected-window",
"set-frame-size", "set-frame-width", "set-fringe-bitmap-face",
"set-input-interrupt-mode", "set-input-meta-mode", "set-input-mode",
"set-keyboard-coding-system-internal", "set-keymap-parent",
"set-marker", "set-marker-insertion-type", "set-match-data",
"set-message-beep", "set-minibuffer-window",
"set-mouse-pixel-position", "set-mouse-position",
"set-network-process-option", "set-output-flow-control",
"set-process-buffer", "set-process-coding-system",
"set-process-datagram-address", "set-process-filter",
"set-process-filter-multibyte",
"set-process-inherit-coding-system-flag", "set-process-plist",
"set-process-query-on-exit-flag", "set-process-sentinel",
"set-process-window-size", "set-quit-char",
"set-safe-terminal-coding-system-internal", "set-screen-color",
"set-standard-case-table", "set-syntax-table",
"set-terminal-coding-system-internal", "set-terminal-local-value",
"set-terminal-parameter", "set-text-properties", "set-time-zone-rule",
"set-visited-file-modtime", "set-window-buffer",
"set-window-combination-limit", "set-window-configuration",
"set-window-dedicated-p", "set-window-display-table",
"set-window-fringes", "set-window-hscroll", "set-window-margins",
"set-window-new-normal", "set-window-new-pixel",
"set-window-new-total", "set-window-next-buffers",
"set-window-parameter", "set-window-point", "set-window-prev-buffers",
"set-window-redisplay-end-trigger", "set-window-scroll-bars",
"set-window-start", "set-window-vscroll", "setcar", "setcdr",
"setplist", "show-face-resources", "signal", "signal-process", "sin",
"single-key-description", "skip-chars-backward", "skip-chars-forward",
"skip-syntax-backward", "skip-syntax-forward", "sleep-for", "sort",
"sort-charsets", "special-variable-p", "split-char",
"split-window-internal", "sqrt", "standard-case-table",
"standard-category-table", "standard-syntax-table", "start-kbd-macro",
"start-process", "stop-process", "store-kbd-macro-event", "string",
"string-as-multibyte", "string-as-unibyte", "string-bytes",
"string-collate-equalp", "string-collate-lessp", "string-equal",
"string-lessp", "string-make-multibyte", "string-make-unibyte",
"string-match", "string-to-char", "string-to-multibyte",
"string-to-number", "string-to-syntax", "string-to-unibyte",
"string-width", "stringp", "subr-name", "subrp",
"subst-char-in-region", "substitute-command-keys",
"substitute-in-file-name", "substring", "substring-no-properties",
"suspend-emacs", "suspend-tty", "suspicious-object", "sxhash",
"symbol-function", "symbol-name", "symbol-plist", "symbol-value",
"symbolp", "syntax-table", "syntax-table-p", "system-groups",
"system-move-file-to-trash", "system-name", "system-users", "tan",
"terminal-coding-system", "terminal-list", "terminal-live-p",
"terminal-local-value", "terminal-name", "terminal-parameter",
"terminal-parameters", "terpri", "test-completion",
"text-char-description", "text-properties-at", "text-property-any",
"text-property-not-all", "this-command-keys",
"this-command-keys-vector", "this-single-command-keys",
"this-single-command-raw-keys", "time-add", "time-less-p",
"time-subtract", "tool-bar-get-system-style", "tool-bar-height",
"tool-bar-pixel-width", "top-level", "trace-redisplay",
"trace-to-stderr", "translate-region-internal", "transpose-regions",
"truncate", "try-completion", "tty-display-color-cells",
"tty-display-color-p", "tty-no-underline",
"tty-suppress-bold-inverse-default-colors", "tty-top-frame",
"tty-type", "type-of", "undo-boundary", "unencodable-char-position",
"unhandled-file-name-directory", "unibyte-char-to-multibyte",
"unibyte-string", "unicode-property-table-internal", "unify-charset",
"unintern", "unix-sync", "unlock-buffer", "upcase", "upcase-initials",
"upcase-initials-region", "upcase-region", "upcase-word",
"use-global-map", "use-local-map", "user-full-name",
"user-login-name", "user-real-login-name", "user-real-uid",
"user-uid", "variable-binding-locus", "vconcat", "vector",
"vector-or-char-table-p", "vectorp", "verify-visited-file-modtime",
"vertical-motion", "visible-frame-list", "visited-file-modtime",
"w16-get-clipboard-data", "w16-selection-exists-p",
"w16-set-clipboard-data", "w32-battery-status",
"w32-default-color-map", "w32-define-rgb-color",
"w32-display-monitor-attributes-list", "w32-frame-menu-bar-size",
"w32-frame-rect", "w32-get-clipboard-data",
"w32-get-codepage-charset", "w32-get-console-codepage",
"w32-get-console-output-codepage", "w32-get-current-locale-id",
"w32-get-default-locale-id", "w32-get-keyboard-layout",
"w32-get-locale-info", "w32-get-valid-codepages",
"w32-get-valid-keyboard-layouts", "w32-get-valid-locale-ids",
"w32-has-winsock", "w32-long-file-name", "w32-reconstruct-hot-key",
"w32-register-hot-key", "w32-registered-hot-keys",
"w32-selection-exists-p", "w32-send-sys-command",
"w32-set-clipboard-data", "w32-set-console-codepage",
"w32-set-console-output-codepage", "w32-set-current-locale",
"w32-set-keyboard-layout", "w32-set-process-priority",
"w32-shell-execute", "w32-short-file-name", "w32-toggle-lock-key",
"w32-unload-winsock", "w32-unregister-hot-key", "w32-window-exists-p",
"w32notify-add-watch", "w32notify-rm-watch",
"waiting-for-user-input-p", "where-is-internal", "widen",
"widget-apply", "widget-get", "widget-put",
"window-absolute-pixel-edges", "window-at", "window-body-height",
"window-body-width", "window-bottom-divider-width", "window-buffer",
"window-combination-limit", "window-configuration-frame",
"window-configuration-p", "window-dedicated-p",
"window-display-table", "window-edges", "window-end", "window-frame",
"window-fringes", "window-header-line-height", "window-hscroll",
"window-inside-absolute-pixel-edges", "window-inside-edges",
"window-inside-pixel-edges", "window-left-child",
"window-left-column", "window-line-height", "window-list",
"window-list-1", "window-live-p", "window-margins",
"window-minibuffer-p", "window-mode-line-height", "window-new-normal",
"window-new-pixel", "window-new-total", "window-next-buffers",
"window-next-sibling", "window-normal-size", "window-old-point",
"window-parameter", "window-parameters", "window-parent",
"window-pixel-edges", "window-pixel-height", "window-pixel-left",
"window-pixel-top", "window-pixel-width", "window-point",
"window-prev-buffers", "window-prev-sibling",
"window-redisplay-end-trigger", "window-resize-apply",
"window-resize-apply-total", "window-right-divider-width",
"window-scroll-bar-height", "window-scroll-bar-width",
"window-scroll-bars", "window-start", "window-system",
"window-text-height", "window-text-pixel-size", "window-text-width",
"window-top-child", "window-top-line", "window-total-height",
"window-total-width", "window-use-time", "window-valid-p",
"window-vscroll", "windowp", "write-char", "write-region",
"x-backspace-delete-keys-p", "x-change-window-property",
"x-change-window-property", "x-close-connection",
"x-close-connection", "x-create-frame", "x-create-frame",
"x-delete-window-property", "x-delete-window-property",
"x-disown-selection-internal", "x-display-backing-store",
"x-display-backing-store", "x-display-color-cells",
"x-display-color-cells", "x-display-grayscale-p",
"x-display-grayscale-p", "x-display-list", "x-display-list",
"x-display-mm-height", "x-display-mm-height", "x-display-mm-width",
"x-display-mm-width", "x-display-monitor-attributes-list",
"x-display-pixel-height", "x-display-pixel-height",
"x-display-pixel-width", "x-display-pixel-width", "x-display-planes",
"x-display-planes", "x-display-save-under", "x-display-save-under",
"x-display-screens", "x-display-screens", "x-display-visual-class",
"x-display-visual-class", "x-family-fonts", "x-file-dialog",
"x-file-dialog", "x-file-dialog", "x-focus-frame", "x-frame-geometry",
"x-frame-geometry", "x-get-atom-name", "x-get-resource",
"x-get-selection-internal", "x-hide-tip", "x-hide-tip",
"x-list-fonts", "x-load-color-file", "x-menu-bar-open-internal",
"x-menu-bar-open-internal", "x-open-connection", "x-open-connection",
"x-own-selection-internal", "x-parse-geometry", "x-popup-dialog",
"x-popup-menu", "x-register-dnd-atom", "x-select-font",
"x-select-font", "x-selection-exists-p", "x-selection-owner-p",
"x-send-client-message", "x-server-max-request-size",
"x-server-max-request-size", "x-server-vendor", "x-server-vendor",
"x-server-version", "x-server-version", "x-show-tip", "x-show-tip",
"x-synchronize", "x-synchronize", "x-uses-old-gtk-dialog",
"x-window-property", "x-window-property", "x-wm-set-size-hint",
"xw-color-defined-p", "xw-color-defined-p", "xw-color-values",
"xw-color-values", "xw-display-color-p", "xw-display-color-p",
"yes-or-no-p", "zlib-available-p", "zlib-decompress-region",
"forward-point",
}
emacsBuiltinFunctionHighlighted = []string{
"defvaralias", "provide", "require",
"with-no-warnings", "define-widget", "with-electric-help",
"throw", "defalias", "featurep",
}
emacsLambdaListKeywords = []string{
"&allow-other-keys", "&aux", "&body", "&environment", "&key", "&optional",
"&rest", "&whole",
}
emacsErrorKeywords = []string{
"cl-assert", "cl-check-type", "error", "signal",
"user-error", "warn",
}
)
// EmacsLisp lexer.
var EmacsLisp = Register(TypeRemappingLexer(MustNewXMLLexer(
embedded,
"embedded/emacslisp.xml",
), TypeMapping{
{NameVariable, NameFunction, emacsBuiltinFunction},
{NameVariable, NameBuiltin, emacsSpecialForms},
{NameVariable, NameException, emacsErrorKeywords},
{NameVariable, NameBuiltin, append(emacsBuiltinFunctionHighlighted, emacsMacros...)},
{NameVariable, KeywordPseudo, emacsLambdaListKeywords},
}))
@@ -0,0 +1,154 @@
<lexer>
<config>
<name>ABAP</name>
<alias>abap</alias>
<filename>*.abap</filename>
<filename>*.ABAP</filename>
<mime_type>text/x-abap</mime_type>
<case_insensitive>true</case_insensitive>
</config>
<rules>
<state name="common">
<rule pattern="\s+">
<token type="Text"/>
</rule>
<rule pattern="^\*.*$">
<token type="CommentSingle"/>
</rule>
<rule pattern="\&#34;.*?\n">
<token type="CommentSingle"/>
</rule>
<rule pattern="##\w+">
<token type="CommentSpecial"/>
</rule>
</state>
<state name="variable-names">
<rule pattern="&lt;\S+&gt;">
<token type="NameVariable"/>
</rule>
<rule pattern="\w[\w~]*(?:(\[\])|-&gt;\*)?">
<token type="NameVariable"/>
</rule>
</state>
<state name="root">
<rule>
<include state="common"/>
</rule>
<rule pattern="CALL\s+(?:BADI|CUSTOMER-FUNCTION|FUNCTION)">
<token type="Keyword"/>
</rule>
<rule pattern="(CALL\s+(?:DIALOG|SCREEN|SUBSCREEN|SELECTION-SCREEN|TRANSACTION|TRANSFORMATION))\b">
<token type="Keyword"/>
</rule>
<rule pattern="(FORM|PERFORM)(\s+)(\w+)">
<bygroups>
<token type="Keyword"/>
<token type="Text"/>
<token type="NameFunction"/>
</bygroups>
</rule>
<rule pattern="(PERFORM)(\s+)(\()(\w+)(\))">
<bygroups>
<token type="Keyword"/>
<token type="Text"/>
<token type="Punctuation"/>
<token type="NameVariable"/>
<token type="Punctuation"/>
</bygroups>
</rule>
<rule pattern="(MODULE)(\s+)(\S+)(\s+)(INPUT|OUTPUT)">
<bygroups>
<token type="Keyword"/>
<token type="Text"/>
<token type="NameFunction"/>
<token type="Text"/>
<token type="Keyword"/>
</bygroups>
</rule>
<rule pattern="(METHOD)(\s+)([\w~]+)">
<bygroups>
<token type="Keyword"/>
<token type="Text"/>
<token type="NameFunction"/>
</bygroups>
</rule>
<rule pattern="(\s+)([\w\-]+)([=\-]&gt;)([\w\-~]+)">
<bygroups>
<token type="Text"/>
<token type="NameVariable"/>
<token type="Operator"/>
<token type="NameFunction"/>
</bygroups>
</rule>
<rule pattern="(?&lt;=(=|-)&gt;)([\w\-~]+)(?=\()">
<token type="NameFunction"/>
</rule>
<rule pattern="(TEXT)(-)(\d{3})">
<bygroups>
<token type="Keyword"/>
<token type="Punctuation"/>
<token type="LiteralNumberInteger"/>
</bygroups>
</rule>
<rule pattern="(TEXT)(-)(\w{3})">
<bygroups>
<token type="Keyword"/>
<token type="Punctuation"/>
<token type="NameVariable"/>
</bygroups>
</rule>
<rule pattern="(ADD-CORRESPONDING|AUTHORITY-CHECK|CLASS-DATA|CLASS-EVENTS|CLASS-METHODS|CLASS-POOL|DELETE-ADJACENT|DIVIDE-CORRESPONDING|EDITOR-CALL|ENHANCEMENT-POINT|ENHANCEMENT-SECTION|EXIT-COMMAND|FIELD-GROUPS|FIELD-SYMBOLS|FUNCTION-POOL|INTERFACE-POOL|INVERTED-DATE|LOAD-OF-PROGRAM|LOG-POINT|MESSAGE-ID|MOVE-CORRESPONDING|MULTIPLY-CORRESPONDING|NEW-LINE|NEW-PAGE|NEW-SECTION|NO-EXTENSION|OUTPUT-LENGTH|PRINT-CONTROL|SELECT-OPTIONS|START-OF-SELECTION|SUBTRACT-CORRESPONDING|SYNTAX-CHECK|SYSTEM-EXCEPTIONS|TYPE-POOL|TYPE-POOLS|NO-DISPLAY)\b">
<token type="Keyword"/>
</rule>
<rule pattern="(?&lt;![-\&gt;])(CREATE\s+(PUBLIC|PRIVATE|DATA|OBJECT)|(PUBLIC|PRIVATE|PROTECTED)\s+SECTION|(TYPE|LIKE)\s+((LINE\s+OF|REF\s+TO|(SORTED|STANDARD|HASHED)\s+TABLE\s+OF))?|FROM\s+(DATABASE|MEMORY)|CALL\s+METHOD|(GROUP|ORDER) BY|HAVING|SEPARATED BY|GET\s+(BADI|BIT|CURSOR|DATASET|LOCALE|PARAMETER|PF-STATUS|(PROPERTY|REFERENCE)\s+OF|RUN\s+TIME|TIME\s+(STAMP)?)?|SET\s+(BIT|BLANK\s+LINES|COUNTRY|CURSOR|DATASET|EXTENDED\s+CHECK|HANDLER|HOLD\s+DATA|LANGUAGE|LEFT\s+SCROLL-BOUNDARY|LOCALE|MARGIN|PARAMETER|PF-STATUS|PROPERTY\s+OF|RUN\s+TIME\s+(ANALYZER|CLOCK\s+RESOLUTION)|SCREEN|TITLEBAR|UPADTE\s+TASK\s+LOCAL|USER-COMMAND)|CONVERT\s+((INVERTED-)?DATE|TIME|TIME\s+STAMP|TEXT)|(CLOSE|OPEN)\s+(DATASET|CURSOR)|(TO|FROM)\s+(DATA BUFFER|INTERNAL TABLE|MEMORY ID|DATABASE|SHARED\s+(MEMORY|BUFFER))|DESCRIBE\s+(DISTANCE\s+BETWEEN|FIELD|LIST|TABLE)|FREE\s(MEMORY|OBJECT)?|PROCESS\s+(BEFORE\s+OUTPUT|AFTER\s+INPUT|ON\s+(VALUE-REQUEST|HELP-REQUEST))|AT\s+(LINE-SELECTION|USER-COMMAND|END\s+OF|NEW)|AT\s+SELECTION-SCREEN(\s+(ON(\s+(BLOCK|(HELP|VALUE)-REQUEST\s+FOR|END\s+OF|RADIOBUTTON\s+GROUP))?|OUTPUT))?|SELECTION-SCREEN:?\s+((BEGIN|END)\s+OF\s+((TABBED\s+)?BLOCK|LINE|SCREEN)|COMMENT|FUNCTION\s+KEY|INCLUDE\s+BLOCKS|POSITION|PUSHBUTTON|SKIP|ULINE)|LEAVE\s+(LIST-PROCESSING|PROGRAM|SCREEN|TO LIST-PROCESSING|TO TRANSACTION)(ENDING|STARTING)\s+AT|FORMAT\s+(COLOR|INTENSIFIED|INVERSE|HOTSPOT|INPUT|FRAMES|RESET)|AS\s+(CHECKBOX|SUBSCREEN|WINDOW)|WITH\s+(((NON-)?UNIQUE)?\s+KEY|FRAME)|(BEGIN|END)\s+OF|DELETE(\s+ADJACENT\s+DUPLICATES\sFROM)?|COMPARING(\s+ALL\s+FIELDS)?|(INSERT|APPEND)(\s+INITIAL\s+LINE\s+(IN)?TO|\s+LINES\s+OF)?|IN\s+((BYTE|CHARACTER)\s+MODE|PROGRAM)|END-OF-(DEFINITION|PAGE|SELECTION)|WITH\s+FRAME(\s+TITLE)|(REPLACE|FIND)\s+((FIRST|ALL)\s+OCCURRENCES?\s+OF\s+)?(SUBSTRING|REGEX)?|MATCH\s+(LENGTH|COUNT|LINE|OFFSET)|(RESPECTING|IGNORING)\s+CASE|IN\s+UPDATE\s+TASK|(SOURCE|RESULT)\s+(XML)?|REFERENCE\s+INTO|AND\s+(MARK|RETURN)|CLIENT\s+SPECIFIED|CORRESPONDING\s+FIELDS\s+OF|IF\s+FOUND|FOR\s+EVENT|INHERITING\s+FROM|LEAVE\s+TO\s+SCREEN|LOOP\s+AT\s+(SCREEN)?|LOWER\s+CASE|MATCHCODE\s+OBJECT|MODIF\s+ID|MODIFY\s+SCREEN|NESTING\s+LEVEL|NO\s+INTERVALS|OF\s+STRUCTURE|RADIOBUTTON\s+GROUP|RANGE\s+OF|REF\s+TO|SUPPRESS DIALOG|TABLE\s+OF|UPPER\s+CASE|TRANSPORTING\s+NO\s+FIELDS|VALUE\s+CHECK|VISIBLE\s+LENGTH|HEADER\s+LINE|COMMON\s+PART)\b">
<token type="Keyword"/>
</rule>
<rule pattern="(^|(?&lt;=(\s|\.)))(ABBREVIATED|ABSTRACT|ADD|ALIASES|ALIGN|ALPHA|ASSERT|AS|ASSIGN(ING)?|AT(\s+FIRST)?|BACK|BLOCK|BREAK-POINT|CASE|CATCH|CHANGING|CHECK|CLASS|CLEAR|COLLECT|COLOR|COMMIT|CREATE|COMMUNICATION|COMPONENTS?|COMPUTE|CONCATENATE|CONDENSE|CONSTANTS|CONTEXTS|CONTINUE|CONTROLS|COUNTRY|CURRENCY|DATA|DATE|DECIMALS|DEFAULT|DEFINE|DEFINITION|DEFERRED|DEMAND|DETAIL|DIRECTORY|DIVIDE|DO|DUMMY|ELSE(IF)?|ENDAT|ENDCASE|ENDCATCH|ENDCLASS|ENDDO|ENDFORM|ENDFUNCTION|ENDIF|ENDINTERFACE|ENDLOOP|ENDMETHOD|ENDMODULE|ENDSELECT|ENDTRY|ENDWHILE|ENHANCEMENT|EVENTS|EXACT|EXCEPTIONS?|EXIT|EXPONENT|EXPORT|EXPORTING|EXTRACT|FETCH|FIELDS?|FOR|FORM|FORMAT|FREE|FROM|FUNCTION|HIDE|ID|IF|IMPORT|IMPLEMENTATION|IMPORTING|IN|INCLUDE|INCLUDING|INDEX|INFOTYPES|INITIALIZATION|INTERFACE|INTERFACES|INTO|LANGUAGE|LEAVE|LENGTH|LINES|LOAD|LOCAL|JOIN|KEY|NEXT|MAXIMUM|MESSAGE|METHOD[S]?|MINIMUM|MODULE|MODIFIER|MODIFY|MOVE|MULTIPLY|NODES|NUMBER|OBLIGATORY|OBJECT|OF|OFF|ON|OTHERS|OVERLAY|PACK|PAD|PARAMETERS|PERCENTAGE|POSITION|PROGRAM|PROVIDE|PUBLIC|PUT|PF\d\d|RAISE|RAISING|RANGES?|READ|RECEIVE|REDEFINITION|REFRESH|REJECT|REPORT|RESERVE|RESUME|RETRY|RETURN|RETURNING|RIGHT|ROLLBACK|REPLACE|SCROLL|SEARCH|SELECT|SHIFT|SIGN|SINGLE|SIZE|SKIP|SORT|SPLIT|STATICS|STOP|STYLE|SUBMATCHES|SUBMIT|SUBTRACT|SUM(?!\()|SUMMARY|SUMMING|SUPPLY|TABLE|TABLES|TIMESTAMP|TIMES?|TIMEZONE|TITLE|\??TO|TOP-OF-PAGE|TRANSFER|TRANSLATE|TRY|TYPES|ULINE|UNDER|UNPACK|UPDATE|USING|VALUE|VALUES|VIA|VARYING|VARY|WAIT|WHEN|WHERE|WIDTH|WHILE|WITH|WINDOW|WRITE|XSD|ZERO)\b">
<token type="Keyword"/>
</rule>
<rule pattern="(abs|acos|asin|atan|boolc|boolx|bit_set|char_off|charlen|ceil|cmax|cmin|condense|contains|contains_any_of|contains_any_not_of|concat_lines_of|cos|cosh|count|count_any_of|count_any_not_of|dbmaxlen|distance|escape|exp|find|find_end|find_any_of|find_any_not_of|floor|frac|from_mixed|insert|lines|log|log10|match|matches|nmax|nmin|numofchar|repeat|replace|rescale|reverse|round|segment|shift_left|shift_right|sign|sin|sinh|sqrt|strlen|substring|substring_after|substring_from|substring_before|substring_to|tan|tanh|to_upper|to_lower|to_mixed|translate|trunc|xstrlen)(\()\b">
<bygroups>
<token type="NameBuiltin"/>
<token type="Punctuation"/>
</bygroups>
</rule>
<rule pattern="&amp;[0-9]">
<token type="Name"/>
</rule>
<rule pattern="[0-9]+">
<token type="LiteralNumberInteger"/>
</rule>
<rule pattern="(?&lt;=(\s|.))(AND|OR|EQ|NE|GT|LT|GE|LE|CO|CN|CA|NA|CS|NOT|NS|CP|NP|BYTE-CO|BYTE-CN|BYTE-CA|BYTE-NA|BYTE-CS|BYTE-NS|IS\s+(NOT\s+)?(INITIAL|ASSIGNED|REQUESTED|BOUND))\b">
<token type="OperatorWord"/>
</rule>
<rule>
<include state="variable-names"/>
</rule>
<rule pattern="[?*&lt;&gt;=\-+&amp;]">
<token type="Operator"/>
</rule>
<rule pattern="&#39;(&#39;&#39;|[^&#39;])*&#39;">
<token type="LiteralStringSingle"/>
</rule>
<rule pattern="`([^`])*`">
<token type="LiteralStringSingle"/>
</rule>
<rule pattern="([|}])([^{}|]*?)([|{])">
<bygroups>
<token type="Punctuation"/>
<token type="LiteralStringSingle"/>
<token type="Punctuation"/>
</bygroups>
</rule>
<rule pattern="[/;:()\[\],.]">
<token type="Punctuation"/>
</rule>
<rule pattern="(!)(\w+)">
<bygroups>
<token type="Operator"/>
<token type="Name"/>
</bygroups>
</rule>
</state>
</rules>
</lexer>
@@ -0,0 +1,66 @@
<lexer>
<config>
<name>ABNF</name>
<alias>abnf</alias>
<filename>*.abnf</filename>
<mime_type>text/x-abnf</mime_type>
</config>
<rules>
<state name="root">
<rule pattern=";.*$">
<token type="CommentSingle"/>
</rule>
<rule pattern="(%[si])?&#34;[^&#34;]*&#34;">
<token type="Literal"/>
</rule>
<rule pattern="%b[01]+\-[01]+\b">
<token type="Literal"/>
</rule>
<rule pattern="%b[01]+(\.[01]+)*\b">
<token type="Literal"/>
</rule>
<rule pattern="%d[0-9]+\-[0-9]+\b">
<token type="Literal"/>
</rule>
<rule pattern="%d[0-9]+(\.[0-9]+)*\b">
<token type="Literal"/>
</rule>
<rule pattern="%x[0-9a-fA-F]+\-[0-9a-fA-F]+\b">
<token type="Literal"/>
</rule>
<rule pattern="%x[0-9a-fA-F]+(\.[0-9a-fA-F]+)*\b">
<token type="Literal"/>
</rule>
<rule pattern="\b[0-9]+\*[0-9]+">
<token type="Operator"/>
</rule>
<rule pattern="\b[0-9]+\*">
<token type="Operator"/>
</rule>
<rule pattern="\b[0-9]+">
<token type="Operator"/>
</rule>
<rule pattern="\*">
<token type="Operator"/>
</rule>
<rule pattern="(HEXDIG|DQUOTE|DIGIT|VCHAR|OCTET|ALPHA|CHAR|CRLF|HTAB|LWSP|BIT|CTL|WSP|LF|SP|CR)\b">
<token type="Keyword"/>
</rule>
<rule pattern="[a-zA-Z][a-zA-Z0-9-]+\b">
<token type="NameClass"/>
</rule>
<rule pattern="(=/|=|/)">
<token type="Operator"/>
</rule>
<rule pattern="[\[\]()]">
<token type="Punctuation"/>
</rule>
<rule pattern="\s+">
<token type="Text"/>
</rule>
<rule pattern=".">
<token type="Text"/>
</rule>
</state>
</rules>
</lexer>
@@ -0,0 +1,68 @@
<lexer>
<config>
<name>ActionScript</name>
<alias>as</alias>
<alias>actionscript</alias>
<filename>*.as</filename>
<mime_type>application/x-actionscript</mime_type>
<mime_type>text/x-actionscript</mime_type>
<mime_type>text/actionscript</mime_type>
<dot_all>true</dot_all>
<not_multiline>true</not_multiline>
</config>
<rules>
<state name="root">
<rule pattern="\s+">
<token type="Text"/>
</rule>
<rule pattern="//.*?\n">
<token type="CommentSingle"/>
</rule>
<rule pattern="/\*.*?\*/">
<token type="CommentMultiline"/>
</rule>
<rule pattern="/(\\\\|\\/|[^/\n])*/[gim]*">
<token type="LiteralStringRegex"/>
</rule>
<rule pattern="[~^*!%&amp;&lt;&gt;|+=:;,/?\\-]+">
<token type="Operator"/>
</rule>
<rule pattern="[{}\[\]();.]+">
<token type="Punctuation"/>
</rule>
<rule pattern="(instanceof|arguments|continue|default|typeof|switch|return|catch|break|while|throw|each|this|with|else|case|var|new|for|try|if|do|in)\b">
<token type="Keyword"/>
</rule>
<rule pattern="(implements|protected|namespace|interface|intrinsic|override|function|internal|private|package|extends|dynamic|import|native|return|public|static|class|const|super|final|get|set)\b">
<token type="KeywordDeclaration"/>
</rule>
<rule pattern="(true|false|null|NaN|Infinity|-Infinity|undefined|Void)\b">
<token type="KeywordConstant"/>
</rule>
<rule pattern="(IDynamicPropertyOutputIDynamicPropertyWriter|DisplacmentMapFilterMode|AccessibilityProperties|ContextMenuBuiltInItems|SharedObjectFlushStatus|DisplayObjectContainer|IllegalOperationError|DisplacmentMapFilter|InterpolationMethod|URLLoaderDataFormat|PrintJobOrientation|ActionScriptVersion|BitmapFilterQuality|GradientBevelFilter|GradientGlowFilter|DeleteObjectSample|StackOverflowError|SoundLoaderContext|ScriptTimeoutError|SecurityErrorEvent|InteractiveObject|StageDisplayState|FileReferenceList|TextFieldAutoSize|ApplicationDomain|BitmapDataChannel|ColorMatrixFilter|ExternalInterface|IMEConversionMode|DropShadowFilter|URLRequestHeader|ContextMenuEvent|ConvultionFilter|URLRequestMethod|BitmapFilterType|IEventDispatcher|ContextMenuItem|LocalConnection|InvalidSWFError|AsyncErrorEvent|MovieClipLoader|IBitmapDrawable|PrintJobOptions|EventDispatcher|NewObjectSample|HTTPStatusEvent|TextFormatAlign|IExternalizable|FullScreenEvent|DefinitionError|TextLineMetrics|NetStatusEvent|ColorTransform|ObjectEncoding|SecurityDomain|StageScaleMode|FocusDirection|ReferenceError|SoundTransform|KeyboardEvent|DisplayObject|PixelSnapping|LoaderContext|NetConnection|SecurityPanel|SecurityError|FileReference|AsBroadcaster|LineScaleMode|AntiAliasType|Accessibility|TextFieldType|URLVariabeles|ActivityEvent|ProgressEvent|TextColorType|StageQuality|TextSnapshot|Capabilities|BitmapFilter|SpreadMethod|GradientType|TextRenderer|SoundChannel|SharedObject|IOErrorEvent|SimpleButton|ContextMenu|InvokeEvent|CSMSettings|SyntaxError|StatusEvent|KeyLocation|IDataOutput|VerifyError|XMLDocument|XMLNodeType|MemoryError|GridFitType|BevelFilter|ErrorEvent|FrameLabel|GlowFilter|LoaderInfo|Microphone|MorphShape|BlurFilter|MouseEvent|FocusEvent|SoundMixer|FileFilter|TimerEvent|JointStyle|EventPhase|StageAlign|Dictionary|URLRequest|StyleSheet|SWFVersion|IDataInput|StaticText|RangeError|BitmapData|TextFormat|StackFrame|Namespace|SyncEvent|Rectangle|URLLoader|TypeError|Responder|NetStream|BlendMode|CapsStyle|DataEvent|ByteArray|MovieClip|Transform|TextField|Selection|AVM1Movie|XMLSocket|URLStream|FontStyle|EvalError|FontType|LoadVars|Graphics|Security|IMEEvent|URIError|Keyboard|Function|EOFError|PrintJob|IOError|XMLList|Boolean|ID3Info|XMLNode|Bitmap|String|RegExp|Sample|Object|Sprite|System|Endian|Matrix|Camera|Locale|Number|Loader|Socket|QName|Class|Timer|Sound|Shape|XMLUI|Mouse|Scene|Stage|Color|Point|Video|Error|Event|Proxy|Array|Date|uint|Math|Font|int|Key|IME|XML)\b">
<token type="NameBuiltin"/>
</rule>
<rule pattern="(decodeURIComponent|updateAfterEvent|clearInterval|setInterval|getVersion|parseFloat|fscommand|isXMLName|encodeURI|decodeURI|getTimer|unescape|isFinite|parseInt|getURL|escape|trace|isNaN|eval)\b">
<token type="NameFunction"/>
</rule>
<rule pattern="[$a-zA-Z_]\w*">
<token type="NameOther"/>
</rule>
<rule pattern="[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?">
<token type="LiteralNumberFloat"/>
</rule>
<rule pattern="0x[0-9a-f]+">
<token type="LiteralNumberHex"/>
</rule>
<rule pattern="[0-9]+">
<token type="LiteralNumberInteger"/>
</rule>
<rule pattern="&#34;(\\\\|\\&#34;|[^&#34;])*&#34;">
<token type="LiteralStringDouble"/>
</rule>
<rule pattern="&#39;(\\\\|\\&#39;|[^&#39;])*&#39;">
<token type="LiteralStringSingle"/>
</rule>
</state>
</rules>
</lexer>
@@ -0,0 +1,163 @@
<lexer>
<config>
<name>ActionScript 3</name>
<alias>as3</alias>
<alias>actionscript3</alias>
<filename>*.as</filename>
<mime_type>application/x-actionscript3</mime_type>
<mime_type>text/x-actionscript3</mime_type>
<mime_type>text/actionscript3</mime_type>
<dot_all>true</dot_all>
</config>
<rules>
<state name="funcparams">
<rule pattern="\s+">
<token type="Text"/>
</rule>
<rule pattern="(\s*)(\.\.\.)?([$a-zA-Z_]\w*)(\s*)(:)(\s*)([$a-zA-Z_]\w*(?:\.&lt;\w+&gt;)?|\*)(\s*)">
<bygroups>
<token type="Text"/>
<token type="Punctuation"/>
<token type="Name"/>
<token type="Text"/>
<token type="Operator"/>
<token type="Text"/>
<token type="KeywordType"/>
<token type="Text"/>
</bygroups>
<push state="defval"/>
</rule>
<rule pattern="\)">
<token type="Operator"/>
<push state="type"/>
</rule>
</state>
<state name="type">
<rule pattern="(\s*)(:)(\s*)([$a-zA-Z_]\w*(?:\.&lt;\w+&gt;)?|\*)">
<bygroups>
<token type="Text"/>
<token type="Operator"/>
<token type="Text"/>
<token type="KeywordType"/>
</bygroups>
<pop depth="2"/>
</rule>
<rule pattern="\s+">
<token type="Text"/>
<pop depth="2"/>
</rule>
<rule>
<pop depth="2"/>
</rule>
</state>
<state name="defval">
<rule pattern="(=)(\s*)([^(),]+)(\s*)(,?)">
<bygroups>
<token type="Operator"/>
<token type="Text"/>
<usingself state="root"/>
<token type="Text"/>
<token type="Operator"/>
</bygroups>
<pop depth="1"/>
</rule>
<rule pattern=",">
<token type="Operator"/>
<pop depth="1"/>
</rule>
<rule>
<pop depth="1"/>
</rule>
</state>
<state name="root">
<rule pattern="\s+">
<token type="Text"/>
</rule>
<rule pattern="(function\s+)([$a-zA-Z_]\w*)(\s*)(\()">
<bygroups>
<token type="KeywordDeclaration"/>
<token type="NameFunction"/>
<token type="Text"/>
<token type="Operator"/>
</bygroups>
<push state="funcparams"/>
</rule>
<rule pattern="(var|const)(\s+)([$a-zA-Z_]\w*)(\s*)(:)(\s*)([$a-zA-Z_]\w*(?:\.&lt;\w+&gt;)?)">
<bygroups>
<token type="KeywordDeclaration"/>
<token type="Text"/>
<token type="Name"/>
<token type="Text"/>
<token type="Punctuation"/>
<token type="Text"/>
<token type="KeywordType"/>
</bygroups>
</rule>
<rule pattern="(import|package)(\s+)((?:[$a-zA-Z_]\w*|\.)+)(\s*)">
<bygroups>
<token type="Keyword"/>
<token type="Text"/>
<token type="NameNamespace"/>
<token type="Text"/>
</bygroups>
</rule>
<rule pattern="(new)(\s+)([$a-zA-Z_]\w*(?:\.&lt;\w+&gt;)?)(\s*)(\()">
<bygroups>
<token type="Keyword"/>
<token type="Text"/>
<token type="KeywordType"/>
<token type="Text"/>
<token type="Operator"/>
</bygroups>
</rule>
<rule pattern="//.*?\n">
<token type="CommentSingle"/>
</rule>
<rule pattern="/\*.*?\*/">
<token type="CommentMultiline"/>
</rule>
<rule pattern="/(\\\\|\\/|[^\n])*/[gisx]*">
<token type="LiteralStringRegex"/>
</rule>
<rule pattern="(\.)([$a-zA-Z_]\w*)">
<bygroups>
<token type="Operator"/>
<token type="NameAttribute"/>
</bygroups>
</rule>
<rule pattern="(case|default|for|each|in|while|do|break|return|continue|if|else|throw|try|catch|with|new|typeof|arguments|instanceof|this|switch|import|include|as|is)\b">
<token type="Keyword"/>
</rule>
<rule pattern="(class|public|final|internal|native|override|private|protected|static|import|extends|implements|interface|intrinsic|return|super|dynamic|function|const|get|namespace|package|set)\b">
<token type="KeywordDeclaration"/>
</rule>
<rule pattern="(true|false|null|NaN|Infinity|-Infinity|undefined|void)\b">
<token type="KeywordConstant"/>
</rule>
<rule pattern="(decodeURI|decodeURIComponent|encodeURI|escape|eval|isFinite|isNaN|isXMLName|clearInterval|fscommand|getTimer|getURL|getVersion|isFinite|parseFloat|parseInt|setInterval|trace|updateAfterEvent|unescape)\b">
<token type="NameFunction"/>
</rule>
<rule pattern="[$a-zA-Z_]\w*">
<token type="Name"/>
</rule>
<rule pattern="[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?">
<token type="LiteralNumberFloat"/>
</rule>
<rule pattern="0x[0-9a-f]+">
<token type="LiteralNumberHex"/>
</rule>
<rule pattern="[0-9]+">
<token type="LiteralNumberInteger"/>
</rule>
<rule pattern="&#34;(\\\\|\\&#34;|[^&#34;])*&#34;">
<token type="LiteralStringDouble"/>
</rule>
<rule pattern="&#39;(\\\\|\\&#39;|[^&#39;])*&#39;">
<token type="LiteralStringSingle"/>
</rule>
<rule pattern="[~^*!%&amp;&lt;&gt;|+=:;,/?\\{}\[\]().-]+">
<token type="Operator"/>
</rule>
</state>
</rules>
</lexer>
@@ -0,0 +1,321 @@
<lexer>
<config>
<name>Ada</name>
<alias>ada</alias>
<alias>ada95</alias>
<alias>ada2005</alias>
<filename>*.adb</filename>
<filename>*.ads</filename>
<filename>*.ada</filename>
<mime_type>text/x-ada</mime_type>
<case_insensitive>true</case_insensitive>
</config>
<rules>
<state name="end">
<rule pattern="(if|case|record|loop|select)">
<token type="KeywordReserved"/>
</rule>
<rule pattern="&#34;[^&#34;]+&#34;|[\w.]+">
<token type="NameFunction"/>
</rule>
<rule pattern="\s+">
<token type="Text"/>
</rule>
<rule pattern=";">
<token type="Punctuation"/>
<pop depth="1"/>
</rule>
</state>
<state name="array_def">
<rule pattern=";">
<token type="Punctuation"/>
<pop depth="1"/>
</rule>
<rule pattern="(\w+)(\s+)(range)">
<bygroups>
<token type="KeywordType"/>
<token type="Text"/>
<token type="KeywordReserved"/>
</bygroups>
</rule>
<rule>
<include state="root"/>
</rule>
</state>
<state name="package_instantiation">
<rule pattern="(&#34;[^&#34;]+&#34;|\w+)(\s+)(=&gt;)">
<bygroups>
<token type="NameVariable"/>
<token type="Text"/>
<token type="Punctuation"/>
</bygroups>
</rule>
<rule pattern="[\w.\&#39;&#34;]">
<token type="Text"/>
</rule>
<rule pattern="\)">
<token type="Punctuation"/>
<pop depth="1"/>
</rule>
<rule>
<include state="root"/>
</rule>
</state>
<state name="subprogram">
<rule pattern="\(">
<token type="Punctuation"/>
<push state="#pop" state="formal_part"/>
</rule>
<rule pattern=";">
<token type="Punctuation"/>
<pop depth="1"/>
</rule>
<rule pattern="is\b">
<token type="KeywordReserved"/>
<pop depth="1"/>
</rule>
<rule pattern="&#34;[^&#34;]+&#34;|\w+">
<token type="NameFunction"/>
</rule>
<rule>
<include state="root"/>
</rule>
</state>
<state name="type_def">
<rule pattern=";">
<token type="Punctuation"/>
<pop depth="1"/>
</rule>
<rule pattern="\(">
<token type="Punctuation"/>
<push state="formal_part"/>
</rule>
<rule pattern="with|and|use">
<token type="KeywordReserved"/>
</rule>
<rule pattern="array\b">
<token type="KeywordReserved"/>
<push state="#pop" state="array_def"/>
</rule>
<rule pattern="record\b">
<token type="KeywordReserved"/>
<push state="record_def"/>
</rule>
<rule pattern="(null record)(;)">
<bygroups>
<token type="KeywordReserved"/>
<token type="Punctuation"/>
</bygroups>
<pop depth="1"/>
</rule>
<rule>
<include state="root"/>
</rule>
</state>
<state name="import">
<rule pattern="[\w.]+">
<token type="NameNamespace"/>
<pop depth="1"/>
</rule>
<rule>
<pop depth="1"/>
</rule>
</state>
<state name="formal_part">
<rule pattern="\)">
<token type="Punctuation"/>
<pop depth="1"/>
</rule>
<rule pattern="\w+">
<token type="NameVariable"/>
</rule>
<rule pattern=",|:[^=]">
<token type="Punctuation"/>
</rule>
<rule pattern="(in|not|null|out|access)\b">
<token type="KeywordReserved"/>
</rule>
<rule>
<include state="root"/>
</rule>
</state>
<state name="package">
<rule pattern="body">
<token type="KeywordDeclaration"/>
</rule>
<rule pattern="is\s+new|renames">
<token type="KeywordReserved"/>
</rule>
<rule pattern="is">
<token type="KeywordReserved"/>
<pop depth="1"/>
</rule>
<rule pattern=";">
<token type="Punctuation"/>
<pop depth="1"/>
</rule>
<rule pattern="\(">
<token type="Punctuation"/>
<push state="package_instantiation"/>
</rule>
<rule pattern="([\w.]+)">
<token type="NameClass"/>
</rule>
<rule>
<include state="root"/>
</rule>
</state>
<state name="attribute">
<rule pattern="(&#39;)(\w+)">
<bygroups>
<token type="Punctuation"/>
<token type="NameAttribute"/>
</bygroups>
</rule>
</state>
<state name="record_def">
<rule pattern="end record">
<token type="KeywordReserved"/>
<pop depth="1"/>
</rule>
<rule>
<include state="root"/>
</rule>
</state>
<state name="root">
<rule pattern="[^\S\n]+">
<token type="Text"/>
</rule>
<rule pattern="--.*?\n">
<token type="CommentSingle"/>
</rule>
<rule pattern="[^\S\n]+">
<token type="Text"/>
</rule>
<rule pattern="function|procedure|entry">
<token type="KeywordDeclaration"/>
<push state="subprogram"/>
</rule>
<rule pattern="(subtype|type)(\s+)(\w+)">
<bygroups>
<token type="KeywordDeclaration"/>
<token type="Text"/>
<token type="KeywordType"/>
</bygroups>
<push state="type_def"/>
</rule>
<rule pattern="task|protected">
<token type="KeywordDeclaration"/>
</rule>
<rule pattern="(subtype)(\s+)">
<bygroups>
<token type="KeywordDeclaration"/>
<token type="Text"/>
</bygroups>
</rule>
<rule pattern="(end)(\s+)">
<bygroups>
<token type="KeywordReserved"/>
<token type="Text"/>
</bygroups>
<push state="end"/>
</rule>
<rule pattern="(pragma)(\s+)(\w+)">
<bygroups>
<token type="KeywordReserved"/>
<token type="Text"/>
<token type="CommentPreproc"/>
</bygroups>
</rule>
<rule pattern="(true|false|null)\b">
<token type="KeywordConstant"/>
</rule>
<rule pattern="(Short_Short_Integer|Short_Short_Float|Long_Long_Integer|Long_Long_Float|Wide_Character|Reference_Type|Short_Integer|Long_Integer|Wide_String|Short_Float|Controlled|Long_Float|Character|Generator|File_Type|File_Mode|Positive|Duration|Boolean|Natural|Integer|Address|Cursor|String|Count|Float|Byte)\b">
<token type="KeywordType"/>
</rule>
<rule pattern="(and(\s+then)?|in|mod|not|or(\s+else)|rem)\b">
<token type="OperatorWord"/>
</rule>
<rule pattern="generic|private">
<token type="KeywordDeclaration"/>
</rule>
<rule pattern="package">
<token type="KeywordDeclaration"/>
<push state="package"/>
</rule>
<rule pattern="array\b">
<token type="KeywordReserved"/>
<push state="array_def"/>
</rule>
<rule pattern="(with|use)(\s+)">
<bygroups>
<token type="KeywordNamespace"/>
<token type="Text"/>
</bygroups>
<push state="import"/>
</rule>
<rule pattern="(\w+)(\s*)(:)(\s*)(constant)">
<bygroups>
<token type="NameConstant"/>
<token type="Text"/>
<token type="Punctuation"/>
<token type="Text"/>
<token type="KeywordReserved"/>
</bygroups>
</rule>
<rule pattern="&lt;&lt;\w+&gt;&gt;">
<token type="NameLabel"/>
</rule>
<rule pattern="(\w+)(\s*)(:)(\s*)(declare|begin|loop|for|while)">
<bygroups>
<token type="NameLabel"/>
<token type="Text"/>
<token type="Punctuation"/>
<token type="Text"/>
<token type="KeywordReserved"/>
</bygroups>
</rule>
<rule pattern="\b(synchronized|overriding|terminate|interface|exception|protected|separate|constant|abstract|renames|reverse|subtype|aliased|declare|requeue|limited|return|tagged|access|record|select|accept|digits|others|pragma|entry|elsif|delta|delay|array|until|range|raise|while|begin|abort|else|loop|when|type|null|then|body|task|goto|case|exit|end|for|abs|xor|all|new|out|is|of|if|or|do|at)\b">
<token type="KeywordReserved"/>
</rule>
<rule pattern="&#34;[^&#34;]*&#34;">
<token type="LiteralString"/>
</rule>
<rule>
<include state="attribute"/>
</rule>
<rule>
<include state="numbers"/>
</rule>
<rule pattern="&#39;[^&#39;]&#39;">
<token type="LiteralStringChar"/>
</rule>
<rule pattern="(\w+)(\s*|[(,])">
<bygroups>
<token type="Name"/>
<usingself state="root"/>
</bygroups>
</rule>
<rule pattern="(&lt;&gt;|=&gt;|:=|[()|:;,.&#39;])">
<token type="Punctuation"/>
</rule>
<rule pattern="[*&lt;&gt;+=/&amp;-]">
<token type="Operator"/>
</rule>
<rule pattern="\n+">
<token type="Text"/>
</rule>
</state>
<state name="numbers">
<rule pattern="[0-9_]+#[0-9a-f]+#">
<token type="LiteralNumberHex"/>
</rule>
<rule pattern="[0-9_]+\.[0-9_]*">
<token type="LiteralNumberFloat"/>
</rule>
<rule pattern="[0-9_]+">
<token type="LiteralNumberInteger"/>
</rule>
</state>
</rules>
</lexer>
@@ -0,0 +1,65 @@
<lexer>
<config>
<name>Agda</name>
<alias>agda</alias>
<filename>*.agda</filename>
<mime_type>text/x-agda</mime_type>
</config>
<rules>
<state name="root">
<rule pattern="^(\s*)([^\s(){}]+)(\s*)(:)(\s*)"><bygroups><token type="TextWhitespace"/><token type="NameFunction"/><token type="TextWhitespace"/><token type="OperatorWord"/><token type="TextWhitespace"/></bygroups></rule>
<rule pattern="--(?![!#$%&amp;*+./&lt;=&gt;?@^|_~:\\]).*?$"><token type="CommentSingle"/></rule>
<rule pattern="\{-"><token type="CommentMultiline"/><push state="comment"/></rule>
<rule pattern="\{!"><token type="CommentMultiline"/><push state="hole"/></rule>
<rule pattern="\b(abstract|codata|coinductive|constructor|data|do|eta-equality|field|forall|hiding|in|inductive|infix|infixl|infixr|instance|interleaved|let|macro|mutual|no-eta-equality|open|overlap|pattern|postulate|primitive|private|quote|quoteTerm|record|renaming|rewrite|syntax|tactic|unquote|unquoteDecl|unquoteDef|using|variable|where|with)(?!\&#x27;)\b"><token type="KeywordReserved"/></rule>
<rule pattern="(import|module)(\s+)"><bygroups><token type="KeywordReserved"/><token type="TextWhitespace"/></bygroups><push state="module"/></rule>
<rule pattern="\b(Set|Prop)[\u2080-\u2089]*\b"><token type="KeywordType"/></rule>
<rule pattern="(\(|\)|\{|\})"><token type="Operator"/></rule>
<rule pattern="(\.{1,3}|\||\u03BB|\u2200|\u2192|:|=|-&gt;)"><token type="OperatorWord"/></rule>
<rule pattern="\d+[eE][+-]?\d+"><token type="LiteralNumberFloat"/></rule>
<rule pattern="\d+\.\d+([eE][+-]?\d+)?"><token type="LiteralNumberFloat"/></rule>
<rule pattern="0[xX][\da-fA-F]+"><token type="LiteralNumberHex"/></rule>
<rule pattern="\d+"><token type="LiteralNumberInteger"/></rule>
<rule pattern="&#x27;"><token type="LiteralStringChar"/><push state="character"/></rule>
<rule pattern="&quot;"><token type="LiteralString"/><push state="string"/></rule>
<rule pattern="[^\s(){}]+"><token type="Text"/></rule>
<rule pattern="\s+?"><token type="TextWhitespace"/></rule>
</state>
<state name="hole">
<rule pattern="[^!{}]+"><token type="CommentMultiline"/></rule>
<rule pattern="\{!"><token type="CommentMultiline"/><push/></rule>
<rule pattern="!\}"><token type="CommentMultiline"/><pop depth="1"/></rule>
<rule pattern="[!{}]"><token type="CommentMultiline"/></rule>
</state>
<state name="module">
<rule pattern="\{-"><token type="CommentMultiline"/><push state="comment"/></rule>
<rule pattern="[a-zA-Z][\w.\&#x27;]*"><token type="Name"/><pop depth="1"/></rule>
<rule pattern="[\W0-9_]+"><token type="Text"/></rule>
</state>
<state name="comment">
<rule pattern="[^-{}]+"><token type="CommentMultiline"/></rule>
<rule pattern="\{-"><token type="CommentMultiline"/><push/></rule>
<rule pattern="-\}"><token type="CommentMultiline"/><pop depth="1"/></rule>
<rule pattern="[-{}]"><token type="CommentMultiline"/></rule>
</state>
<state name="character">
<rule pattern="[^\\&#x27;]&#x27;"><token type="LiteralStringChar"/><pop depth="1"/></rule>
<rule pattern="\\"><token type="LiteralStringEscape"/><push state="escape"/></rule>
<rule pattern="&#x27;"><token type="LiteralStringChar"/><pop depth="1"/></rule>
</state>
<state name="string">
<rule pattern="[^\\&quot;]+"><token type="LiteralString"/></rule>
<rule pattern="\\"><token type="LiteralStringEscape"/><push state="escape"/></rule>
<rule pattern="&quot;"><token type="LiteralString"/><pop depth="1"/></rule>
</state>
<state name="escape">
<rule pattern="[abfnrtv&quot;\&#x27;&amp;\\]"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
<rule pattern="\^[][A-ZÀ-ÖØ-ÞĀĂĄĆĈĊČĎĐĒĔĖĘĚĜĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸ-ŹŻŽƁ-ƂƄƆ-ƇƉ-ƋƎ-ƑƓ-ƔƖ-ƘƜ-ƝƟ-ƠƢƤƦ-ƧƩƬƮ-ƯƱ-ƳƵƷ-ƸƼDŽLJNJǍǏǑǓǕǗǙǛǞǠǢǤǦǨǪǬǮDZǴǶ-ǸǺǼǾȀȂȄȆȈȊȌȎȐȒȔȖȘȚȜȞȠȢȤȦȨȪȬȮȰȲȺ-ȻȽ-ȾɁɃ-ɆɈɊɌɎͰͲͶͿΆΈ-ΊΌΎ-ΏΑ-ΡΣ-ΫϏϒ-ϔϘϚϜϞϠϢϤϦϨϪϬϮϴϷϹ-ϺϽ-ЯѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎҐҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾӀ-ӁӃӅӇӉӋӍӐӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӶӸӺӼӾԀԂԄԆԈԊԌԎԐԒԔԖԘԚԜԞԠԢԤԦԨԪԬԮԱ-ՖႠ-ჅჇჍᎠ-ᏵᲐ-ᲺᲽ-ᲿḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẐẒẔẞẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸỺỼỾἈ-ἏἘ-ἝἨ-ἯἸ-ἿὈ-ὍὙὛὝὟὨ-ὯᾸ-ΆῈ-ΉῘ-ΊῨ-ῬῸ-Ώℂℇℋ-ℍℐ-ℒℕℙ-ℝℤΩℨK-ℭℰ-ℳℾ-ℿⅅↃⰀ-ⰮⱠⱢ-ⱤⱧⱩⱫⱭ-ⱰⱲⱵⱾ-ⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢⳫⳭⳲꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙠꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꚘꚚꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝽ-ꝾꞀꞂꞄꞆꞋꞍꞐꞒꞖꞘꞚꞜꞞꞠꞢꞤꞦꞨꞪ-ꞮꞰ-ꞴꞶꞸA-Z𐐀-𐐧𐒰-𐓓𐲀-𐲲𑢠-𑢿𖹀-𖹟𝐀-𝐙𝐴-𝑍𝑨-𝒁𝒜𝒞-𝒟𝒢𝒥-𝒦𝒩-𝒬𝒮-𝒵𝓐-𝓩𝔄-𝔅𝔇-𝔊𝔍-𝔔𝔖-𝔜𝔸-𝔹𝔻-𝔾𝕀-𝕄𝕆𝕊-𝕐𝕬-𝖅𝖠-𝖹𝗔-𝗭𝘈-𝘡𝘼-𝙕𝙰-𝚉𝚨-𝛀𝛢-𝛺𝜜-𝜴𝝖-𝝮𝞐-𝞨𝟊𞤀-𞤡@^_]"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
<rule pattern="NUL|SOH|[SE]TX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|S[OI]|DLE|DC[1-4]|NAK|SYN|ETB|CAN|EM|SUB|ESC|[FGRU]S|SP|DEL"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
<rule pattern="o[0-7]+"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
<rule pattern="x[\da-fA-F]+"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
<rule pattern="\d+"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
<rule pattern="(\s+)(\\)"><bygroups><token type="TextWhitespace"/><token type="LiteralStringEscape"/></bygroups><pop depth="1"/></rule>
</state>
</rules>
</lexer>
@@ -0,0 +1,75 @@
<lexer>
<config>
<name>AL</name>
<alias>al</alias>
<filename>*.al</filename>
<filename>*.dal</filename>
<mime_type>text/x-al</mime_type>
<case_insensitive>true</case_insensitive>
<dot_all>true</dot_all>
</config>
<rules>
<state name="root">
<rule pattern="\s+">
<token type="TextWhitespace"/>
</rule>
<rule pattern="(?s)\/\*.*?\\*\*\/">
<token type="CommentMultiline"/>
</rule>
<rule pattern="(?s)//.*?\n">
<token type="CommentSingle"/>
</rule>
<rule pattern="\&#34;([^\&#34;])*\&#34;">
<token type="Text"/>
</rule>
<rule pattern="&#39;([^&#39;])*&#39;">
<token type="LiteralString"/>
</rule>
<rule pattern="\b(?i:(ARRAY|ASSERTERROR|BEGIN|BREAK|CASE|DO|DOWNTO|ELSE|END|EVENT|EXIT|FOR|FOREACH|FUNCTION|IF|IMPLEMENTS|IN|INDATASET|INTERFACE|INTERNAL|LOCAL|OF|PROCEDURE|PROGRAM|PROTECTED|REPEAT|RUNONCLIENT|SECURITYFILTERING|SUPPRESSDISPOSE|TEMPORARY|THEN|TO|TRIGGER|UNTIL|VAR|WHILE|WITH|WITHEVENTS))\b">
<token type="Keyword"/>
</rule>
<rule pattern="\b(?i:(AND|DIV|MOD|NOT|OR|XOR))\b">
<token type="OperatorWord"/>
</rule>
<rule pattern="\b(?i:(AVERAGE|CONST|COUNT|EXIST|FIELD|FILTER|LOOKUP|MAX|MIN|ORDER|SORTING|SUM|TABLEDATA|UPPERLIMIT|WHERE|ASCENDING|DESCENDING))\b">
<token type="Keyword"/>
</rule>
<rule pattern="\b(?i:(CODEUNIT|PAGE|PAGEEXTENSION|PAGECUSTOMIZATION|DOTNET|ENUM|ENUMEXTENSION|VALUE|QUERY|REPORT|TABLE|TABLEEXTENSION|XMLPORT|PROFILE|CONTROLADDIN|REPORTEXTENSION|INTERFACE|PERMISSIONSET|PERMISSIONSETEXTENSION|ENTITLEMENT))\b">
<token type="Keyword"/>
</rule>
<rule pattern="\b(?i:(Action|Array|Automation|BigInteger|BigText|Blob|Boolean|Byte|Char|ClientType|Code|Codeunit|CompletionTriggerErrorLevel|ConnectionType|Database|DataClassification|DataScope|Date|DateFormula|DateTime|Decimal|DefaultLayout|Dialog|Dictionary|DotNet|DotNetAssembly|DotNetTypeDeclaration|Duration|Enum|ErrorInfo|ErrorType|ExecutionContext|ExecutionMode|FieldClass|FieldRef|FieldType|File|FilterPageBuilder|Guid|InStream|Integer|Joker|KeyRef|List|ModuleDependencyInfo|ModuleInfo|None|Notification|NotificationScope|ObjectType|Option|OutStream|Page|PageResult|Query|Record|RecordId|RecordRef|Report|ReportFormat|SecurityFilter|SecurityFiltering|Table|TableConnectionType|TableFilter|TestAction|TestField|TestFilterField|TestPage|TestPermissions|TestRequestPage|Text|TextBuilder|TextConst|TextEncoding|Time|TransactionModel|TransactionType|Variant|Verbosity|Version|XmlPort|HttpContent|HttpHeaders|HttpClient|HttpRequestMessage|HttpResponseMessage|JsonToken|JsonValue|JsonArray|JsonObject|View|Views|XmlAttribute|XmlAttributeCollection|XmlComment|XmlCData|XmlDeclaration|XmlDocument|XmlDocumentType|XmlElement|XmlNamespaceManager|XmlNameTable|XmlNode|XmlNodeList|XmlProcessingInstruction|XmlReadOptions|XmlText|XmlWriteOptions|WebServiceActionContext|WebServiceActionResultCode|SessionSettings))\b">
<token type="Keyword"/>
</rule>
<rule pattern="\b([&lt;&gt;]=|&lt;&gt;|&lt;|&gt;)\b?">
<token type="Operator"/>
</rule>
<rule pattern="\b(\-|\+|\/|\*)\b">
<token type="Operator"/>
</rule>
<rule pattern="\s*(\:=|\+=|-=|\/=|\*=)\s*?">
<token type="Operator"/>
</rule>
<rule pattern="\b(?i:(ADD|ADDFIRST|ADDLAST|ADDAFTER|ADDBEFORE|ACTION|ACTIONS|AREA|ASSEMBLY|CHARTPART|CUEGROUP|CUSTOMIZES|COLUMN|DATAITEM|DATASET|ELEMENTS|EXTENDS|FIELD|FIELDGROUP|FIELDATTRIBUTE|FIELDELEMENT|FIELDGROUPS|FIELDS|FILTER|FIXED|GRID|GROUP|MOVEAFTER|MOVEBEFORE|KEY|KEYS|LABEL|LABELS|LAYOUT|MODIFY|MOVEFIRST|MOVELAST|MOVEBEFORE|MOVEAFTER|PART|REPEATER|USERCONTROL|REQUESTPAGE|SCHEMA|SEPARATOR|SYSTEMPART|TABLEELEMENT|TEXTATTRIBUTE|TEXTELEMENT|TYPE))\b">
<token type="Keyword"/>
</rule>
<rule pattern="\s*[(\.\.)&amp;\|]\s*">
<token type="Operator"/>
</rule>
<rule pattern="\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\b">
<token type="LiteralNumber"/>
</rule>
<rule pattern="[;:,]">
<token type="Punctuation"/>
</rule>
<rule pattern="#[ \t]*(if|else|elif|endif|define|undef|region|endregion|pragma)\b.*?\n">
<token type="CommentPreproc"/>
</rule>
<rule pattern="\w+">
<token type="Text"/>
</rule>
<rule pattern=".">
<token type="Text"/>
</rule>
</state>
</rules>
</lexer>
@@ -0,0 +1,57 @@
<lexer>
<config>
<name>Alloy</name>
<alias>alloy</alias>
<filename>*.als</filename>
<mime_type>text/x-alloy</mime_type>
<dot_all>true</dot_all>
</config>
<rules>
<state name="sig">
<rule pattern="(extends)\b"><token type="Keyword"/><pop depth="1"/></rule>
<rule pattern="[a-zA-Z_][\w]*&quot;*"><token type="Name"/></rule>
<rule pattern="[^\S\n]+"><token type="TextWhitespace"/></rule>
<rule pattern=","><token type="Punctuation"/></rule>
<rule pattern="\{"><token type="Operator"/><pop depth="1"/></rule>
</state>
<state name="module">
<rule pattern="[^\S\n]+"><token type="TextWhitespace"/></rule>
<rule pattern="[a-zA-Z_][\w]*&quot;*"><token type="Name"/><pop depth="1"/></rule>
</state>
<state name="fun">
<rule pattern="[^\S\n]+"><token type="TextWhitespace"/></rule>
<rule pattern="\{"><token type="Operator"/><pop depth="1"/></rule>
<rule pattern="[a-zA-Z_][\w]*&quot;*"><token type="Name"/><pop depth="1"/></rule>
</state>
<state name="fact">
<rule><include state="fun"/></rule>
<rule pattern="&quot;\b(\\\\|\\[^\\]|[^&quot;\\])*&quot;"><token type="LiteralString"/><pop depth="1"/></rule>
</state>
<state name="root">
<rule pattern="--.*?$"><token type="CommentSingle"/></rule>
<rule pattern="//.*?$"><token type="CommentSingle"/></rule>
<rule pattern="/\*.*?\*/"><token type="CommentMultiline"/></rule>
<rule pattern="[^\S\n]+"><token type="TextWhitespace"/></rule>
<rule pattern="(module|open)(\s+)"><bygroups><token type="KeywordNamespace"/><token type="TextWhitespace"/></bygroups><push state="module"/></rule>
<rule pattern="(sig|enum)(\s+)"><bygroups><token type="KeywordDeclaration"/><token type="TextWhitespace"/></bygroups><push state="sig"/></rule>
<rule pattern="(iden|univ|none)\b"><token type="KeywordConstant"/></rule>
<rule pattern="(int|Int)\b"><token type="KeywordType"/></rule>
<rule pattern="(var|this|abstract|extends|set|seq|one|lone|let)\b"><token type="Keyword"/></rule>
<rule pattern="(all|some|no|sum|disj|when|else)\b"><token type="Keyword"/></rule>
<rule pattern="(run|check|for|but|exactly|expect|as|steps)\b"><token type="Keyword"/></rule>
<rule pattern="(always|after|eventually|until|release)\b"><token type="Keyword"/></rule>
<rule pattern="(historically|before|once|since|triggered)\b"><token type="Keyword"/></rule>
<rule pattern="(and|or|implies|iff|in)\b"><token type="OperatorWord"/></rule>
<rule pattern="(fun|pred|assert)(\s+)"><bygroups><token type="Keyword"/><token type="TextWhitespace"/></bygroups><push state="fun"/></rule>
<rule pattern="(fact)(\s+)"><bygroups><token type="Keyword"/><token type="TextWhitespace"/></bygroups><push state="fact"/></rule>
<rule pattern="!|#|&amp;&amp;|\+\+|&lt;&lt;|&gt;&gt;|&gt;=|&lt;=&gt;|&lt;=|\.\.|\.|-&gt;"><token type="Operator"/></rule>
<rule pattern="[-+/*%=&lt;&gt;&amp;!^|~{}\[\]().\&#x27;;]"><token type="Operator"/></rule>
<rule pattern="[a-zA-Z_][\w]*&quot;*"><token type="Name"/></rule>
<rule pattern="[:,]"><token type="Punctuation"/></rule>
<rule pattern="[0-9]+"><token type="LiteralNumberInteger"/></rule>
<rule pattern="&quot;\b(\\\\|\\[^\\]|[^&quot;\\])*&quot;"><token type="LiteralString"/></rule>
<rule pattern="\n"><token type="TextWhitespace"/></rule>
</state>
</rules>
</lexer>
@@ -0,0 +1,98 @@
<lexer>
<config>
<name>AMPL</name>
<alias>ampl</alias>
<filename>*.mod</filename>
<filename>*.run</filename>
<mime_type>text/x-ampl</mime_type>
<case_insensitive>false</case_insensitive>
<dot_all>true</dot_all>
</config>
<rules>
<state name="root">
<!-- Whitespace -->
<rule pattern="\s+">
<token type="Text"/>
</rule>
<!-- Comments: '#' to end-of-line and C-style comments -->
<rule pattern="#[^\n]*">
<token type="Comment"/>
</rule>
<rule pattern="/\*">
<token type="Comment"/>
<push state="comment"/>
</rule>
<!-- Strings -->
<rule pattern="&quot;(\\.|[^\\&quot;])*&quot;">
<token type="LiteralString"/>
</rule>
<rule pattern="&#39;(\\.|[^\\&#39;])*&#39;">
<token type="LiteralString"/>
</rule>
<!-- Numbers -->
<rule pattern="\b\d+\.\d*([eE][+-]?\d+)?\b">
<token type="LiteralNumberFloat"/>
</rule>
<rule pattern="\b\d+[eE][+-]?\d+\b">
<token type="LiteralNumberFloat"/>
</rule>
<rule pattern="\b\d+\b">
<token type="LiteralNumberInteger"/>
</rule>
<!-- Commands / Reserved keywords -->
<rule pattern="\b(call|cd|close|commands|data|delete|display|drop|end|environ|exit|expand|include|load|model|objective|option|problem|purge|quit|redeclare|reload|remove|reset|restore|shell|show|solexpand|solution|solve|update|unload|xref|coeff|coef|cover|obj|interval|default|from|to|to_come|net_in|net_out|dimen|dimension|check|complements|write|function|pipe|format|if|then|else|in|while|repeat|for)\b">
<token type="KeywordReserved"/>
</rule>
<!-- Types -->
<rule pattern="\b(integer|binary|symbolic|ordered|circular|reversed|INOUT|IN|OUT|LOCAL)\b">
<token type="KeywordType"/>
</rule>
<!-- Declarations (set/param/var etc) -->
<rule pattern="\b(set|param|var|arc|minimize|maximize|subject to|s\.t\.|subj to|node|table|suffix|read table|write table)\b">
<token type="KeywordDeclaration"/>
</rule>
<!-- Builtins / Functions -->
<rule pattern="\b(abs|acos|acosh|alias|asin|asinh|atan|atan2|atanh|ceil|ctime|cos|exp|floor|log|log10|max|min|precision|round|sin|sinh|sqrt|tan|tanh|time|trunc|Beta|Cauchy|Exponential|Gamma|Irand224|Normal|Normal01|Poisson|Uniform|Uniform01|num|num0|ichar|char|length|substr|sprintf|match|sub|gsub|print|printf|next|nextw|prev|prevw|first|last|ord|ord0|card|arity|indexarity)\b">
<token type="NameBuiltin"/>
</rule>
<!-- Operator words (logical/set operators) -->
<rule pattern="\b(or|exists|forall|and|in|not|within|union|diff|difference|symdiff|inter|intersect|intersection|cross|setof|by|less|sum|prod|product|div|mod)\b">
<token type="OperatorWord"/>
</rule>
<!-- Identifiers -->
<rule pattern="[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)*">
<token type="Name"/>
</rule>
<!-- Operators and punctuation -->
<rule pattern="\+|\-|\*|/|\*\*|=|&lt;=|&gt;=|==|\||\^|&lt;|&gt;|\!|\.\.|:=|\&amp;|\!=|&lt;&lt;|&gt;&gt;">
<token type="Operator"/>
</rule>
<rule pattern="[(),;:\[\]{}.]">
<token type="Punctuation"/>
</rule>
</state>
<state name="comment">
<rule pattern="[^*]+">
<token type="Comment"/>
</rule>
<rule pattern="\*/">
<token type="Comment"/>
<pop depth="1"/>
</rule>
<rule pattern="\*">
<token type="Comment"/>
</rule>
</state>
</rules>
</lexer>

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