1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -04:00

Compare commits

...

186 Commits

Author SHA1 Message Date
bbedward db4de55338 popout: decouple shadow from content layer 2026-02-18 10:46:01 -05:00
bbedward 37ecbbbbde popout: disable layer after animation 2026-02-18 10:34:21 -05:00
purian23 d6a6d2a438 notifications: Maintain shadow during expansion 2026-02-18 10:34:21 -05:00
purian23 bf1c6eec74 notifications: Update initial popup height surfaces 2026-02-18 10:34:21 -05:00
bbedward 0ddae80584 running apps: fix scroll events being propagated fixes #1724 2026-02-18 10:34:21 -05:00
bbedward 5c96c03bfa matugen: make v4 detection more resilient 2026-02-18 09:57:35 -05:00
bbedward dfe36e47d8 process list: fix scaling with fonts fixes #1721 2026-02-18 09:57:35 -05:00
purian23 63e1b75e57 dankinstall: Fix Debian ARM64 detection 2026-02-18 09:57:35 -05:00
bbedward 29efdd8598 matugen: detect emacs directory fixes #1720 2026-02-18 09:57:35 -05:00
bbedward 34d03cf11b osd: optimize bindings 2026-02-18 09:57:35 -05:00
bbedward c339389d44 screenshot: adjust cursor CLI option to be more explicit 2026-02-17 22:28:46 -05:00
bbedward af5f6eb656 settings: workaround crash 2026-02-17 22:20:19 -05:00
purian23 a6d28e2553 notifications: Tweak animation scale & settings 2026-02-17 22:07:36 -05:00
bbedward 6213267908 settings: guard internal writes from watcher 2026-02-17 22:03:57 -05:00
bbedward d084114149 cc: fix plugin reloading in bar position changes 2026-02-17 17:25:19 -05:00
bbedward f6d99eca0d popout: anchor height changing popout surfaces to top and bottom 2026-02-17 17:25:19 -05:00
bbedward 722eb3289e workspaces: fix named workspace icons 2026-02-17 17:25:19 -05:00
bbedward b7f2bdcb2d dankinstall: no_anim on dms layers 2026-02-17 17:25:19 -05:00
bbedward 11c20db6e6 1.4.1 2026-02-17 14:08:15 -05:00
bbedward 8a4e3f8bb1 system updater: fix hide no update option 2026-02-17 14:08:04 -05:00
bbedward bc8fe97c13 launcher: fix kb navigation not always showing last delegate in view 2026-02-17 14:08:04 -05:00
bbedward 47262155aa doctor: add qt6-imageformats check 2026-02-17 14:08:04 -05:00
bbedward dd4c41a6b2 v1.4.0 2026-02-17 12:01:36 -05:00
bbedward 92a25fdb6a process list: add all/user/system filters 2026-02-17 11:25:05 -05:00
bbedward d6650be008 dgop service: expose username 2026-02-17 11:04:55 -05:00
bbedward 2646e7b19a launcher v2: apply transparency to footer 2026-02-17 10:54:49 -05:00
bbedward 4133f11d82 changelog: remove text note 2026-02-17 10:50:01 -05:00
bbedward 22ed740394 ripple: small tweaks to shader 2026-02-17 10:39:32 -05:00
bbedward 063299a434 cc: network tab performance improvements 2026-02-17 10:25:19 -05:00
bbedward 44d836c975 ripple: use a shader for ripple effect 2026-02-17 09:27:18 -05:00
bbedward da437e77fb keybinds: auto-focus cheatsheet search 2026-02-17 08:44:47 -05:00
Jonas Bloch 34a6bbfb32 feat: Keybinds cheatsheet search (#1706)
* feat(wip): add fuzzy finder in keybinds cheatsheet

* fix: replace GridLayout with RowLayout and don't use anchors in KeybindsModal

* fix: replace fuzzyfinder with simple inclusion criterion for keybind search

* fix: bring back categoryKeys (there was no reason to remove it)
2026-02-17 08:42:45 -05:00
Jonas Bloch 9ed53bac9e feat: Auto settings reload (#1707)
* feat: auto-reload settings json file

* fix: set settings file reload debounce to 50ms
2026-02-17 08:41:18 -05:00
purian23 3a6752c3d2 dock: Update indicator padding 2026-02-17 07:55:33 -05:00
purian23 ef19568dd7 audio: New ability to hide input/output devices
- Updated slider presets
- Disabled mouse wheel scrolling on list scroll
2026-02-17 00:54:32 -05:00
bbedward f280cd9d3b keybinds: dont pass dirs 2026-02-16 23:55:11 -05:00
Divya Jain cf4ce3c476 add support for globalprotect vpn using saml auth flow (#1689)
* add support for globalprotect vpn using saml auth flow

* go fmt

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-16 23:25:35 -05:00
bbedward ae59e53c4c settings: fix wallpaper cycle buttons 2026-02-16 23:14:56 -05:00
dms-ci[bot] 7e0d661f63 nix: update vendorHash for go.mod changes 2026-02-17 04:02:14 +00:00
bbedward 0b33d3f905 miraclewm: add support for Miracle WM 2026-02-16 23:00:25 -05:00
purian23 d62bdda56b theme: Add Cosmic light/dark & icon theming support 2026-02-16 21:25:30 -05:00
Lucas 5841b38cd9 Update nix packaging (#1703)
* nix: add kimageformats to DMS qml dependencies

* nix: enable polkit by default in NixOS module
2026-02-16 20:47:40 -05:00
purian23 83e2b5a7a6 notifications: Tweak toast button padding 2026-02-16 19:29:57 -05:00
bbedward 2f863f64ee core: set qt platform to wayland;xcb by default 2026-02-16 18:28:31 -05:00
bbedward 1a8b397cfd weather: keep tab height consistent 2026-02-16 18:14:30 -05:00
bbedward 196c421b75 Squashed commit of the following:
commit 051b7576f7
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 16:38:45 2026 -0500

    Height for realz

commit 7784488a61
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 16:34:09 2026 -0500

    Fix height and truncate text/URLs

commit 31b328d428
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 16:25:57 2026 -0500

    notifications: handle URL encoding in markdown2html

commit dbb04f74a2
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 16:10:20 2026 -0500

    notifications: more comprehensive decoder

commit b29c7192c2
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 15:51:37 2026 -0500

    notifications: html unescape

commit 8a48fa11ec
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 15:04:33 2026 -0500

    Add expressive curve on init toast

commit ee124f5e04
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 15:02:16 2026 -0500

    Expressive curves on swipe & btn height

commit 0fce904635
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 13:40:02 2026 -0500

    Provide bottom button clearance

commit 00d3829999
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 13:24:31 2026 -0500

    notifications: cleanup popup display logic

commit fd05768059
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 01:00:55 2026 -0500

    Add Privacy Mode
    - Smoother notification expansions
    - Shadow & Privacy Toggles

commit 0dba11d845
Author: purian23 <purian23@gmail.com>
Date:   Sat Feb 14 22:48:46 2026 -0500

    Further M3 enhancements

commit 949c216964
Author: purian23 <purian23@gmail.com>
Date:   Sat Feb 14 19:59:38 2026 -0500

    Right-Click to set Rules on Notifications directly

commit 62bc25782c
Author: bbedward <bbedward@gmail.com>
Date:   Fri Feb 13 21:44:27 2026 -0500

    notifications: fix compact spacing, reveal header bar, add bottom center
    position, pointing hand cursor fix

commit ed495d4396
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 20:25:40 2026 -0500

    Tighten init toast

commit ebe38322a0
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 20:09:59 2026 -0500

    Update more m3 baselines & spacing

commit b1735bb701
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 14:10:05 2026 -0500

    Expand rules on-Click

commit 9f13546b4d
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 12:59:29 2026 -0500

    Add Notification Rules
    - Additional right-click ops
    - Allow for 3rd boy line on init notification popup

commit be133b73c7
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 10:10:03 2026 -0500

    Truncate long title in groups

commit 4fc275bead
Author: bbedward <bbedward@gmail.com>
Date:   Thu Feb 12 23:27:34 2026 -0500

    notification: expand/collapse animation adjustment

commit 00e6172a68
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:50:11 2026 -0500

    Fix global warnings

commit 0772f6deb7
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:46:40 2026 -0500

    Tweak expansion duration

commit 0ffeed3ff0
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:16:16 2026 -0500

    notifications: Update Material 3 baselines
    - New right-click to mute option
    - New independent Notification Animation settings
2026-02-16 17:57:13 -05:00
bbedward 8399d64c2d settings: drop beta from confiugration 2026-02-16 17:51:20 -05:00
bbedward c530eab303 settings: fix dropped disconnected displays on save 2026-02-16 17:47:28 -05:00
xdenotte 45b6362dd3 fix: correct preview centering with scaling (#1701) 2026-02-16 17:47:21 -05:00
bbedward 50b77dcfc3 i18n: term update 2026-02-16 17:42:10 -05:00
bbedward be8f3adf01 core/screensaver: add methods to introspect XML 2026-02-16 17:36:49 -05:00
bbedward 75a8c171ea launcher: remove double loader 2026-02-16 12:17:46 -05:00
bbedward 466ff59573 launcher: keep loaded default 2026-02-16 12:06:31 -05:00
bbedward 053bb91927 process list: fix clipped graphs
fixes #1697
2026-02-16 11:37:19 -05:00
bbedward 2c9b22c016 changelog: add and enable 1.4 changelog 2026-02-16 10:33:59 -05:00
Jon Rogers a9ee91586e fix: preserve _preScored from plugin items to allow ordering control (#1696)
The _preScored property allows plugins to control the ordering of
launcher results. Previously, this property was being overwritten
with undefined during item transformation, preventing plugins from
controlling which items appear first.

This change preserves the _preScored value from plugin items in:
- transformBuiltInLauncherItem()
- transformPluginItem()

Required for: devnullvoid/dms-web-search#7
2026-02-16 00:38:10 -05:00
Kristoffer Grönlund 81bce74612 greeter: Add support for Debian greetd user/group name (#1685)
* greeter: Detect user and group used by greetd

On most distros greetd runs as user and group "greeter",
but on Debian the user and group "_greetd" are used.

* greeter: Use correct group in sync command

* greeter: more generic group detection

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-16 00:36:58 -05:00
purian23 f2a6d2c7da core: Fix DMS Greeter group check & add Cosmic support 2026-02-15 22:48:22 -05:00
bbedward 0a9a34912e wallpapers: support more image formats + case insensitivity
fixes #1694
fixes #1660
2026-02-15 16:22:27 -05:00
Higor Prado abff670814 fix(niri): restore lazy overview spotlight lifecycle to reduce idle VRAM (#1693) 2026-02-15 15:49:55 -05:00
bbedward 0d49acaaa8 launcher: try a more targeted unload approach 2026-02-15 15:48:49 -05:00
Higor Prado ebe1785411 fix(launcher): release DankLauncherV2 resources after close (#1692)
* fix(launcher): release DankLauncherV2 resources after close

* launcher: make unload on close optional

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-15 15:26:03 -05:00
bbedward f9f0192b22 i18n: update terms 2026-02-15 13:40:34 -05:00
bbedward e5cdbf4cf5 clipboard: option to paste on enter
fixes #1686
2026-02-15 13:36:19 -05:00
Higor Prado 13ef1efa7b fix(qml): optimize VRAM usage in DankRipple (#1691)
Replace layer+MultiEffect mask with GPU-native clipping for rounded
corners. The previous approach created offscreen textures for every
clickable element with rounded corners (used in 34+ files across the
UI), adding 100-300MB VRAM on NVIDIA GPUs.

The new approach uses clip: true on a Rectangle with the corner
radius, which is handled natively by the GPU without creating
intermediate textures.

Visual impact: Minimal - the ripple effect works identically.
The only theoretical difference is slightly less smooth edges on
rounded corners during the ripple animation, which is not noticeable
at 10% opacity during the quick animation.

VRAM improvement: Tested on NVIDIA, ~100-300MB reduction.
2026-02-15 13:25:45 -05:00
Sunny fbd9301a2d fixed emacs template to work for both light and dark themes (#1682) 2026-02-15 12:22:23 -05:00
Artem 24e3024b57 fix(brightness): refresh sysfs cache on hotplug (#1674)
* fix(brightness): refresh sysfs cache on hotplug

The SysfsBackend used a cache that was never refreshed on display hot plug, causing new backlight devices to not appear in IPC until restart.

This adds Rescan() to SysfsBackend and calls it in Manager.Rescan(), matching the behavior of DDCBackend.

Fixes: hotplugged external monitor brightness control via IPC

* make fmt

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-14 14:00:01 -05:00
bbedward 52d5af11ba dankdash: fix triggering when clock widget isnt present on bar
fixes #1601
2026-02-14 13:54:51 -05:00
bbedward 44a45b00cf widgets: cleanup rectangles across popouts, modals, OSDs 2026-02-14 11:15:26 -05:00
bbedward 2b78fe5b9f popout: remove double rectangle artifact 2026-02-14 10:45:50 -05:00
bbedward 14f92669c6 doctor: add cups-pk-helper 2026-02-14 10:38:03 -05:00
bbedward 124106de87 scrollies: switch to frame animation for kinetic scroll 2026-02-13 22:26:36 -05:00
Connor Welsh bb8e0d384f dock: resolve icons for pre-substituted app IDs (#1669) 2026-02-13 21:40:17 -05:00
bbedward 59d37847ec osd: allow overriding layer 2026-02-13 18:04:34 -05:00
chimera acdc531dca MangoWC and Scroll Greeter Support for NixOS (#1647)
* add mangowc greeter to nix.

i am going to be suprised if this only needed this line

* point mangowc to mango

there is no way this works

* mango flake detection and maybe scroll support

* " "

* no mango flake dependency

* mango dependency remove too

i have got to add "parenthesis" to stuff more

* Final De-dependification of MangoWC

it works without the flake YES

* mangowc -> mango pt 1

* mangowc -> mango pt 2

necessary evil. will break inital greetd confs but works after change

* Preserve Compatibility
2026-02-13 17:38:21 -05:00
bbedward ce75dac81b track art: use URLs directly 2026-02-13 17:31:51 -05:00
bbedward b8d40761ff network: simplify connection handling 2026-02-13 17:24:58 -05:00
bbedward 3a7430f6da osd: reverse media playback icons and handle screen changes 2026-02-13 15:43:46 -05:00
bbedward 242660c51d theme: improve handling of custom themes with variants and accents in
light/dark mode (e.g. catpuccin will react to light/dark changes and
remember theme per-mode)
fixes #1094
2026-02-13 10:31:59 -05:00
bbedward 8a6c1e45ce themes: fix overflow of option button group
fixes #1399
2026-02-13 10:22:07 -05:00
bbedward b8e5f9f3b1 matugen: support v4 2026-02-13 09:40:51 -05:00
bbedward d60e70f9cc notifications: fix crash in modal 2026-02-12 23:15:22 -05:00
bbedward cdb70fadb3 launcher v2: fix kb navigation to top of scroll 2026-02-12 22:41:40 -05:00
purian23 7867deef60 dock: Fix option to use custom logos 2026-02-12 21:02:06 -05:00
bbedward a77c1adb32 matugen: dont signal terminals when disabled
fixes #1658
2026-02-12 16:57:21 -05:00
bbedward da14d75a3b i18n: term update 2026-02-12 15:06:23 -05:00
Bernardo Gomes 7c66a34931 fix(i18n): capture missing strings and add pt-BR translations (#1654) 2026-02-12 15:05:44 -05:00
Bernardo Gomes 425715e0f0 feat(notifications): add configurable notification rules (#1655) 2026-02-12 15:04:02 -05:00
bbedward a3baf8ce31 running apps: fix focusing of windows when grouped 2026-02-12 14:51:10 -05:00
bbedward 605e03b065 dankbar: fix spacing at scale of running apps, dock, and system tray 2026-02-12 14:38:31 -05:00
bbedward 0e9b21d359 plugins: add plugin state helpers 2026-02-12 14:04:56 -05:00
bbedward ba5bf0cabc i18n: general RTL fixes 2026-02-12 11:58:32 -05:00
bbedward 96b9d7aab3 ci: update go version and golangci-lint version 2026-02-12 09:57:38 -05:00
dms-ci[bot] 750e4c4527 nix: update vendorHash for go.mod changes 2026-02-12 14:50:23 +00:00
bbedward 7417e26444 core: replace go-localereader directive 2026-02-12 09:48:10 -05:00
bbedward 00e1099912 weather: light redesign for dash card 2026-02-12 09:42:29 -05:00
bbedward bd46d29ff0 settings: optimize sidebar bindings 2026-02-11 18:42:32 -05:00
bbedward 1a9d7684b9 wallpaper: fix per-monitor view modes
fixes #1582
2026-02-11 17:58:44 -05:00
bbedward 0133c19276 dock: fix auto-hide hit area
media osd: fix showing without album art
2026-02-11 17:51:29 -05:00
ArijanJ 46bb3b613b feat: add osd toggles to search index (#1652)
* feat: add osd toggles to search index
this commit also regenerates the search index

* add newline at end of translations file

* ran prek to fix the file :)
2026-02-11 13:28:04 -05:00
bbedward 5839a5de30 displays: add full screen only for hyprland and convert vrr to dropdown
fixes #1649
fixes #1548
2026-02-11 09:31:35 -05:00
bbedward 535d0bb0f0 lock/greeter: fix keyboard layout on Hyprland
fixes #1650
fixes #672
fixes #1600
2026-02-11 08:57:24 -05:00
bbedward 4d316007af lock: add lock at startup action, not sure how to handle it in crash
scenarios
launcher v2: fix state reset in section changes
fixes #1648
2026-02-10 23:25:54 -05:00
purian23 3c2d60d8e1 fix: QT notifs warning 2026-02-10 21:38:25 -05:00
purian23 9c4f4cbd0d notifications: Add Left/Right Keyboard Nav to Current/History tabs 2026-02-10 20:51:13 -05:00
bbedward a337585b00 core/server/dbus: suppress unsubscribe warnings 2026-02-10 17:52:39 -05:00
bbedward 1cdec5d687 launcher v2: add visibility guards 2026-02-10 17:40:41 -05:00
bbedward 081b15e24c dock: fix intelligent auto hide on hyprland
fixes #1535
2026-02-10 17:29:39 -05:00
purian23 b04cb7b3cc guide: Include Fedora paths in the Contributing guide 2026-02-10 16:07:33 -05:00
ArijanJ e2c3ff00fb feat(ipc): add player-specific mpris volume control (#1645)
* feat: add mpris volume control through ipc

* feat: add mpris volume action and default binds
2026-02-10 15:44:56 -05:00
bbedward c783ff3dcf core: add DL helper, apply to TrackArt OSD, DankLocationSearch
- unrelated change to add gsettingsOrDconf helpers
2026-02-10 15:42:40 -05:00
bbedward 2c360dc3e8 mautgen: post-hook reload GTK4 and qt6ct
fixes #1643
2026-02-10 15:06:44 -05:00
bbedward 5342647bfb launcher v2: performance optimizations
- Use ListView in all tab
- use filesystem cache to speed up first launch
- apply highlights to visible models
2026-02-10 14:56:29 -05:00
bbedward 46a2f6f0d8 launcher v2: de-dupe cached entries by ID 2026-02-10 12:59:53 -05:00
bbedward f8af8fc171 processlist: fix default popout focus 2026-02-10 12:52:11 -05:00
bbedward 3d0ee9d72b animations/ripple: clean up effect and apply more universally 2026-02-10 12:48:12 -05:00
bbedward 5a0bb260b4 popout: only scale texture size on DPR > 1 2026-02-10 09:56:20 -05:00
Erwin Boskma 9a7f1f5f2f feat: configurable volume amount on scroll for Media widget (#1641)
* Add audioWheelScrollAmount setting

* Add UI to configure volume change amount for Media Player widget

* Let Media Player widget use the configured volume change on wheel event
2026-02-10 09:12:03 -05:00
purian23 d88b04fc30 notifications: Update group expansion card animations 2026-02-09 22:48:07 -05:00
bbedward 6fe4cc98b9 popout: fix blurry text 2026-02-09 21:33:12 -05:00
ArijanJ b9bcfd8d2c Making the new media playback OSD more beautiful (#1638)
* feat: decouple track art downloads into new TrackArtService

* feat: beautify media playback osd with track art

* fix: bug when switching from art to no art
2026-02-09 21:13:23 -05:00
bbedward e3bd31bb52 clipboard: fix row layout overflow 2026-02-09 21:09:15 -05:00
purian23 0922e3e459 clipboard: Fix pinned entry logic
- Add keyboard nav to pinned entries
- Fix wrong copied selection upon Enter
2026-02-09 20:53:48 -05:00
purian23 a168b12bb2 dankbar: Fix widget context focus w/Autohide enabled 2026-02-09 19:42:27 -05:00
bbedward 8c01deba86 anims: revise ListView animations 2026-02-09 13:21:35 -05:00
bbedward b645487e79 cc: expand mouse areas of scroll/click targets in bar 2026-02-09 13:15:43 -05:00
bbedward 91569affd7 displays: update mango display config syntax
fixes #1629
2026-02-09 09:49:31 -05:00
bbedward 1ed44ee6f3 audio: add per-device max volume limit setting 2026-02-09 09:26:34 -05:00
bbedward fce120fa31 system monitor: disable anims until list is stable 2026-02-08 22:16:45 -05:00
bbedward a02b0c0c3c animations: tweak list view transitions to not animate X 2026-02-08 22:10:51 -05:00
bbedward c86999f389 popout: move layer to content wrapper 2026-02-08 22:06:35 -05:00
bbedward 2b546967d2 weather: fix anim on open 2026-02-08 21:43:56 -05:00
purian23 591d2ba4d4 settings: DankCollapsible 2026-02-08 20:26:06 -05:00
purian23 37cc4ab197 dms: Material Animation Refactor
- Thanks Google for Material 3 Expressive stuffs
- Thanks Caelestia shell for pushing qml limits to showcase the blueprint
2026-02-08 20:24:37 -05:00
Jhannes Reimann d775974a90 i18n: wrap missing user-facing strings in I18n.tr() (#1624)
* fix: reverse rotation direction of sync icon in plugin browser

* i18n: wrap missing user-facing strings in I18n.tr()
2026-02-08 19:18:17 -05:00
Vladimir Kosteley cc62aa4a9e fix: VpnPopout and Vpn widget tooltip positions (#1623)
* fix(vpn-widget): correct tooltip positioning for bottom bar alignment

- Calculate tooltip Y position based on bar edge (bottom vs top)
- Position tooltip above bar when edge is bottom to prevent overflow
- Account for tooltip height when positioning on bottom edge
- Use proper screen reference for consistent positioning across displays

* fix(VpnPopout): use correct screen height for popup sizing

- Fix popup height calculation to use the assigned screen property
  instead of the global Screen object
- Prevents incorrect positioning when multiple screens are present
- Fallback to Screen.height if screen property is not set

* fix(widgets): close DankPopout when screen is removed

- Add Connections handler to monitor Quickshell.onScreensChanged
- Automatically close popout if its assigned screen no longer exists
- Prevent orphaned popouts when displays are disconnected
2026-02-08 19:17:53 -05:00
bbedward 5b8b7b04be niri: replace github ref 2026-02-08 10:21:34 -05:00
Tulip Blossom b4a8853591 fix(greeter): use 0755 permissions for greeter directories (#1619)
* fix(greeter): use 0755 permissions for greeter directories

Directories need to be executable to read/write to them (weirdly enough)

Fixes: https://github.com/AvengeMedia/DankMaterialShell/issues/1618

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>

* fix: also need 4 for world writeable perms on DMS greeter directory

---------

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>
2026-02-07 22:25:39 -05:00
Jhannes Reimann 4220dfe2a5 fix: reverse rotation direction of sync icon in plugin browser (#1617) 2026-02-07 18:53:00 -05:00
bbedward 4557426c28 notifications: fix kb navigation breaking on history tab close 2026-02-07 17:42:20 -05:00
purian23 8ee7fe8e66 notifications: Refactor Animations 2026-02-07 17:25:26 -05:00
bri c4a41f994a feat(dms-greeter): add Niri override kdl includes (#1616)
This will optionally include one or two files into the Niri conf for
dms-greeter:
  - `/usr/share/greetd/niri_overrides.kdl` (for building into a system image)
  - `/etc/greetd/niri_overrides.kdl` (for local overrides)

This enables a distro or a system administrator to easily add their own
customizations to the dms-greeter Niri configuration.

Note: Niri next includes https://github.com/YaLTeR/niri/pull/3022 which
can make this a whole lot simpler: we're testing for the existence of
files and optionally adding `include` lines within the dms-greeter shell
script, but in the future it will be possible to simply use `include
optional=true` which we can just append unconditionally.

Lastly, I deduplicated the line that spawns the actual greeter by
appending that unconditionally at the end.
2026-02-07 12:45:13 -05:00
bbedward fa639424f5 core/config: update default steam window rules
fixes #1615
2026-02-07 11:43:34 -05:00
bbedward e618a8390c launcher v2: fix hover effect and view mode pref in all tab 2026-02-06 11:53:59 -05:00
bbedward 393e9ed2e4 greeter: try to fix random nixos issue 2026-02-06 10:56:44 -05:00
bbedward e1ea441215 cava: use input source pipwire and auto 2026-02-06 10:12:16 -05:00
bbedward 654661fd66 cli/setup: add subcommands for individual includes 2026-02-06 09:53:41 -05:00
bbedward c5a21f8da0 niri: ensure other configs too 2026-02-06 08:20:17 -05:00
bbedward ca5b168117 niri: add ensure colors.kdl existence
fixes #1606
2026-02-06 08:18:37 -05:00
bbedward aa88eb42ee cava: remove input config 2026-02-06 08:10:48 -05:00
purian23 ac84cadd77 fix: Truncate Media Playback OSD 2026-02-05 22:21:45 -05:00
ArijanJ 81d5235b9f feat: media playback OSD (#1602)
* feat: media playback OSD

* Update code style improvements
- Removes resetTimer to reuse event driven code

---------

Co-authored-by: purian23 <purian23@gmail.com>
2026-02-05 19:18:52 -05:00
bbedward 8944762c76 notifications: cap max anim speed in popout 2026-02-05 15:17:17 -05:00
bbedward 3d05c34673 i18n: sync terms 2026-02-05 14:38:17 -05:00
bbedward c2ee41c844 running apps: make settings bar-specific 2026-02-05 14:37:05 -05:00
bbedward 6b537f30a5 matugen: sync adwaita accent color by visual similarity 2026-02-05 14:16:30 -05:00
bbedward a3ae95df09 launcher v2: general performance improvements 2026-02-05 13:22:49 -05:00
bbedward 4349d68f87 greeter: block loading of memory file 2026-02-05 12:18:44 -05:00
bbedward 7d5c20125a animations: fine-grained anim settings for modals and popouts 2026-02-05 12:10:17 -05:00
bbedward 2583dbd3f2 niri/keybinds: expose when-locked, inhibitied, repeat through GUI editor
fixes #1437
2026-02-05 11:55:25 -05:00
bbedward a103b93583 greeter: add connection on session load 2026-02-05 09:40:27 -05:00
claymorwan fff018eafb fix(layers): change layers namespace for desktop widgets and plugin popouts (#1594) 2026-02-05 09:18:47 -05:00
bbedward 60b824e7a4 i18n: update settings search index 2026-02-05 09:12:08 -05:00
bbedward e27e904157 doctor: add --copy option for github issue reporting 2026-02-04 19:33:22 -05:00
bbedward fe15667986 clipboard: add watch -m for mime-types 2026-02-04 11:36:41 -05:00
bbedward bd9029e533 niri: support any screenshot editor tool 2026-02-04 11:05:52 -05:00
bbedward fa71d563ea vpn: uncheck "save password" by defaul 2026-02-04 10:55:06 -05:00
bbedward 143918bc5e plugins: fix reload IPC on failure 2026-02-04 09:46:10 -05:00
purian23 961680af8c feat: Alias for Audio Devices
- New custom audio UI to set custom names for input/output devices
2026-02-04 07:09:55 -05:00
purian23 6e3b3ce888 fix: Notepad Transparency override 2026-02-03 21:19:57 -05:00
bbedward 44292c3b55 theme: fix popup transparency setting 2026-02-03 21:06:15 -05:00
bbedward c024c1b8e4 matugen: fix emacs template
fixes #1580
2026-02-03 16:15:13 -05:00
bbedward 13adfdec11 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2026-02-03 16:04:11 -05:00
bbedward e9ec28aab7 core/greeter: embed base config, tie enable into sync, sync config
argument always, preserve existing args
2026-02-03 16:02:42 -05:00
purian23 0af4d1d6e3 greeter: Restore baseline configs 2026-02-03 15:52:07 -05:00
purian23 3ef0e63533 feat: DMS Greeter Sync w/niri include settings
- cursor, debug, input & options
- This lets the greeter inherit your niri display/output layout, input device behavior, cursor settings & debug flags overrides
2026-02-03 14:57:29 -05:00
purian23 f4dad69ccd General agent files 2026-02-03 14:56:56 -05:00
bbedward b811316d0c DankPopout: make bg and content siblings 2026-02-03 14:31:31 -05:00
bbedward f59aeb2782 dankbar: fix centering of numerous bar widgets 2026-02-03 13:44:57 -05:00
bbedward 24ce41935e bluetooth: improve performance of details 2026-02-03 11:30:38 -05:00
bbedward 3c4749ead0 widgets: add a button color setting 2026-02-03 11:03:33 -05:00
xxyangyoulin 22ab5b9660 feat(ipc): add tray icon control commands (#1576)
Add IPC commands to interact with system tray icons:
  - `dms ipc call tray list` - list all tray items
  - `dms ipc call tray activate <id>` - activate (left-click) a tray item
  - `dms ipc call tray status <id>` - show tray item details
2026-02-03 09:36:35 -05:00
bbedward 22f16f1da3 widgets: theme text field selection color 2026-02-03 09:35:35 -05:00
bbedward a97409dfd7 window: freeze mask geometry in popout 2026-02-02 16:29:59 -05:00
grokXcopilot eaa6a664c8 feat(niri): Add drag-and-drop workspace reordering (#1569)
* feat(niri): Add drag-and-drop workspace reordering

Add interactive drag-and-drop reordering for Niri workspace indicators
with smooth animations matching the system tray behavior.

- Add moveWorkspaceToIndex() to NiriService for workspace reordering
- Implement drag detection with 5px threshold
- Add shift animation for items between source and target
- Clamp drag offset to stay within workspace row bounds
- Reset drag state when workspace list changes during drag
- Visual feedback: opacity change, border highlight on drag/drop target

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(settings): Add workspace drag reorder toggle

Add workspaceDragReorder setting to enable/disable workspace
drag-and-drop reordering. Enabled by default, only visible on Niri.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:58:05 -05:00
bbedward d934b3b3b4 launcher v2: improve search result responsiveness, highlight matches 2026-02-02 12:49:20 -05:00
327 changed files with 27323 additions and 8744 deletions
+57
View File
@@ -0,0 +1,57 @@
{
"permissions": {
"allow": [
"Bash(cat:*)",
"Bash(git -C /home/purian23/dms diff --stat .github/workflows/)",
"Bash(git -C /home/purian23/projects/danklinux diff --stat .github/workflows/)",
"Bash(git -C /home/purian23/dms diff .github/workflows/)",
"Bash(git -C /home/purian23/dms diff .github/workflows/run-ppa.yml)",
"Bash(osc cat:*)",
"Bash(ls:*)",
"Bash(find:*)",
"Bash(git show-ref:*)",
"Bash(git tag:*)",
"Bash(bash -c 'ALL_PATHS=$(grep -A 5 \"\"<service name=\\\"\"download_url\\\"\">\"\" distro/debian/dms/_service | grep \"\"<param name=\\\"\"path\\\"\">\"\" | sed \"\"s/.*<param name=\\\"\"path\\\"\">\\(.*\\)<\\/param>.*/\\1/\"\"); SOURCE_PATH=\"\"\"\"; for path in $ALL_PATHS; do if echo \"\"$path\"\" | grep -qE \"\"(source|archive|\\.tar\\.(gz|xz|bz2))\"\" && ! echo \"\"$path\"\" | grep -qE \"\"(distropkg|binary)\"\"; then SOURCE_PATH=\"\"$path\"\"; break; fi; done; echo \"\"Selected path: $SOURCE_PATH\"\"')",
"Bash(curl:*)",
"Bash(tar:*)",
"Bash(git -C /home/purian23/dms log:*)",
"Bash(osc status:*)",
"Bash(osc commit:*)",
"Bash(osc up:*)",
"Bash(osc results:*)",
"Bash(osc api:*)",
"Bash(systemctl:*)",
"Bash(dms version:*)",
"Bash(git describe:*)",
"Bash(qmlsc:*)",
"Bash(qmllint-qt6:*)",
"Bash(make fmt:*)",
"Bash(make test:*)",
"Bash(dms chroma list-styles:*)",
"Bash(python3:*)",
"Bash(time dms chroma:*)",
"Bash(dms chroma:*)",
"Bash(make build:*)",
"Bash(pgrep:*)",
"Bash(go build:*)",
"Bash(/tmp/dms-test chroma:*)",
"Bash(1)",
"Bash(go install:*)",
"Bash(grep:*)",
"Bash(journalctl:*)",
"Bash(qdbus:*)",
"Bash(TZ='Asia/Tokyo' date:*)",
"Bash(dms --help:*)",
"Bash(dms run:*)",
"Bash(dms status:*)",
"Bash(dms kill:*)",
"Bash(tee:*)",
"Bash(qmlscene:*)",
"Bash(quickshell --version:*)",
"WebFetch(domain:forum.qt.io)",
"Bash(gh api:*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)"
]
}
}
+3 -3
View File
@@ -45,9 +45,9 @@ body:
- type: textarea - type: textarea
id: dms_doctor id: dms_doctor
attributes: attributes:
label: dms doctor -v label: dms doctor -vC
description: Output of `dms doctor -v` command description: Output of `dms doctor -vC` command
placeholder: Paste the output of `dms doctor -v` here placeholder: Paste the output of `dms doctor -vC` here
validations: validations:
required: true required: true
- type: textarea - type: textarea
+3 -3
View File
@@ -30,9 +30,9 @@ body:
- type: textarea - type: textarea
id: dms_doctor id: dms_doctor
attributes: attributes:
label: dms doctor -v label: dms doctor -vC
description: Output of `dms doctor -v` command description: Output of `dms doctor -vC` command
placeholder: Paste the output of `dms doctor -v` here placeholder: Paste the output of `dms doctor -vC` here
validations: validations:
required: false required: false
- type: textarea - type: textarea
+19
View File
@@ -191,6 +191,11 @@ jobs:
git fetch origin --force tag ${TAG} git fetch origin --force tag ${TAG}
git checkout ${TAG} git checkout ${TAG}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -229,6 +234,7 @@ jobs:
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems - **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems - **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems - **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
- **`dms-qml.tar.gz`** - QML source code only - **`dms-qml.tar.gz`** - QML source code only
### Checksums ### Checksums
@@ -387,6 +393,19 @@ jobs:
rm -rf _temp_full rm -rf _temp_full
done done
- name: Generate vendored source tarball
run: |
set -euxo pipefail
VERSION_NUM=${TAG#v}
cd core
go mod vendor
cd ..
tar czf "_release_assets/dms-cli-${VERSION_NUM}.tar.gz" \
--transform "s,^core/,dms-cli-${VERSION_NUM}/," \
--exclude='core/.git' \
core/
(cd _release_assets && sha256sum "dms-cli-${VERSION_NUM}.tar.gz" > "dms-cli-${VERSION_NUM}.tar.gz.sha256")
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
+1 -1
View File
@@ -335,7 +335,7 @@ jobs:
- name: Install Go - name: Install Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.24" go-version-file: ./core/go.mod
- name: Install OSC - name: Install OSC
run: | run: |
+1 -1
View File
@@ -158,7 +158,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.24" go-version-file: ./core/go.mod
cache: false cache: false
- name: Install build dependencies - name: Install build dependencies
+2
View File
@@ -56,6 +56,8 @@ UNUSED
CLAUDE-activeContext.md CLAUDE-activeContext.md
CLAUDE-temp.md CLAUDE-temp.md
AGENTS-activeContext.md
AGENTS-temp.md
# Auto-generated theme files # Auto-generated theme files
*.generated.* *.generated.*
+34 -1
View File
@@ -37,10 +37,43 @@ This is a monorepo, the easiest thing to do is to open an editor in either `quic
1. Install the [QML Extension](https://doc.qt.io/vscodeext/) 1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path 2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
**Note:** Paths may vary by distribution. Below are examples for Arch Linux and Fedora.
**Arch Linux:**
```json ```json
{ {
"[qml]": {
"editor.defaultFormatter": "qt-project.qmlls",
"editor.formatOnSave": true
},
"qt-qml.doNotAskForQmllsDownload": true, "qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls" "qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls",
"qt-core.additionalQtPaths": [
{
"name": "Qt-6.x-linux-g++",
"path": "/usr/bin/qmake"
}
]
}
```
**Fedora:**
```json
{
"[qml]": {
"editor.defaultFormatter": "qt-project.qmlls",
"editor.formatOnSave": true
},
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/bin/qmlls",
"qt-core.additionalQtPaths": [
{
"name": "Qt-6.x-Fedora-linux-g++",
"path": "/usr/bin/qmake6"
}
]
} }
``` ```
+2 -2
View File
@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
</div> </div>
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop. DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure ## Repository Structure
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors ## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features. Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors) [Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
+1 -1
View File
@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.6.2 rev: v2.9.0
hooks: hooks:
- id: golangci-lint-fmt - id: golangci-lint-fmt
require_serial: true require_serial: true
+26
View File
@@ -112,6 +112,7 @@ var clipClearCmd = &cobra.Command{
} }
var clipWatchStore bool var clipWatchStore bool
var clipWatchMimes bool
var clipSearchCmd = &cobra.Command{ var clipSearchCmd = &cobra.Command{
Use: "search [query]", Use: "search [query]",
@@ -211,6 +212,7 @@ func init() {
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking") clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking")
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)") clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
clipWatchCmd.Flags().BoolVarP(&clipWatchMimes, "mimes", "m", false, "Show all offered MIME types")
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration") clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
@@ -328,6 +330,30 @@ func runClipWatch(cmd *cobra.Command, args []string) {
}); err != nil && err != context.Canceled { }); err != nil && err != context.Canceled {
log.Fatalf("Watch error: %v", err) log.Fatalf("Watch error: %v", err)
} }
case clipWatchMimes:
if err := clipboard.WatchAll(ctx, func(data []byte, mimeType string, allMimes []string) {
if clipJSONOutput {
out := map[string]any{
"data": string(data),
"mimeType": mimeType,
"mimeTypes": allMimes,
"timestamp": time.Now().Format(time.RFC3339),
"size": len(data),
}
j, _ := json.Marshal(out)
fmt.Println(string(j))
return
}
fmt.Printf("=== Clipboard Change ===\n")
fmt.Printf("Selected: %s\n", mimeType)
fmt.Printf("All MIME types:\n")
for _, m := range allMimes {
fmt.Printf(" - %s\n", m)
}
fmt.Printf("Size: %d bytes\n\n", len(data))
}); err != nil && err != context.Canceled {
log.Fatalf("Watch error: %v", err)
}
case clipJSONOutput: case clipJSONOutput:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) { if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
out := map[string]any{ out := map[string]any{
+1
View File
@@ -524,5 +524,6 @@ func getCommonCommands() []*cobra.Command {
chromaCmd, chromaCmd,
doctorCmd, doctorCmd,
configCmd, configCmd,
dlCmd,
} }
} }
+164 -3
View File
@@ -11,6 +11,7 @@ import (
"slices" "slices"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
@@ -101,11 +102,13 @@ var doctorCmd = &cobra.Command{
var ( var (
doctorVerbose bool doctorVerbose bool
doctorJSON bool doctorJSON bool
doctorCopy bool
) )
func init() { func init() {
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions") doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions")
doctorCmd.Flags().BoolVarP(&doctorJSON, "json", "j", false, "Output results in JSON format") doctorCmd.Flags().BoolVarP(&doctorJSON, "json", "j", false, "Output results in JSON format")
doctorCmd.Flags().BoolVarP(&doctorCopy, "copy", "C", false, "Copy results to clipboard in GitHub-friendly format")
} }
type category int type category int
@@ -192,7 +195,7 @@ func (r checkResult) toJSON() checkResultJSON {
} }
func runDoctor(cmd *cobra.Command, args []string) { func runDoctor(cmd *cobra.Command, args []string) {
if !doctorJSON { if !doctorJSON && !doctorCopy {
printDoctorHeader() printDoctorHeader()
} }
@@ -210,9 +213,17 @@ func runDoctor(cmd *cobra.Command, args []string) {
checkEnvironmentVars(), checkEnvironmentVars(),
) )
if doctorJSON { switch {
case doctorCopy:
text := formatResultsPlain(results)
if err := clipboard.CopyOpts([]byte(text), "text/plain;charset=utf-8", false, false); err != nil {
fmt.Fprintf(os.Stderr, "Failed to copy to clipboard: %v\n", err)
os.Exit(1)
}
fmt.Println("Doctor report copied to clipboard")
case doctorJSON:
printResultsJSON(results) printResultsJSON(results)
} else { default:
printResults(results) printResults(results)
printSummary(results, qsMissingFeatures) printSummary(results, qsMissingFeatures)
} }
@@ -638,6 +649,109 @@ func checkI2CAvailability() checkResult {
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"} return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
} }
func checkImageFormatPlugins() []checkResult {
url := doctorDocsURL + "#optional-features"
pluginDir := findQtPluginDir()
if pluginDir == "" {
return []checkResult{
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
}
}
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
type pluginCheck struct {
name string
desc string
plugins []struct{ file, format string }
}
checks := []pluginCheck{
{
name: "qt6-imageformats",
desc: "WebP, TIFF, GIF, JP2 support",
plugins: []struct{ file, format string }{
{"libqwebp.so", "WebP"},
{"libqtiff.so", "TIFF"},
{"libqgif.so", "GIF"},
{"libqjp2.so", "JP2"},
{"libqicns.so", "ICNS"},
},
},
{
name: "kimageformats",
desc: "AVIF, HEIF, JXL support",
plugins: []struct{ file, format string }{
{"kimg_avif.so", "AVIF"},
{"kimg_heif.so", "HEIF"},
{"kimg_jxl.so", "JXL"},
{"kimg_exr.so", "EXR"},
},
},
}
var results []checkResult
for _, c := range checks {
var found []string
for _, p := range c.plugins {
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
found = append(found, p.format)
}
}
var result checkResult
switch {
case len(found) == 0:
result = checkResult{catOptionalFeatures, c.name, statusWarn, "Not installed", c.desc, url}
default:
details := ""
if doctorVerbose {
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
}
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
}
results = append(results, result)
}
return results
}
func findQtPluginDir() string {
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
for dir := range strings.SplitSeq(envPath, ":") {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
return dir
}
}
}
// Try qtpaths
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
if dir := strings.TrimSpace(string(output)); dir != "" {
return dir
}
}
}
// Fallback: common distro paths
for _, dir := range []string{
"/usr/lib/qt6/plugins",
"/usr/lib64/qt6/plugins",
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
} {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
return dir
}
}
return ""
}
func detectNetworkBackend(stackResult *network.DetectResult) string { func detectNetworkBackend(stackResult *network.DetectResult) string {
switch stackResult.Backend { switch stackResult.Backend {
case network.BackendNetworkManager: case network.BackendNetworkManager:
@@ -678,7 +792,21 @@ func checkOptionalDependencies() []checkResult {
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1") logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL}) results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
cupsPkHelperBus := "org.opensuse.CupsPkHelper.Mechanism"
var cupsPkStatus status
var cupsPkMsg string
switch {
case utils.IsDBusServiceAvailable(cupsPkHelperBus):
cupsPkStatus, cupsPkMsg = statusOK, "Running"
case utils.IsDBusServiceActivatable(cupsPkHelperBus):
cupsPkStatus, cupsPkMsg = statusOK, "Available"
default:
cupsPkStatus, cupsPkMsg = statusWarn, "Not available (install cups-pk-helper)"
}
results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL})
results = append(results, checkI2CAvailability()) results = append(results, checkI2CAvailability())
results = append(results, checkImageFormatPlugins()...)
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"} terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 { if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
@@ -929,3 +1057,36 @@ func printSummary(results []checkResult, qsMissingFeatures bool) {
} }
fmt.Println() fmt.Println()
} }
func formatResultsPlain(results []checkResult) string {
var sb strings.Builder
sb.WriteString("## DMS Doctor Report\n\n")
currentCategory := category(-1)
for _, r := range results {
if r.category != currentCategory {
if currentCategory != -1 {
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
currentCategory = r.category
}
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
if doctorVerbose && r.details != "" {
sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
}
}
var ds DoctorStatus
for _, r := range results {
ds.Add(r)
}
sb.WriteString("\n---\n")
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
return sb.String()
}
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
)
var dlOutput string
var dlUserAgent string
var dlTimeout int
var dlIPv4Only bool
var dlCmd = &cobra.Command{
Use: "dl <url>",
Short: "Download a URL to stdout or file",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := runDownload(args[0]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}
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().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)
}
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)
}
}
client := &http.Client{Transport: transport}
resp, err := client.Do(req)
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)
}
}
f, err := os.Create(dlOutput)
if err != nil {
return fmt.Errorf("create failed: %w", err)
}
defer f.Close()
if _, err := io.Copy(f, resp.Body); err != nil {
os.Remove(dlOutput)
return fmt.Errorf("write failed: %w", err)
}
fmt.Println(dlOutput)
return nil
}
+86 -27
View File
@@ -119,7 +119,7 @@ func installGreeter() error {
} }
fmt.Println("\nSynchronizing DMS configurations...") fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil { if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, ""); err != nil {
return err return err
} }
@@ -147,12 +147,30 @@ func syncGreeter() error {
} }
fmt.Printf("✓ Found DMS at: %s\n", dmsPath) fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
if !isGreeterEnabled() {
fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.")
fmt.Print("Would you like to enable it now? (Y/n): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response != "n" && response != "no" {
if err := enableGreeter(); err != nil {
return err
}
} else {
return fmt.Errorf("greeter must be enabled before syncing")
}
}
cacheDir := "/var/cache/dms-greeter" cacheDir := "/var/cache/dms-greeter"
if _, err := os.Stat(cacheDir); os.IsNotExist(err) { if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir) return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
} }
greeterGroupExists := checkGroupExists("greeter") greeterGroup := greeter.DetectGreeterGroup()
greeterGroupExists := utils.HasGroup(greeterGroup)
if greeterGroupExists { if greeterGroupExists {
currentUser, err := user.Current() currentUser, err := user.Current()
if err != nil { if err != nil {
@@ -165,36 +183,59 @@ func syncGreeter() error {
return fmt.Errorf("failed to check groups: %w", err) return fmt.Errorf("failed to check groups: %w", err)
} }
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter") inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
if !inGreeterGroup { if !inGreeterGroup {
fmt.Println("\n⚠ Warning: You are not in the greeter group.") fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
fmt.Print("Would you like to add your user to the greeter group? (y/N): ") fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
var response string var response string
fmt.Scanln(&response) fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response)) response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" { if response != "n" && response != "no" {
fmt.Println("\nAdding user to greeter group...") fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username) addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
addUserCmd.Stdout = os.Stdout addUserCmd.Stdout = os.Stdout
addUserCmd.Stderr = os.Stderr addUserCmd.Stderr = os.Stderr
if err := addUserCmd.Run(); err != nil { if err := addUserCmd.Run(); err != nil {
return fmt.Errorf("failed to add user to greeter group: %w", err) return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
} }
fmt.Println("✓ User added to greeter group") fmt.Printf("✓ User added to %s group\n", greeterGroup)
fmt.Println("⚠ You will need to log out and back in for the group change to take effect") fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
} else {
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
} }
} }
} }
compositor := detectConfiguredCompositor()
if compositor == "" {
compositors := greeter.DetectCompositors()
switch len(compositors) {
case 0:
return fmt.Errorf("no supported compositors found")
case 1:
compositor = compositors[0]
fmt.Printf("✓ Using compositor: %s\n", compositor)
default:
var err error
compositor, err = promptCompositorChoice(compositors)
if err != nil {
return err
}
fmt.Printf("✓ Selected compositor: %s\n", compositor)
}
} else {
fmt.Printf("✓ Detected compositor from config: %s\n", compositor)
}
fmt.Println("\nSetting up permissions and ACLs...") fmt.Println("\nSetting up permissions and ACLs...")
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
return err return err
} }
fmt.Println("\nSynchronizing DMS configurations...") fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil { if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, ""); err != nil {
return err return err
} }
@@ -205,21 +246,6 @@ func syncGreeter() error {
return nil return nil
} }
func checkGroupExists(groupName string) bool {
data, err := os.ReadFile("/etc/group")
if err != nil {
return false
}
lines := strings.SplitSeq(string(data), "\n")
for line := range lines {
if strings.HasPrefix(line, groupName+":") {
return true
}
}
return false
}
func disableDisplayManager(dmName string) (bool, error) { func disableDisplayManager(dmName string) (bool, error) {
state, err := getSystemdServiceState(dmName) state, err := getSystemdServiceState(dmName)
if err != nil { if err != nil {
@@ -351,7 +377,7 @@ func ensureGraphicalTarget() error {
func handleConflictingDisplayManagers() error { func handleConflictingDisplayManagers() error {
fmt.Println("\n=== Checking for Conflicting Display Managers ===") fmt.Println("\n=== Checking for Conflicting Display Managers ===")
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"} conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
disabledAny := false disabledAny := false
var errors []string var errors []string
@@ -552,6 +578,39 @@ func enableGreeter() error {
return nil return nil
} }
func isGreeterEnabled() bool {
data, err := os.ReadFile("/etc/greetd/config.toml")
if err != nil {
return false
}
return strings.Contains(string(data), "dms-greeter")
}
func detectConfiguredCompositor() string {
data, err := os.ReadFile("/etc/greetd/config.toml")
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "command") || !strings.Contains(trimmed, "dms-greeter") {
continue
}
switch {
case strings.Contains(trimmed, "--command niri"):
return "niri"
case strings.Contains(trimmed, "--command hyprland"):
return "hyprland"
case strings.Contains(trimmed, "--command sway"):
return "sway"
}
}
return ""
}
func promptCompositorChoice(compositors []string) (string, error) { func promptCompositorChoice(compositors []string) (string, error) {
fmt.Println("\nMultiple compositors detected:") fmt.Println("\nMultiple compositors detected:")
for i, comp := range compositors { for i, comp := range compositors {
+26 -8
View File
@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
@@ -63,6 +64,7 @@ func init() {
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked") keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds") keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat") keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
keybindsSetCmd.Flags().Bool("no-inhibiting", false, "Keep bind active when shortcuts are inhibited (allow-inhibiting=false)")
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)") keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)") keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
@@ -81,24 +83,35 @@ func init() {
func initializeProviders() { func initializeProviders() {
registry := keybinds.GetDefaultRegistry() registry := keybinds.GetDefaultRegistry()
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr") hyprlandProvider := providers.NewHyprlandProvider("")
if err := registry.Register(hyprlandProvider); err != nil { if err := registry.Register(hyprlandProvider); err != nil {
log.Warnf("Failed to register Hyprland provider: %v", err) log.Warnf("Failed to register Hyprland provider: %v", err)
} }
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango") mangowcProvider := providers.NewMangoWCProvider("")
if err := registry.Register(mangowcProvider); err != nil { if err := registry.Register(mangowcProvider); err != nil {
log.Warnf("Failed to register MangoWC provider: %v", err) log.Warnf("Failed to register MangoWC provider: %v", err)
} }
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll") configDir, _ := os.UserConfigDir()
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err) if configDir != "" {
scrollProvider := providers.NewSwayProvider(filepath.Join(configDir, "scroll"))
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
}
} }
swayProvider := providers.NewSwayProvider("$HOME/.config/sway") miracleProvider := providers.NewMiracleProvider("")
if err := registry.Register(swayProvider); err != nil { if err := registry.Register(miracleProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err) log.Warnf("Failed to register Miracle WM provider: %v", err)
}
if configDir != "" {
swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "sway"))
if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err)
}
} }
niriProvider := providers.NewNiriProvider("") niriProvider := providers.NewNiriProvider("")
@@ -143,6 +156,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
return providers.NewSwayProvider(path) return providers.NewSwayProvider(path)
case "scroll": case "scroll":
return providers.NewSwayProvider(path) return providers.NewSwayProvider(path)
case "miracle":
return providers.NewMiracleProvider(path)
case "niri": case "niri":
return providers.NewNiriProvider(path) return providers.NewNiriProvider(path)
default: default:
@@ -212,6 +227,9 @@ func runKeybindsSet(cmd *cobra.Command, args []string) {
if v, _ := cmd.Flags().GetBool("no-repeat"); v { if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false options["repeat"] = false
} }
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
options["allow-inhibiting"] = false
}
if v, _ := cmd.Flags().GetString("flags"); v != "" { if v, _ := cmd.Flags().GetString("flags"); v != "" {
options["flags"] = v options["flags"] = v
} }
+15 -13
View File
@@ -13,16 +13,16 @@ import (
) )
var ( var (
ssOutputName string ssOutputName string
ssIncludeCursor bool ssCursor string
ssFormat string ssFormat string
ssQuality int ssQuality int
ssOutputDir string ssOutputDir string
ssFilename string ssFilename string
ssNoClipboard bool ssNoClipboard bool
ssNoFile bool ssNoFile bool
ssNoNotify bool ssNoNotify bool
ssStdout bool ssStdout bool
) )
var screenshotCmd = &cobra.Command{ var screenshotCmd = &cobra.Command{
@@ -52,7 +52,7 @@ Examples:
dms screenshot last # Last region (pre-selected) dms screenshot last # Last region (pre-selected)
dms screenshot --no-clipboard # Save file only dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only dms screenshot --no-file # Clipboard only
dms screenshot --cursor # Include cursor dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`, dms screenshot -f jpg -q 85 # JPEG with quality 85`,
} }
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
func init() { func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode") screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot") 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().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)") screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory") screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
@@ -136,7 +136,9 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig() config := screenshot.DefaultConfig()
config.Mode = mode config.Mode = mode
config.OutputName = ssOutputName config.OutputName = ssOutputName
config.IncludeCursor = ssIncludeCursor if strings.EqualFold(ssCursor, "on") {
config.Cursor = screenshot.CursorOn
}
config.Clipboard = !ssNoClipboard config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify config.Notify = !ssNoNotify
+239
View File
@@ -9,7 +9,9 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -24,6 +26,243 @@ var setupCmd = &cobra.Command{
}, },
} }
var setupBindsCmd = &cobra.Command{
Use: "binds",
Short: "Deploy default keybinds config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("binds"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupLayoutCmd = &cobra.Command{
Use: "layout",
Short: "Deploy default layout config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("layout"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupColorsCmd = &cobra.Command{
Use: "colors",
Short: "Deploy default colors config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("colors"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupAlttabCmd = &cobra.Command{
Use: "alttab",
Short: "Deploy default alt-tab config (niri only)",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("alttab"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupOutputsCmd = &cobra.Command{
Use: "outputs",
Short: "Deploy default outputs config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("outputs"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupCursorCmd = &cobra.Command{
Use: "cursor",
Short: "Deploy default cursor config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("cursor"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
var setupWindowrulesCmd = &cobra.Command{
Use: "windowrules",
Short: "Deploy default window rules config",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetupDmsConfig("windowrules"); err != nil {
log.Fatalf("Error: %v", err)
}
},
}
type dmsConfigSpec struct {
niriFile string
hyprFile string
niriContent func(terminal string) string
hyprContent func(terminal string) string
}
var dmsConfigSpecs = map[string]dmsConfigSpec{
"binds": {
niriFile: "binds.kdl",
hyprFile: "binds.conf",
niriContent: func(t string) string {
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
},
hyprContent: func(t string) string {
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
},
},
"layout": {
niriFile: "layout.kdl",
hyprFile: "layout.conf",
niriContent: func(_ string) string { return config.NiriLayoutConfig },
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
},
"colors": {
niriFile: "colors.kdl",
hyprFile: "colors.conf",
niriContent: func(_ string) string { return config.NiriColorsConfig },
hyprContent: func(_ string) string { return config.HyprColorsConfig },
},
"alttab": {
niriFile: "alttab.kdl",
niriContent: func(_ string) string { return config.NiriAlttabConfig },
},
"outputs": {
niriFile: "outputs.kdl",
hyprFile: "outputs.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
},
"cursor": {
niriFile: "cursor.kdl",
hyprFile: "cursor.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
},
"windowrules": {
niriFile: "windowrules.kdl",
hyprFile: "windowrules.conf",
niriContent: func(_ string) string { return "" },
hyprContent: func(_ string) string { return "" },
},
}
func detectTerminal() (string, error) {
terminals := []string{"ghostty", "foot", "kitty", "alacritty"}
var found []string
for _, t := range terminals {
if utils.CommandExists(t) {
found = append(found, t)
}
}
switch len(found) {
case 0:
return "ghostty", nil
case 1:
return found[0], nil
}
fmt.Println("Multiple terminals detected:")
for i, t := range found {
fmt.Printf("%d) %s\n", i+1, t)
}
fmt.Printf("\nChoice (1-%d): ", len(found))
var response string
fmt.Scanln(&response)
response = strings.TrimSpace(response)
choice := 0
fmt.Sscanf(response, "%d", &choice)
if choice < 1 || choice > len(found) {
return "", fmt.Errorf("invalid choice")
}
return found[choice-1], nil
}
func detectCompositorForSetup() (string, error) {
compositors := greeter.DetectCompositors()
switch len(compositors) {
case 0:
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
case 1:
return strings.ToLower(compositors[0]), nil
}
selected, err := greeter.PromptCompositorChoice(compositors)
if err != nil {
return "", err
}
return strings.ToLower(selected), nil
}
func runSetupDmsConfig(name string) error {
spec, ok := dmsConfigSpecs[name]
if !ok {
return fmt.Errorf("unknown config: %s", name)
}
compositor, err := detectCompositorForSetup()
if err != nil {
return err
}
var filename string
var contentFn func(string) string
switch compositor {
case "niri":
filename = spec.niriFile
contentFn = spec.niriContent
case "hyprland":
filename = spec.hyprFile
contentFn = spec.hyprContent
default:
return fmt.Errorf("unsupported compositor: %s", compositor)
}
if filename == "" {
return fmt.Errorf("%s is not supported for %s", name, compositor)
}
var dmsDir string
switch compositor {
case "niri":
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "niri", "dms")
case "hyprland":
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "hypr", "dms")
}
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
return fmt.Errorf("failed to create dms directory: %w", err)
}
path := filepath.Join(dmsDir, filename)
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
return fmt.Errorf("%s already exists and is not empty: %s", name, path)
}
terminal := "ghostty"
if contentFn != nil && name == "binds" {
terminal, err = detectTerminal()
if err != nil {
return err
}
}
content := contentFn(terminal)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", filename, err)
}
fmt.Printf("Deployed %s to %s\n", name, path)
return nil
}
func runSetup() error { func runSetup() error {
fmt.Println("=== DMS Configuration Setup ===") fmt.Println("=== DMS Configuration Setup ===")
+3
View File
@@ -19,6 +19,9 @@ func init() {
// Add subcommands to greeter // Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to update // Add subcommands to update
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
+3
View File
@@ -20,6 +20,9 @@ func init() {
// Add subcommands to greeter // Add subcommands to greeter
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd) greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to plugins // Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
+2 -2
View File
@@ -210,7 +210,7 @@ func runShellInteractive(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
} }
if os.Getenv("QT_QPA_PLATFORM") == "" { if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
} }
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@@ -450,7 +450,7 @@ func runShellDaemon(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
} }
if os.Getenv("QT_QPA_PLATFORM") == "" { if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
} }
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0) devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
+21 -18
View File
@@ -1,11 +1,11 @@
module github.com/AvengeMedia/DankMaterialShell/core module github.com/AvengeMedia/DankMaterialShell/core
go 1.24.6 go 1.25.0
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.23.1 github.com/alecthomas/chroma/v2 v2.23.1
github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/log v0.4.2
@@ -19,44 +19,43 @@ require (
github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
golang.org/x/image v0.35.0 golang.org/x/image v0.36.0
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.8.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.50.0 // indirect
) )
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.4 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9 github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@@ -70,7 +69,11 @@ require (
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.40.0 golang.org/x/sys v0.41.0
golang.org/x/text v0.33.0 golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1
) )
// v0.0.1 tag is missing a LICENSE file; master has it.
// See: https://github.com/mattn/go-localereader/issues/2
replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75
+34 -36
View File
@@ -20,30 +20,28 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= 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/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 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= 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= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.8.0 h1:/z8v+H+4XLluJKS7rAc7uHZTalT5Z+1430ld3lePSRI= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.8.0/go.mod h1:UpOXiIKep+TohQYwvAAM/VDU8v3Z5rnWTxiwueR0XvQ= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -66,12 +64,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY= github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc= github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9 h1:VzdR70t+SMjYnBgnbtNpq4ElZAAovLPMG+GFX8OBRtM= github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9/go.mod h1:EWlxLBkiFCzXNCadvt05fT9PCAE2sUedgDsvUUIo18s= github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -88,8 +86,8 @@ github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvE
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk= github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -103,8 +101,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -152,24 +150,24 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.m
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+128 -2
View File
@@ -13,8 +13,9 @@ import (
) )
type ClipboardChange struct { type ClipboardChange struct {
Data []byte Data []byte
MimeType string MimeType string
MimeTypes []string
} }
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error { func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
@@ -141,6 +142,131 @@ func Watch(ctx context.Context, callback func(data []byte, mimeType string)) err
} }
} }
func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
wlCtx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
if e.Id == nil {
return
}
offerMimeTypes[e.Id] = nil
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
})
})
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
if e.Id == nil {
return
}
mimes := offerMimeTypes[e.Id]
selectedMime := selectPreferredMimeType(mimes)
if selectedMime == "" {
return
}
mimesCopy := make([]string, len(mimes))
copy(mimesCopy, mimes)
r, w, err := os.Pipe()
if err != nil {
return
}
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
w.Close()
r.Close()
return
}
w.Close()
go func() {
defer r.Close()
data, err := io.ReadAll(r)
if err != nil || len(data) == 0 {
return
}
callback(data, selectedMime, mimesCopy)
}()
})
display.Roundtrip()
display.Roundtrip()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
return fmt.Errorf("set read deadline: %w", err)
}
if err := wlCtx.Dispatch(); err != nil {
if isTimeoutError(err) {
continue
}
return fmt.Errorf("dispatch: %w", err)
}
}
}
}
func isTimeoutError(err error) bool { func isTimeoutError(err error) bool {
if err == nil { if err == nil {
return false return false
+3 -3
View File
@@ -644,7 +644,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") { if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true startupSectionFound = true
result = append(result, "exec-once = dms run") result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland") result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto") result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3") result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3") result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
@@ -659,7 +659,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
if strings.Contains(line, "STARTUP APPS") { if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{ insertLines := []string{
"exec-once = dms run", "exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland", "env = QT_QPA_PLATFORM,wayland;xcb",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto", "env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3", "env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3", "env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
@@ -677,7 +677,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string { func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment { envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri" XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland" QT_QPA_PLATFORM "wayland;xcb"
ELECTRON_OZONE_PLATFORM_HINT "auto" ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3" QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3" QT_QPA_PLATFORMTHEME_QT6 "gtk3"
@@ -27,6 +27,8 @@ bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
bindl = , XF86AudioNext, exec, dms ipc call mpris next bindl = , XF86AudioNext, exec, dms ipc call mpris next
bindel = CTRL, XF86AudioRaiseVolume, exec, dms ipc call mpris increment 3
bindel = CTRL, XF86AudioLowerVolume, exec, dms ipc call mpris decrement 3
# === Brightness Controls === # === Brightness Controls ===
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 "" bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
+4 -1
View File
@@ -98,9 +98,11 @@ windowrule = float on, match:class ^(gnome-calculator)$
windowrule = float on, match:class ^(galculator)$ windowrule = float on, match:class ^(galculator)$
windowrule = float on, match:class ^(blueman-manager)$ windowrule = float on, match:class ^(blueman-manager)$
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$ windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
windowrule = float on, match:class ^(steam)$
windowrule = float on, match:class ^(xdg-desktop-portal)$ windowrule = float on, match:class ^(xdg-desktop-portal)$
windowrule = noinitialfocus on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$ windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
windowrule = float on, match:class ^(zoom)$ windowrule = float on, match:class ^(zoom)$
@@ -109,6 +111,7 @@ windowrule = float on, match:class ^(zoom)$
# windowrule = float on, match:class ^(org.quickshell)$ # windowrule = float on, match:class ^(org.quickshell)$
layerrule = no_anim on, match:namespace ^(quickshell)$ layerrule = no_anim on, match:namespace ^(quickshell)$
layerrule = no_anim on, match:namespace ^dms:.*
source = ./dms/colors.conf source = ./dms/colors.conf
source = ./dms/outputs.conf source = ./dms/outputs.conf
@@ -60,6 +60,12 @@ binds {
XF86AudioNext allow-when-locked=true { XF86AudioNext allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "next"; spawn "dms" "ipc" "call" "mpris" "next";
} }
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "increment" "3";
}
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
}
// === Brightness Controls === // === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true { XF86MonBrightnessUp allow-when-locked=true {
@@ -0,0 +1,17 @@
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
+5 -1
View File
@@ -228,10 +228,14 @@ window-rule {
match app-id=r#"^galculator$"# match app-id=r#"^galculator$"#
match app-id=r#"^blueman-manager$"# match app-id=r#"^blueman-manager$"#
match app-id=r#"^org\.gnome\.Nautilus$"# match app-id=r#"^org\.gnome\.Nautilus$"#
match app-id=r#"^steam$"#
match app-id=r#"^xdg-desktop-portal$"# match app-id=r#"^xdg-desktop-portal$"#
open-floating true open-floating true
} }
window-rule {
match app-id=r#"^steam$"# title=r#"^notificationtoasts_\d+_desktop$"#
default-floating-position x=10 y=10 relative-to="bottom-right"
open-focused false
}
window-rule { window-rule {
match app-id=r#"^org\.wezfurlong\.wezterm$"# match app-id=r#"^org\.wezfurlong\.wezterm$"#
match app-id="Alacritty" match app-id="Alacritty"
+3
View File
@@ -16,3 +16,6 @@ var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl //go:embed embedded/niri-binds.kdl
var NiriBindsConfig string var NiriBindsConfig string
//go:embed embedded/niri-greeter.kdl
var NiriGreeterConfig string
+1 -1
View File
@@ -430,7 +430,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
// Add repository // Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL) repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
+417 -17
View File
@@ -8,10 +8,13 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/sblinch/kdl-go"
"github.com/sblinch/kdl-go/document"
) )
// DetectDMSPath checks for DMS installation following XDG Base Directory specification // DetectDMSPath checks for DMS installation following XDG Base Directory specification
@@ -19,6 +22,21 @@ func DetectDMSPath() (string, error) {
return config.LocateDMSConfig() return config.LocateDMSConfig()
} }
func DetectGreeterGroup() string {
data, err := os.ReadFile("/etc/group")
if err != nil {
fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/group, defaulting to greeter")
return "greeter"
}
if group, found := utils.FindGroupData(string(data), "greeter", "greetd", "_greeter"); found {
return group
}
fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter group found in /etc/group, defaulting to greeter")
return "greeter"
}
// DetectCompositors checks which compositors are installed // DetectCompositors checks which compositors are installed
func DetectCompositors() []string { func DetectCompositors() []string {
var compositors []string var compositors []string
@@ -191,14 +209,17 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
return fmt.Errorf("failed to create cache directory: %w", err) return fmt.Errorf("failed to create cache directory: %w", err)
} }
if err := runSudoCmd(sudoPassword, "chown", "greeter:greeter", cacheDir); err != nil { group := DetectGreeterGroup()
owner := fmt.Sprintf("%s:%s", group, group)
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
return fmt.Errorf("failed to set cache directory owner: %w", err) return fmt.Errorf("failed to set cache directory owner: %w", err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "750", cacheDir); err != nil { if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil {
return fmt.Errorf("failed to set cache directory permissions: %w", err) return fmt.Errorf("failed to set cache directory permissions: %w", err)
} }
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: greeter:greeter, permissions: 750)", cacheDir)) logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, permissions: 755)", cacheDir, owner))
return nil return nil
} }
@@ -231,6 +252,8 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"}, {filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
} }
owner := DetectGreeterGroup()
logFunc("\nSetting up parent directory ACLs for greeter user access...") logFunc("\nSetting up parent directory ACLs for greeter user access...")
for _, dir := range parentDirs { for _, dir := range parentDirs {
@@ -242,9 +265,9 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
} }
// Set ACL to allow greeter user read+execute permission (for session discovery) // Set ACL to allow greeter user read+execute permission (for session discovery)
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:rx", dir.path); err != nil { if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path)) logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path))
continue continue
} }
@@ -268,17 +291,19 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
return fmt.Errorf("failed to determine current user") return fmt.Errorf("failed to determine current user")
} }
group := DetectGreeterGroup()
// Check if user is already in greeter group // Check if user is already in greeter group
groupsCmd := exec.Command("groups", currentUser) groupsCmd := exec.Command("groups", currentUser)
groupsOutput, err := groupsCmd.Output() groupsOutput, err := groupsCmd.Output()
if err == nil && strings.Contains(string(groupsOutput), "greeter") { if err == nil && strings.Contains(string(groupsOutput), group) {
logFunc(fmt.Sprintf("✓ %s is already in greeter group", currentUser)) logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
} else { } else {
// Add current user to greeter group for file access permissions // Add current user to greeter group for file access permissions
if err := runSudoCmd(sudoPassword, "usermod", "-aG", "greeter", currentUser); err != nil { if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
return fmt.Errorf("failed to add %s to greeter group: %w", currentUser, err) return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
} }
logFunc(fmt.Sprintf("✓ Added %s to greeter group (logout/login required for changes to take effect)", currentUser)) logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
} }
configDirs := []struct { configDirs := []struct {
@@ -301,7 +326,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
} }
} }
if err := runSudoCmd(sudoPassword, "chgrp", "-R", "greeter", dir.path); err != nil { if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
continue continue
} }
@@ -322,7 +347,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
return nil return nil
} }
func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) error { func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err) return fmt.Errorf("failed to get user home directory: %w", err)
@@ -378,9 +403,351 @@ func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) e
logFunc(fmt.Sprintf("✓ Synced %s", link.desc)) logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
} }
if strings.ToLower(compositor) != "niri" {
return nil
}
if err := syncNiriGreeterConfig(logFunc, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to sync niri greeter config: %v", err))
}
return nil return nil
} }
type niriGreeterSync struct {
processed map[string]bool
nodes []*document.Node
inputCount int
outputCount int
cursorCount int
debugCount int
cursorNode *document.Node
}
func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
configDir, err := os.UserConfigDir()
if err != nil {
return fmt.Errorf("failed to resolve user config directory: %w", err)
}
configPath := filepath.Join(configDir, "niri", "config.kdl")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
logFunc(" Niri config not found; skipping greeter niri sync")
return nil
} else if err != nil {
return fmt.Errorf("failed to stat niri config: %w", err)
}
extractor := &niriGreeterSync{
processed: make(map[string]bool),
}
if err := extractor.processFile(configPath); err != nil {
return err
}
if len(extractor.nodes) == 0 {
logFunc(" No niri input/output sections found; skipping greeter niri sync")
return nil
}
content := extractor.render()
if strings.TrimSpace(content) == "" {
logFunc(" No niri input/output content to sync; skipping greeter niri sync")
return nil
}
greeterDir := "/etc/greetd/niri"
greeterGroup := DetectGreeterGroup()
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
return fmt.Errorf("failed to create greetd niri directory: %w", err)
}
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
}
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
}
dmsTemp, err := os.CreateTemp("", "dms-greeter-niri-dms-*.kdl")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(dmsTemp.Name())
if _, err := dmsTemp.WriteString(content); err != nil {
_ = dmsTemp.Close()
return fmt.Errorf("failed to write temp niri config: %w", err)
}
if err := dmsTemp.Close(); err != nil {
return fmt.Errorf("failed to close temp niri config: %w", err)
}
dmsPath := filepath.Join(greeterDir, "dms.kdl")
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
}
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
}
mainContent := fmt.Sprintf("%s\ninclude \"%s\"\n", config.NiriGreeterConfig, dmsPath)
mainTemp, err := os.CreateTemp("", "dms-greeter-niri-main-*.kdl")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(mainTemp.Name())
if _, err := mainTemp.WriteString(mainContent); err != nil {
_ = mainTemp.Close()
return fmt.Errorf("failed to write temp niri main config: %w", err)
}
if err := mainTemp.Close(); err != nil {
return fmt.Errorf("failed to close temp niri main config: %w", err)
}
mainPath := filepath.Join(greeterDir, "config.kdl")
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
}
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
return fmt.Errorf("failed to install greetd niri main config: %w", err)
}
if err := ensureGreetdNiriConfig(logFunc, sudoPassword, mainPath); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to update greetd config for niri: %v", err))
}
logFunc(fmt.Sprintf("✓ Synced niri greeter config (%d input, %d output, %d cursor, %d debug) to %s", extractor.inputCount, extractor.outputCount, extractor.cursorCount, extractor.debugCount, dmsPath))
return nil
}
func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfigPath string) error {
configPath := "/etc/greetd/config.toml"
data, err := os.ReadFile(configPath)
if os.IsNotExist(err) {
logFunc(" greetd config not found; skipping niri config wiring")
return nil
}
if err != nil {
return fmt.Errorf("failed to read greetd config: %w", err)
}
lines := strings.Split(string(data), "\n")
updated := false
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "command") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
command := strings.Trim(strings.TrimSpace(parts[1]), "\"")
if !strings.Contains(command, "dms-greeter") {
continue
}
if !strings.Contains(command, "--command niri") {
continue
}
// Strip existing -C or --config and their arguments
command = stripConfigFlag(command)
newCommand := fmt.Sprintf("%s -C %s", command, niriConfigPath)
idx := strings.Index(line, "command")
leading := ""
if idx > 0 {
leading = line[:idx]
}
lines[i] = fmt.Sprintf("%scommand = \"%s\"", leading, newCommand)
updated = true
break
}
if !updated {
return nil
}
if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup greetd config: %w", err)
}
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
if err != nil {
return fmt.Errorf("failed to create temp greetd config: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(strings.Join(lines, "\n")); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write temp greetd config: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp greetd config: %w", err)
}
if err := runSudoCmd(sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
return fmt.Errorf("failed to update greetd config: %w", err)
}
logFunc(fmt.Sprintf("✓ Updated greetd config to use niri config %s", niriConfigPath))
return nil
}
func backupFileIfExists(sudoPassword string, path string, suffix string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil
} else if err != nil {
return err
}
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
return runSudoCmd(sudoPassword, "cp", "-p", path, backupPath)
}
func (s *niriGreeterSync) processFile(filePath string) error {
absPath, err := filepath.Abs(filePath)
if err != nil {
return fmt.Errorf("failed to resolve path %s: %w", filePath, err)
}
if s.processed[absPath] {
return nil
}
s.processed[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", absPath, err)
}
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
}
baseDir := filepath.Dir(absPath)
for _, node := range doc.Nodes {
name := node.Name.String()
switch name {
case "include":
if err := s.handleInclude(node, baseDir); err != nil {
return err
}
case "input":
s.nodes = append(s.nodes, node)
s.inputCount++
case "output":
s.nodes = append(s.nodes, node)
s.outputCount++
case "cursor":
if s.cursorNode == nil {
s.cursorNode = node
s.cursorNode.Children = dedupeCursorChildren(s.cursorNode.Children)
s.nodes = append(s.nodes, node)
s.cursorCount++
} else if len(node.Children) > 0 {
s.cursorNode.Children = mergeCursorChildren(s.cursorNode.Children, node.Children)
}
case "debug":
s.nodes = append(s.nodes, node)
s.debugCount++
}
}
return nil
}
func mergeCursorChildren(existing []*document.Node, incoming []*document.Node) []*document.Node {
if len(incoming) == 0 {
return existing
}
indexByName := make(map[string]int, len(existing))
for i, child := range existing {
indexByName[child.Name.String()] = i
}
for _, child := range incoming {
name := child.Name.String()
if idx, ok := indexByName[name]; ok {
existing[idx] = child
continue
}
indexByName[name] = len(existing)
existing = append(existing, child)
}
return existing
}
func dedupeCursorChildren(children []*document.Node) []*document.Node {
if len(children) == 0 {
return children
}
var result []*document.Node
indexByName := make(map[string]int, len(children))
for _, child := range children {
name := child.Name.String()
if idx, ok := indexByName[name]; ok {
result[idx] = child
continue
}
indexByName[name] = len(result)
result = append(result, child)
}
return result
}
func (s *niriGreeterSync) handleInclude(node *document.Node, baseDir string) error {
if len(node.Arguments) == 0 {
return nil
}
includePath := strings.Trim(node.Arguments[0].String(), "\"")
if includePath == "" {
return nil
}
fullPath := includePath
if !filepath.IsAbs(includePath) {
fullPath = filepath.Join(baseDir, includePath)
}
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("failed to stat include %s: %w", fullPath, err)
}
return s.processFile(fullPath)
}
func (s *niriGreeterSync) render() string {
if len(s.nodes) == 0 {
return ""
}
var builder strings.Builder
for _, node := range s.nodes {
_, _ = node.WriteToOptions(&builder, document.NodeWriteOptions{
LeadingTrailingSpace: true,
NameAndType: true,
Depth: 0,
Indent: []byte(" "),
IgnoreFlags: false,
})
builder.WriteString("\n")
}
return builder.String()
}
func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
configPath := "/etc/greetd/config.toml" configPath := "/etc/greetd/config.toml"
@@ -392,17 +759,19 @@ func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassw
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath)) logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
} }
greeterUser := DetectGreeterGroup()
var configContent string var configContent string
if data, err := os.ReadFile(configPath); err == nil { if data, err := os.ReadFile(configPath); err == nil {
configContent = string(data) configContent = string(data)
} else { } else {
configContent = `[terminal] configContent = fmt.Sprintf(`[terminal]
vt = 1 vt = 1
[default_session] [default_session]
user = "greeter" user = "%s"
` `, greeterUser)
} }
lines := strings.Split(configContent, "\n") lines := strings.Split(configContent, "\n")
@@ -411,7 +780,7 @@ user = "greeter"
trimmed := strings.TrimSpace(line) trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") { if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
newLines = append(newLines, `user = "greeter"`) newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser))
} else { } else {
newLines = append(newLines, line) newLines = append(newLines, line)
} }
@@ -463,10 +832,41 @@ user = "greeter"
return fmt.Errorf("failed to move config to /etc/greetd: %w", err) return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
} }
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: greeter, command: %s --command %s -p %s)", wrapperCmd, compositorLower, dmsPath)) logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s --command %s -p %s)", greeterUser, wrapperCmd, compositorLower, dmsPath))
return nil return nil
} }
func stripConfigFlag(command string) string {
for _, flag := range []string{" -C ", " --config "} {
idx := strings.Index(command, flag)
if idx == -1 {
continue
}
before := command[:idx]
after := command[idx+len(flag):]
switch {
case strings.HasPrefix(after, `"`):
if end := strings.Index(after[1:], `"`); end != -1 {
after = after[end+2:]
} else {
after = ""
}
default:
if space := strings.Index(after, " "); space != -1 {
after = after[space:]
} else {
after = ""
}
}
command = strings.TrimSpace(before + after)
}
return command
}
func runSudoCmd(sudoPassword string, command string, args ...string) error { func runSudoCmd(sudoPassword string, command string, args ...string) error {
var cmd *exec.Cmd var cmd *exec.Cmd
@@ -0,0 +1,95 @@
package providers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type MiracleProvider struct {
configPath string
}
func NewMiracleProvider(configPath string) *MiracleProvider {
if configPath == "" {
configDir, err := os.UserConfigDir()
if err == nil {
configPath = filepath.Join(configDir, "miracle-wm")
}
}
return &MiracleProvider{configPath: configPath}
}
func (m *MiracleProvider) Name() string {
return "miracle"
}
func (m *MiracleProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
config, err := ParseMiracleConfig(m.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse miracle-wm config: %w", err)
}
bindings := MiracleConfigToBindings(config)
categorizedBinds := make(map[string][]keybinds.Keybind)
for _, kb := range bindings {
category := m.categorizeAction(kb.Action)
bind := keybinds.Keybind{
Key: m.formatKey(kb),
Description: kb.Comment,
Action: kb.Action,
}
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
return &keybinds.CheatSheet{
Title: "Miracle WM Keybinds",
Provider: m.Name(),
Binds: categorizedBinds,
}, nil
}
func (m *MiracleProvider) GetOverridePath() string {
expanded, err := utils.ExpandPath(m.configPath)
if err != nil {
return filepath.Join(m.configPath, "config.yaml")
}
return filepath.Join(expanded, "config.yaml")
}
func (m *MiracleProvider) formatKey(kb MiracleKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (m *MiracleProvider) categorizeAction(action string) string {
switch {
case strings.HasPrefix(action, "select_workspace_") || strings.HasPrefix(action, "move_to_workspace_"):
return "Workspace"
case strings.Contains(action, "select_") || strings.Contains(action, "move_"):
return "Window"
case action == "toggle_resize" || strings.HasPrefix(action, "resize_"):
return "Window"
case action == "fullscreen" || action == "toggle_floating" || action == "quit_active_window" || action == "toggle_pinned_to_workspace":
return "Window"
case action == "toggle_tabbing" || action == "toggle_stacking" || action == "request_vertical" || action == "request_horizontal":
return "Layout"
case action == "quit_compositor":
return "System"
case action == "terminal":
return "Execute"
case strings.HasPrefix(action, "magnifier_"):
return "Accessibility"
case strings.HasPrefix(action, "dms ") || strings.Contains(action, "dms ipc"):
return "Execute"
default:
return "Execute"
}
}
@@ -0,0 +1,320 @@
package providers
import (
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"gopkg.in/yaml.v3"
)
type MiracleConfig struct {
Terminal string `yaml:"terminal"`
ActionKey string `yaml:"action_key"`
DefaultActionOverrides []MiracleActionOverride `yaml:"default_action_overrides"`
CustomActions []MiracleCustomAction `yaml:"custom_actions"`
}
type MiracleActionOverride struct {
Name string `yaml:"name"`
Action string `yaml:"action"`
Modifiers []string `yaml:"modifiers"`
Key string `yaml:"key"`
}
type MiracleCustomAction struct {
Command string `yaml:"command"`
Action string `yaml:"action"`
Modifiers []string `yaml:"modifiers"`
Key string `yaml:"key"`
}
type MiracleKeyBinding struct {
Mods []string
Key string
Action string
Comment string
}
var miracleDefaultBinds = []MiracleKeyBinding{
{Mods: []string{"Super"}, Key: "Return", Action: "terminal", Comment: "Open terminal"},
{Mods: []string{"Super"}, Key: "v", Action: "request_vertical", Comment: "Layout windows vertically"},
{Mods: []string{"Super"}, Key: "h", Action: "request_horizontal", Comment: "Layout windows horizontally"},
{Mods: []string{"Super"}, Key: "Up", Action: "select_up", Comment: "Select window above"},
{Mods: []string{"Super"}, Key: "Down", Action: "select_down", Comment: "Select window below"},
{Mods: []string{"Super"}, Key: "Left", Action: "select_left", Comment: "Select window left"},
{Mods: []string{"Super"}, Key: "Right", Action: "select_right", Comment: "Select window right"},
{Mods: []string{"Super", "Shift"}, Key: "Up", Action: "move_up", Comment: "Move window up"},
{Mods: []string{"Super", "Shift"}, Key: "Down", Action: "move_down", Comment: "Move window down"},
{Mods: []string{"Super", "Shift"}, Key: "Left", Action: "move_left", Comment: "Move window left"},
{Mods: []string{"Super", "Shift"}, Key: "Right", Action: "move_right", Comment: "Move window right"},
{Mods: []string{"Super"}, Key: "r", Action: "toggle_resize", Comment: "Toggle resize mode"},
{Mods: []string{"Super"}, Key: "f", Action: "fullscreen", Comment: "Toggle fullscreen"},
{Mods: []string{"Super", "Shift"}, Key: "q", Action: "quit_active_window", Comment: "Close window"},
{Mods: []string{"Super", "Shift"}, Key: "e", Action: "quit_compositor", Comment: "Exit compositor"},
{Mods: []string{"Super"}, Key: "Space", Action: "toggle_floating", Comment: "Toggle floating"},
{Mods: []string{"Super", "Shift"}, Key: "p", Action: "toggle_pinned_to_workspace", Comment: "Toggle pinned to workspace"},
{Mods: []string{"Super"}, Key: "w", Action: "toggle_tabbing", Comment: "Toggle tabbing layout"},
{Mods: []string{"Super"}, Key: "s", Action: "toggle_stacking", Comment: "Toggle stacking layout"},
{Mods: []string{"Super"}, Key: "1", Action: "select_workspace_0", Comment: "Workspace 1"},
{Mods: []string{"Super"}, Key: "2", Action: "select_workspace_1", Comment: "Workspace 2"},
{Mods: []string{"Super"}, Key: "3", Action: "select_workspace_2", Comment: "Workspace 3"},
{Mods: []string{"Super"}, Key: "4", Action: "select_workspace_3", Comment: "Workspace 4"},
{Mods: []string{"Super"}, Key: "5", Action: "select_workspace_4", Comment: "Workspace 5"},
{Mods: []string{"Super"}, Key: "6", Action: "select_workspace_5", Comment: "Workspace 6"},
{Mods: []string{"Super"}, Key: "7", Action: "select_workspace_6", Comment: "Workspace 7"},
{Mods: []string{"Super"}, Key: "8", Action: "select_workspace_7", Comment: "Workspace 8"},
{Mods: []string{"Super"}, Key: "9", Action: "select_workspace_8", Comment: "Workspace 9"},
{Mods: []string{"Super"}, Key: "0", Action: "select_workspace_9", Comment: "Workspace 10"},
{Mods: []string{"Super", "Shift"}, Key: "1", Action: "move_to_workspace_0", Comment: "Move to workspace 1"},
{Mods: []string{"Super", "Shift"}, Key: "2", Action: "move_to_workspace_1", Comment: "Move to workspace 2"},
{Mods: []string{"Super", "Shift"}, Key: "3", Action: "move_to_workspace_2", Comment: "Move to workspace 3"},
{Mods: []string{"Super", "Shift"}, Key: "4", Action: "move_to_workspace_3", Comment: "Move to workspace 4"},
{Mods: []string{"Super", "Shift"}, Key: "5", Action: "move_to_workspace_4", Comment: "Move to workspace 5"},
{Mods: []string{"Super", "Shift"}, Key: "6", Action: "move_to_workspace_5", Comment: "Move to workspace 6"},
{Mods: []string{"Super", "Shift"}, Key: "7", Action: "move_to_workspace_6", Comment: "Move to workspace 7"},
{Mods: []string{"Super", "Shift"}, Key: "8", Action: "move_to_workspace_7", Comment: "Move to workspace 8"},
{Mods: []string{"Super", "Shift"}, Key: "9", Action: "move_to_workspace_8", Comment: "Move to workspace 9"},
{Mods: []string{"Super", "Shift"}, Key: "0", Action: "move_to_workspace_9", Comment: "Move to workspace 10"},
}
func ParseMiracleConfig(configPath string) (*MiracleConfig, error) {
expanded, err := utils.ExpandPath(configPath)
if err != nil {
return nil, err
}
info, err := os.Stat(expanded)
if err != nil {
return nil, err
}
var configFile string
if info.IsDir() {
configFile = filepath.Join(expanded, "config.yaml")
} else {
configFile = expanded
}
data, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
var config MiracleConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
if config.ActionKey == "" {
config.ActionKey = "meta"
}
return &config, nil
}
func resolveMiracleModifier(mod, actionKey string) string {
switch mod {
case "primary":
return resolveActionKey(actionKey)
case "alt", "alt_left", "alt_right":
return "Alt"
case "shift", "shift_left", "shift_right":
return "Shift"
case "ctrl", "ctrl_left", "ctrl_right":
return "Ctrl"
case "meta", "meta_left", "meta_right":
return "Super"
default:
return mod
}
}
func resolveActionKey(actionKey string) string {
switch actionKey {
case "meta":
return "Super"
case "alt":
return "Alt"
case "ctrl":
return "Ctrl"
default:
return "Super"
}
}
func miracleKeyCodeToName(keyCode string) string {
name := strings.TrimPrefix(keyCode, "KEY_")
name = strings.ToLower(name)
switch name {
case "enter":
return "Return"
case "space":
return "Space"
case "up":
return "Up"
case "down":
return "Down"
case "left":
return "Left"
case "right":
return "Right"
case "tab":
return "Tab"
case "escape", "esc":
return "Escape"
case "delete":
return "Delete"
case "backspace":
return "BackSpace"
case "home":
return "Home"
case "end":
return "End"
case "pageup":
return "Page_Up"
case "pagedown":
return "Page_Down"
case "print":
return "Print"
case "pause":
return "Pause"
case "volumeup":
return "XF86AudioRaiseVolume"
case "volumedown":
return "XF86AudioLowerVolume"
case "mute":
return "XF86AudioMute"
case "micmute":
return "XF86AudioMicMute"
case "brightnessup":
return "XF86MonBrightnessUp"
case "brightnessdown":
return "XF86MonBrightnessDown"
case "kbdillumup":
return "XF86KbdBrightnessUp"
case "kbdillumdown":
return "XF86KbdBrightnessDown"
case "comma":
return "comma"
case "minus":
return "minus"
case "equal":
return "equal"
}
if len(name) == 1 {
return name
}
return name
}
func MiracleConfigToBindings(config *MiracleConfig) []MiracleKeyBinding {
overridden := make(map[string]bool)
var bindings []MiracleKeyBinding
for _, override := range config.DefaultActionOverrides {
mods := make([]string, 0, len(override.Modifiers))
for _, mod := range override.Modifiers {
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
}
bindings = append(bindings, MiracleKeyBinding{
Mods: mods,
Key: miracleKeyCodeToName(override.Key),
Action: override.Name,
Comment: miracleActionDescription(override.Name),
})
overridden[override.Name] = true
}
for _, def := range miracleDefaultBinds {
if overridden[def.Action] {
continue
}
bindings = append(bindings, def)
}
for _, custom := range config.CustomActions {
mods := make([]string, 0, len(custom.Modifiers))
for _, mod := range custom.Modifiers {
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
}
bindings = append(bindings, MiracleKeyBinding{
Mods: mods,
Key: miracleKeyCodeToName(custom.Key),
Action: custom.Command,
Comment: custom.Command,
})
}
return bindings
}
func miracleActionDescription(action string) string {
switch action {
case "terminal":
return "Open terminal"
case "request_vertical":
return "Layout windows vertically"
case "request_horizontal":
return "Layout windows horizontally"
case "select_up":
return "Select window above"
case "select_down":
return "Select window below"
case "select_left":
return "Select window left"
case "select_right":
return "Select window right"
case "move_up":
return "Move window up"
case "move_down":
return "Move window down"
case "move_left":
return "Move window left"
case "move_right":
return "Move window right"
case "toggle_resize":
return "Toggle resize mode"
case "fullscreen":
return "Toggle fullscreen"
case "quit_active_window":
return "Close window"
case "quit_compositor":
return "Exit compositor"
case "toggle_floating":
return "Toggle floating"
case "toggle_pinned_to_workspace":
return "Toggle pinned to workspace"
case "toggle_tabbing":
return "Toggle tabbing layout"
case "toggle_stacking":
return "Toggle stacking layout"
case "magnifier_on":
return "Enable magnifier"
case "magnifier_off":
return "Disable magnifier"
case "magnifier_increase_size":
return "Increase magnifier area"
case "magnifier_decrease_size":
return "Decrease magnifier area"
case "magnifier_increase_scale":
return "Increase magnifier scale"
case "magnifier_decrease_scale":
return "Decrease magnifier scale"
}
if num, ok := strings.CutPrefix(action, "select_workspace_"); ok {
return "Workspace " + num
}
if num, ok := strings.CutPrefix(action, "move_to_workspace_"); ok {
return "Move to workspace " + num
}
return action
}
+23 -15
View File
@@ -118,6 +118,9 @@ func (n *NiriProvider) categorizeByAction(action string) string {
return "Overview" return "Overview"
case action == "quit" || case action == "quit" ||
action == "power-off-monitors" || action == "power-off-monitors" ||
action == "power-on-monitors" ||
action == "suspend" ||
action == "do-screen-transition" ||
action == "toggle-keyboard-shortcuts-inhibit" || action == "toggle-keyboard-shortcuts-inhibit" ||
strings.Contains(action, "dpms"): strings.Contains(action, "dpms"):
return "System" return "System"
@@ -151,13 +154,16 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
} }
bind := keybinds.Keybind{ bind := keybinds.Keybind{
Key: keyStr, Key: keyStr,
Description: kb.Description, Description: kb.Description,
Action: rawAction, Action: rawAction,
Subcategory: subcategory, Subcategory: subcategory,
Source: source, Source: source,
HideOnOverlay: kb.HideOnOverlay, HideOnOverlay: kb.HideOnOverlay,
CooldownMs: kb.CooldownMs, CooldownMs: kb.CooldownMs,
AllowWhenLocked: kb.AllowWhenLocked,
AllowInhibiting: kb.AllowInhibiting,
Repeat: kb.Repeat,
} }
if source == "dms" && conflicts != nil { if source == "dms" && conflicts != nil {
@@ -341,14 +347,10 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
} }
if actionNode.Properties != nil { if actionNode.Properties != nil {
if val, ok := actionNode.Properties.Get("focus"); ok { for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
parts = append(parts, "focus="+val.String()) if val, ok := actionNode.Properties.Get(propName); ok {
} parts = append(parts, propName+"="+val.String())
if val, ok := actionNode.Properties.Get("show-pointer"); ok { }
parts = append(parts, "show-pointer="+val.String())
}
if val, ok := actionNode.Properties.Get("write-to-disk"); ok {
parts = append(parts, "write-to-disk="+val.String())
} }
} }
@@ -372,6 +374,9 @@ func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
if val, ok := node.Properties.Get("allow-when-locked"); ok { if val, ok := node.Properties.Get("allow-when-locked"); ok {
opts["allow-when-locked"] = val.String() == "true" opts["allow-when-locked"] = val.String() == "true"
} }
if val, ok := node.Properties.Get("allow-inhibiting"); ok {
opts["allow-inhibiting"] = val.String() == "true"
}
return opts return opts
} }
@@ -405,6 +410,9 @@ func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
if v, ok := bind.Options["allow-when-locked"]; ok && v == true { if v, ok := bind.Options["allow-when-locked"]; ok && v == true {
node.AddProperty("allow-when-locked", true, "") node.AddProperty("allow-when-locked", true, "")
} }
if v, ok := bind.Options["allow-inhibiting"]; ok && v == false {
node.AddProperty("allow-inhibiting", false, "")
}
} }
if bind.Description != "" { if bind.Description != "" {
+40 -18
View File
@@ -12,14 +12,17 @@ import (
) )
type NiriKeyBinding struct { type NiriKeyBinding struct {
Mods []string Mods []string
Key string Key string
Action string Action string
Args []string Args []string
Description string Description string
HideOnOverlay bool HideOnOverlay bool
CooldownMs int CooldownMs int
Source string AllowWhenLocked bool
AllowInhibiting *bool
Repeat *bool
Source string
} }
type NiriSection struct { type NiriSection struct {
@@ -269,8 +272,10 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
args = append(args, arg.ValueString()) args = append(args, arg.ValueString())
} }
if actionNode.Properties != nil { if actionNode.Properties != nil {
if val, ok := actionNode.Properties.Get("focus"); ok { for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
args = append(args, "focus="+val.String()) if val, ok := actionNode.Properties.Get(propName); ok {
args = append(args, propName+"="+val.String())
}
} }
} }
} }
@@ -278,6 +283,9 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
var description string var description string
var hideOnOverlay bool var hideOnOverlay bool
var cooldownMs int var cooldownMs int
var allowWhenLocked bool
var allowInhibiting *bool
var repeat *bool
if node.Properties != nil { if node.Properties != nil {
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok { if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
switch val.ValueString() { switch val.ValueString() {
@@ -290,17 +298,31 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
if val, ok := node.Properties.Get("cooldown-ms"); ok { if val, ok := node.Properties.Get("cooldown-ms"); ok {
cooldownMs, _ = strconv.Atoi(val.String()) cooldownMs, _ = strconv.Atoi(val.String())
} }
if val, ok := node.Properties.Get("allow-when-locked"); ok {
allowWhenLocked = val.String() == "true"
}
if val, ok := node.Properties.Get("allow-inhibiting"); ok {
v := val.String() == "true"
allowInhibiting = &v
}
if val, ok := node.Properties.Get("repeat"); ok {
v := val.String() == "true"
repeat = &v
}
} }
return &NiriKeyBinding{ return &NiriKeyBinding{
Mods: mods, Mods: mods,
Key: key, Key: key,
Action: action, Action: action,
Args: args, Args: args,
Description: description, Description: description,
HideOnOverlay: hideOnOverlay, HideOnOverlay: hideOnOverlay,
CooldownMs: cooldownMs, CooldownMs: cooldownMs,
Source: p.currentSource, AllowWhenLocked: allowWhenLocked,
AllowInhibiting: allowInhibiting,
Repeat: repeat,
Source: p.currentSource,
} }
} }
+16 -8
View File
@@ -3,6 +3,7 @@ package providers
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -18,14 +19,21 @@ func NewSwayProvider(configPath string) *SwayProvider {
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK") _, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
if configPath == "" { if configPath == "" {
configDir, err := os.UserConfigDir()
if err != nil {
configDir = ""
}
if scrollEnvSet { if scrollEnvSet {
configPath = "$HOME/.config/scroll" if configDir != "" {
configPath = filepath.Join(configDir, "scroll")
}
isScroll = true isScroll = true
} else { } else {
configPath = "$HOME/.config/sway" if configDir != "" {
configPath = filepath.Join(configDir, "sway")
}
} }
} else { } else {
// Determine isScroll based on the provided config path
isScroll = strings.Contains(configPath, "scroll") isScroll = strings.Contains(configPath, "scroll")
} }
@@ -36,16 +44,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
} }
func (s *SwayProvider) Name() string { func (s *SwayProvider) Name() string {
if s != nil && s.isScroll {
return "scroll"
}
if s == nil { if s == nil {
_, ok := os.LookupEnv("SCROLLSOCK") if os.Getenv("SCROLLSOCK") != "" {
if ok {
return "scroll" return "scroll"
} }
return "sway"
} }
if s.isScroll {
return "scroll"
}
return "sway" return "sway"
} }
@@ -15,8 +15,13 @@ func TestSwayProviderName(t *testing.T) {
func TestSwayProviderDefaultPath(t *testing.T) { func TestSwayProviderDefaultPath(t *testing.T) {
provider := NewSwayProvider("") provider := NewSwayProvider("")
if provider.configPath != "$HOME/.config/sway" { configDir, err := os.UserConfigDir()
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway") if err != nil {
t.Skip("UserConfigDir not available")
}
expected := filepath.Join(configDir, "sway")
if provider.configPath != expected {
t.Errorf("configPath = %q, want %q", provider.configPath, expected)
} }
} }
+12 -9
View File
@@ -1,15 +1,18 @@
package keybinds package keybinds
type Keybind struct { type Keybind struct {
Key string `json:"key"` Key string `json:"key"`
Description string `json:"desc"` Description string `json:"desc"`
Action string `json:"action,omitempty"` Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"` Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
HideOnOverlay bool `json:"hideOnOverlay,omitempty"` HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
CooldownMs int `json:"cooldownMs,omitempty"` CooldownMs int `json:"cooldownMs,omitempty"`
Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press
Conflict *Keybind `json:"conflict,omitempty"` AllowWhenLocked bool `json:"allowWhenLocked,omitempty"`
AllowInhibiting *bool `json:"allowInhibiting,omitempty"` // nil=default(true), false=explicitly disabled
Repeat *bool `json:"repeat,omitempty"` // nil=default(true), false=explicitly disabled
Conflict *Keybind `json:"conflict,omitempty"`
} }
type DMSBindsStatus struct { type DMSBindsStatus struct {
+281 -75
View File
@@ -3,6 +3,7 @@ package matugen
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -10,10 +11,12 @@ import (
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16" "github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/lucasb-eyer/go-colorful"
) )
type ColorMode string type ColorMode string
@@ -30,6 +33,7 @@ const (
TemplateKindTerminal TemplateKindTerminal
TemplateKindGTK TemplateKindGTK
TemplateKindVSCode TemplateKindVSCode
TemplateKindEmacs
) )
type TemplateDef struct { type TemplateDef struct {
@@ -62,7 +66,7 @@ var templateRegistry = []TemplateDef{
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"}, {ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true}, {ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode}, {ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"}, {ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
} }
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
@@ -75,8 +79,10 @@ func (c *ColorMode) GTKTheme() string {
} }
var ( var (
matugenVersionOnce sync.Once matugenVersionMu sync.Mutex
matugenVersionOK bool
matugenSupportsCOE bool matugenSupportsCOE bool
matugenIsV4 bool
) )
type Options struct { type Options struct {
@@ -250,8 +256,22 @@ func buildOnce(opts *Options) error {
} }
} }
refreshGTK(opts.ConfigDir, opts.Mode) if isDMSGTKActive(opts.ConfigDir) {
signalTerminals() switch opts.Mode {
case ColorModeLight:
syncAccentColor(primaryLight)
default:
syncAccentColor(primaryDark)
}
refreshGTK(opts.Mode)
refreshGTK4()
}
if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) {
refreshQt6ct()
}
signalTerminals(opts)
return nil return nil
} }
@@ -316,6 +336,10 @@ output_path = '%s'
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
}
default: default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
} }
@@ -473,6 +497,9 @@ func substituteVars(content, shellDir string) string {
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/") result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/") result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/") result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
}
return result return result
} }
@@ -493,67 +520,160 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
return content[startIdx : startIdx+endIdx] return content[startIdx : startIdx+endIdx]
} }
func checkMatugenVersion() { type matugenFlags struct {
matugenVersionOnce.Do(func() { supportsCOE bool
cmd := exec.Command("matugen", "--version") isV4 bool
output, err := cmd.Output()
if err != nil {
return
}
versionStr := strings.TrimSpace(string(output))
versionStr = strings.TrimPrefix(versionStr, "matugen ")
parts := strings.Split(versionStr, ".")
if len(parts) < 2 {
return
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return
}
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
if matugenSupportsCOE {
log.Infof("Matugen %s supports --continue-on-error", versionStr)
}
})
} }
func runMatugen(args []string) error { func detectMatugenVersion() (matugenFlags, error) {
checkMatugenVersion() matugenVersionMu.Lock()
defer matugenVersionMu.Unlock()
if matugenSupportsCOE { if matugenVersionOK {
args = append([]string{"--continue-on-error"}, args...) return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
} }
return detectMatugenVersionLocked()
}
func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
matugenVersionMu.Lock()
defer matugenVersionMu.Unlock()
matugenVersionOK = false
flags, err := detectMatugenVersionLocked()
if err != nil {
return old, false
}
changed := flags.supportsCOE != old.supportsCOE || flags.isV4 != old.isV4
return flags, changed
}
func detectMatugenVersionLocked() (matugenFlags, error) {
cmd := exec.Command("matugen", "--version")
output, err := cmd.Output()
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
}
versionStr := strings.TrimSpace(string(output))
versionStr = strings.TrimPrefix(versionStr, "matugen ")
parts := strings.Split(versionStr, ".")
if len(parts) < 2 {
return matugenFlags{}, fmt.Errorf("unexpected matugen version format: %q", versionStr)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to parse matugen major version %q: %w", parts[0], err)
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to parse matugen minor version %q: %w", parts[1], err)
}
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
matugenIsV4 = major >= 4
matugenVersionOK = true
if matugenSupportsCOE {
log.Infof("Matugen %s supports --continue-on-error", versionStr)
}
if matugenIsV4 {
log.Infof("Matugen %s: using v4 flags", versionStr)
}
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
}
func buildMatugenArgs(baseArgs []string, flags matugenFlags) []string {
args := make([]string, 0, len(baseArgs)+4)
if flags.supportsCOE {
args = append(args, "--continue-on-error")
}
args = append(args, baseArgs...)
if flags.isV4 {
args = append(args, "--source-color-index", "0")
}
return args
}
func runMatugen(baseArgs []string) error {
flags, err := detectMatugenVersion()
if err != nil {
return err
}
args := buildMatugenArgs(baseArgs, flags)
cmd := exec.Command("matugen", args...) cmd := exec.Command("matugen", args...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() runErr := cmd.Run()
if runErr == nil {
return nil
}
log.Warnf("Matugen failed (v4=%v): %v", flags.isV4, runErr)
newFlags, changed := redetectMatugenVersion(flags)
if !changed {
return runErr
}
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
args = buildMatugenArgs(baseArgs, newFlags)
retryCmd := exec.Command("matugen", args...)
retryCmd.Stdout = os.Stdout
retryCmd.Stderr = os.Stderr
return retryCmd.Run()
} }
func runMatugenDryRun(opts *Options) (string, error) { func runMatugenDryRun(opts *Options) (string, error) {
var args []string flags, err := detectMatugenVersion()
switch opts.Kind {
case "hex":
args = []string{"color", "hex", opts.Value}
default:
args = []string{opts.Kind, opts.Value}
}
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
cmd := exec.Command("matugen", args...)
output, err := cmd.Output()
if err != nil { if err != nil {
return "", err return "", err
} }
output, dryErr := execDryRun(opts, flags)
if dryErr == nil {
return output, nil
}
log.Warnf("Matugen dry-run failed (v4=%v): %v", flags.isV4, dryErr)
newFlags, changed := redetectMatugenVersion(flags)
if !changed {
return "", dryErr
}
log.Warnf("Matugen version changed (v4: %v -> %v), retrying dry-run", flags.isV4, newFlags.isV4)
return execDryRun(opts, newFlags)
}
func execDryRun(opts *Options, flags matugenFlags) (string, error) {
var baseArgs []string
switch opts.Kind {
case "hex":
baseArgs = []string{"color", "hex", opts.Value}
default:
baseArgs = []string{opts.Kind, opts.Value}
}
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
if flags.isV4 {
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
}
cmd := exec.Command("matugen", baseArgs...)
var stderr strings.Builder
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
if stderr.Len() > 0 {
return "", fmt.Errorf("matugen %v failed (v4=%v): %s", baseArgs, flags.isV4, strings.TrimSpace(stderr.String()))
}
return "", fmt.Errorf("matugen %v failed (v4=%v): %w", baseArgs, flags.isV4, err)
}
return strings.ReplaceAll(string(output), "\n", ""), nil return strings.ReplaceAll(string(output), "\n", ""), nil
} }
@@ -617,40 +737,73 @@ func generateDank16Variants(primaryDark, primaryLight, surface string, mode Colo
return dank16.GenerateVariantJSON(variantColors) return dank16.GenerateVariantJSON(variantColors)
} }
func refreshGTK(configDir string, mode ColorMode) { func isDMSGTKActive(configDir string) bool {
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css") gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
info, err := os.Lstat(gtkCSS) info, err := os.Lstat(gtkCSS)
if err != nil { if err != nil {
return return false
} }
shouldRun := false
if info.Mode()&os.ModeSymlink != 0 { if info.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(gtkCSS) target, err := os.Readlink(gtkCSS)
if err == nil && strings.Contains(target, "dank-colors.css") { return err == nil && strings.Contains(target, "dank-colors.css")
shouldRun = true
}
} else {
data, err := os.ReadFile(gtkCSS)
if err == nil && strings.Contains(string(data), "dank-colors.css") {
shouldRun = true
}
} }
if !shouldRun { data, err := os.ReadFile(gtkCSS)
return return err == nil && strings.Contains(string(data), "dank-colors.css")
}
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()).Run()
} }
func signalTerminals() { func refreshGTK(mode ColorMode) {
signalByName("kitty", syscall.SIGUSR1) if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
signalByName("ghostty", syscall.SIGUSR2) log.Warnf("Failed to reset gtk-theme: %v", err)
signalByName(".kitty-wrapped", syscall.SIGUSR1) }
signalByName(".ghostty-wrappe", syscall.SIGUSR2) if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil {
log.Warnf("Failed to set gtk-theme: %v", err)
}
}
func refreshGTK4() {
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
if err != nil {
return
}
current := strings.Trim(output, "'")
var toggle string
if current == "prefer-dark" {
toggle = "default"
} else {
toggle = "prefer-dark"
}
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
return
}
time.Sleep(50 * time.Millisecond)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
}
}
func refreshQt6ct() {
confPath := filepath.Join(utils.XDGConfigHome(), "qt6ct", "qt6ct.conf")
now := time.Now()
if err := os.Chtimes(confPath, now, now); err != nil {
log.Warnf("Failed to touch qt6ct.conf: %v", err)
}
}
func signalTerminals(opts *Options) {
if !opts.ShouldSkipTemplate("kitty") && appExists(opts.AppChecker, []string{"kitty"}, nil) {
signalByName("kitty", syscall.SIGUSR1)
signalByName(".kitty-wrapped", syscall.SIGUSR1)
}
if !opts.ShouldSkipTemplate("ghostty") && appExists(opts.AppChecker, []string{"ghostty"}, nil) {
signalByName("ghostty", syscall.SIGUSR2)
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
}
} }
func signalByName(name string, sig syscall.Signal) { func signalByName(name string, sig syscall.Signal) {
@@ -679,8 +832,59 @@ func syncColorScheme(mode ColorMode) {
scheme = "default" scheme = "default"
} }
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", scheme); err != nil {
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run() log.Warnf("Failed to sync color-scheme: %v", err)
}
}
var adwaitaAccents = []struct {
name string
colors []colorful.Color
}{
{"blue", hexColors("#3f8ae5", "#438de6", "#a4caee")},
{"green", hexColors("#26a269", "#39ac76", "#81d5ad")},
{"orange", hexColors("#f17738", "#ff7800", "#ffc994")},
{"pink", hexColors("#e4358a", "#e64392", "#f9b3d5")},
{"purple", hexColors("#954ab5", "#9c46b9", "#d099d6")},
{"red", hexColors("#e84053", "#e01b24", "#f2a1a5")},
{"slate", hexColors("#557b9f", "#6a8daf", "#b4c6d6")},
{"teal", hexColors("#129eb0", "#2190a4", "#7bdff4")},
{"yellow", hexColors("#cbac10", "#d4b411", "#f5c211")},
}
func hexColors(hexes ...string) []colorful.Color {
out := make([]colorful.Color, len(hexes))
for i, h := range hexes {
out[i], _ = colorful.Hex(h)
}
return out
}
func closestAdwaitaAccent(primaryHex string) string {
c, err := colorful.Hex(primaryHex)
if err != nil {
return "blue"
}
best := "blue"
bestDist := math.MaxFloat64
for _, a := range adwaitaAccents {
for _, ref := range a.colors {
d := c.DistanceCIEDE2000(ref)
if d < bestDist {
bestDist = d
best = a.name
}
}
}
return best
}
func syncAccentColor(primaryHex string) {
accent := closestAdwaitaAccent(primaryHex)
log.Infof("Setting GNOME accent color: %s", accent)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil {
log.Warnf("Failed to set accent-color: %v", err)
} }
} }
@@ -705,6 +909,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
detected = true detected = true
case tmpl.Kind == TemplateKindVSCode: case tmpl.Kind == TemplateKindVSCode:
detected = checkVSCodeExtension(homeDir) detected = checkVSCodeExtension(homeDir)
case tmpl.Kind == TemplateKindEmacs:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
default: default:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
} }
+10
View File
@@ -15,6 +15,9 @@ const (
notifyDest = "org.freedesktop.Notifications" notifyDest = "org.freedesktop.Notifications"
notifyPath = "/org/freedesktop/Notifications" notifyPath = "/org/freedesktop/Notifications"
notifyInterface = "org.freedesktop.Notifications" notifyInterface = "org.freedesktop.Notifications"
maxSummaryLen = 29
maxBodyLen = 80
) )
type Notification struct { type Notification struct {
@@ -39,6 +42,13 @@ func Send(n Notification) error {
n.Timeout = 5000 n.Timeout = 5000
} }
if len(n.Summary) > maxSummaryLen {
n.Summary = n.Summary[:maxSummaryLen-3] + "..."
}
if len(n.Body) > maxBodyLen {
n.Body = n.Body[:maxBodyLen-3] + "..."
}
var actions []string var actions []string
if n.FilePath != "" { if n.FilePath != "" {
actions = []string{ actions = []string{
+28 -1
View File
@@ -21,6 +21,7 @@ const (
CompositorNiri CompositorNiri
CompositorDWL CompositorDWL
CompositorScroll CompositorScroll
CompositorMiracle
) )
var detectedCompositor Compositor = -1 var detectedCompositor Compositor = -1
@@ -34,6 +35,7 @@ func DetectCompositor() Compositor {
niriSocket := os.Getenv("NIRI_SOCKET") niriSocket := os.Getenv("NIRI_SOCKET")
swaySocket := os.Getenv("SWAYSOCK") swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK") scrollSocket := os.Getenv("SCROLLSOCK")
miracleSocket := os.Getenv("MIRACLESOCK")
switch { switch {
case niriSocket != "": case niriSocket != "":
@@ -46,7 +48,11 @@ func DetectCompositor() Compositor {
detectedCompositor = CompositorScroll detectedCompositor = CompositorScroll
return detectedCompositor return detectedCompositor
} }
case miracleSocket != "":
if _, err := os.Stat(miracleSocket); err == nil {
detectedCompositor = CompositorMiracle
return detectedCompositor
}
case swaySocket != "": case swaySocket != "":
if _, err := os.Stat(swaySocket); err == nil { if _, err := os.Stat(swaySocket); err == nil {
detectedCompositor = CompositorSway detectedCompositor = CompositorSway
@@ -260,6 +266,25 @@ func getScrollFocusedMonitor() string {
return "" return ""
} }
func getMiracleFocusedMonitor() string {
output, err := exec.Command("miraclemsg", "-t", "get_workspaces").Output()
if err != nil {
return ""
}
var workspaces []swayWorkspace
if err := json.Unmarshal(output, &workspaces); err != nil {
return ""
}
for _, ws := range workspaces {
if ws.Focused {
return ws.Output
}
}
return ""
}
type niriWorkspace struct { type niriWorkspace struct {
Output string `json:"output"` Output string `json:"output"`
IsFocused bool `json:"is_focused"` IsFocused bool `json:"is_focused"`
@@ -407,6 +432,8 @@ func GetFocusedMonitor() string {
return getSwayFocusedMonitor() return getSwayFocusedMonitor()
case CompositorScroll: case CompositorScroll:
return getScrollFocusedMonitor() return getScrollFocusedMonitor()
case CompositorMiracle:
return getMiracleFocusedMonitor()
case CompositorNiri: case CompositorNiri:
return getNiriFocusedMonitor() return getNiriFocusedMonitor()
case CompositorDWL: case CompositorDWL:
+1 -1
View File
@@ -108,7 +108,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
screenshoter: s, screenshoter: s,
outputs: make(map[uint32]*WaylandOutput), outputs: make(map[uint32]*WaylandOutput),
preCapture: make(map[*WaylandOutput]*PreCapture), preCapture: make(map[*WaylandOutput]*PreCapture),
showCapturedCursor: true, showCapturedCursor: s.config.Cursor == CursorOn,
} }
} }
+2 -8
View File
@@ -453,10 +453,7 @@ func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted
} }
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) { func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
cursor := int32(0) cursor := int32(s.config.Cursor)
if s.config.IncludeCursor {
cursor = 1
}
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput) frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
if err != nil { if err != nil {
@@ -624,10 +621,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
} }
} }
cursor := int32(0) cursor := int32(s.config.Cursor)
if s.config.IncludeCursor {
cursor = 1
}
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h) frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
if err != nil { if err != nil {
+2 -3
View File
@@ -3,11 +3,11 @@ package screenshot
import ( import (
"encoding/json" "encoding/json"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type ThemeColors struct { type ThemeColors struct {
@@ -83,12 +83,11 @@ func getColorsFilePath() string {
} }
func isLightMode() bool { func isLightMode() bool {
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output() scheme, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
if err != nil { if err != nil {
return false return false
} }
scheme := strings.TrimSpace(string(out))
switch scheme { switch scheme {
case "'prefer-light'", "'default'": case "'prefer-light'", "'default'":
return true return true
+27 -20
View File
@@ -19,6 +19,13 @@ const (
FormatPPM FormatPPM
) )
type CursorMode int
const (
CursorOff CursorMode = iota
CursorOn
)
type Region struct { type Region struct {
X int32 `json:"x"` X int32 `json:"x"`
Y int32 `json:"y"` Y int32 `json:"y"`
@@ -42,29 +49,29 @@ type Output struct {
} }
type Config struct { type Config struct {
Mode Mode Mode Mode
OutputName string OutputName string
IncludeCursor bool Cursor CursorMode
Format Format Format Format
Quality int Quality int
OutputDir string OutputDir string
Filename string Filename string
Clipboard bool Clipboard bool
SaveFile bool SaveFile bool
Notify bool Notify bool
Stdout bool Stdout bool
} }
func DefaultConfig() Config { func DefaultConfig() Config {
return Config{ return Config{
Mode: ModeRegion, Mode: ModeRegion,
IncludeCursor: false, Cursor: CursorOff,
Format: FormatPNG, Format: FormatPNG,
Quality: 90, Quality: 90,
OutputDir: "", OutputDir: "",
Filename: "", Filename: "",
Clipboard: true, Clipboard: true,
SaveFile: true, SaveFile: true,
Notify: true, Notify: true,
} }
} }
@@ -96,6 +96,12 @@ func (m *Manager) Rescan() {
} }
} }
if m.sysfsReady && m.sysfsBackend != nil {
if err := m.sysfsBackend.Rescan(); err != nil {
log.Debugf("Sysfs rescan failed: %v", err)
}
}
m.updateState() m.updateState()
} }
+4
View File
@@ -101,6 +101,10 @@ func shouldSuppressDevice(name string) bool {
return false return false
} }
func (b *SysfsBackend) Rescan() error {
return b.scanDevices()
}
func (b *SysfsBackend) GetDevices() ([]Device, error) { func (b *SysfsBackend) GetDevices() ([]Device, error) {
devices := make([]Device, 0) devices := make([]Device, 0)
+10 -3
View File
@@ -146,9 +146,16 @@ func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
return return
} }
if err := m.TouchEntry(uint64(id)); err != nil { if entry.Pinned {
models.RespondError(conn, req.ID, err.Error()) if err := m.CreateHistoryEntryFromPinned(entry); err != nil {
return models.RespondError(conn, req.ID, err.Error())
return
}
} else {
if err := m.TouchEntry(uint64(id)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
} }
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
+92 -1
View File
@@ -388,6 +388,10 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
if extractHash(v) != hash { if extractHash(v) != hash {
continue continue
} }
entry, err := decodeEntry(v)
if err == nil && entry.Pinned {
continue
}
if err := b.Delete(k); err != nil { if err := b.Delete(k); err != nil {
return err return err
} }
@@ -842,6 +846,62 @@ func (m *Manager) TouchEntry(id uint64) error {
return nil return nil
} }
func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
if m.db == nil {
return fmt.Errorf("database not available")
}
// Create a new unpinned entry with the same data
newEntry := Entry{
Data: pinnedEntry.Data,
MimeType: pinnedEntry.MimeType,
Size: pinnedEntry.Size,
Timestamp: time.Now(),
IsImage: pinnedEntry.IsImage,
Preview: pinnedEntry.Preview,
Pinned: false,
}
if err := m.storeEntryWithoutDedup(newEntry); err != nil {
return err
}
m.updateState()
m.notifySubscribers()
return nil
}
func (m *Manager) storeEntryWithoutDedup(entry Entry) error {
if m.db == nil {
return fmt.Errorf("database not available")
}
entry.Hash = computeHash(entry.Data)
return m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
id, err := b.NextSequence()
if err != nil {
return err
}
entry.ID = id
encoded, err := encodeEntry(entry)
if err != nil {
return err
}
if err := b.Put(itob(id), encoded); err != nil {
return err
}
return m.trimLengthInTx(b)
})
}
func (m *Manager) ClearHistory() { func (m *Manager) ClearHistory() {
if m.db == nil { if m.db == nil {
return return
@@ -1419,6 +1479,37 @@ func (m *Manager) PinEntry(id uint64) error {
return fmt.Errorf("database not available") return fmt.Errorf("database not available")
} }
entryToPin, err := m.GetEntry(id)
if err != nil {
return err
}
var hashExists bool
if err := m.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return nil
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
if err != nil || !entry.Pinned {
continue
}
if entry.Hash == entryToPin.Hash {
hashExists = true
return nil
}
}
return nil
}); err != nil {
return err
}
if hashExists {
return nil
}
// Check pinned count // Check pinned count
cfg := m.getConfig() cfg := m.getConfig()
pinnedCount := 0 pinnedCount := 0
@@ -1443,7 +1534,7 @@ func (m *Manager) PinEntry(id uint64) error {
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned) return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
} }
err := m.db.Update(func(tx *bolt.Tx) error { err = m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard")) b := tx.Bucket([]byte("clipboard"))
v := b.Get(itob(id)) v := b.Get(itob(id))
if v == nil { if v == nil {
+1 -3
View File
@@ -276,9 +276,7 @@ func (m *Manager) UnsubscribeClient(clientID string) {
}) })
for _, subID := range toDelete { for _, subID := range toDelete {
if err := m.Unsubscribe(subID); err != nil { _ = m.Unsubscribe(subID)
log.Warnf("dbus: failed to unsubscribe %s: %v", subID, err)
}
} }
} }
+3 -19
View File
@@ -1,11 +1,9 @@
package freedesktop package freedesktop
import ( import (
"context"
"fmt" "fmt"
"os/exec"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -107,22 +105,8 @@ func (m *Manager) GetUserIconFile(username string) (string, error) {
} }
func (m *Manager) SetIconTheme(iconTheme string) error { func (m *Manager) SetIconTheme(iconTheme string) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) if err := utils.GsettingsSet("org.gnome.desktop.interface", "icon-theme", iconTheme); err != nil {
defer cancel() return fmt.Errorf("failed to set icon theme: %w", err)
check := exec.CommandContext(ctx, "gsettings", "writable", "org.gnome.desktop.interface", "icon-theme")
if err := check.Run(); err == nil {
cmd := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.desktop.interface", "icon-theme", iconTheme)
if err := cmd.Run(); err != nil {
return fmt.Errorf("gsettings set failed: %w", err)
}
return nil
} }
checkDconf := exec.CommandContext(ctx, "dconf", "write", "/org/gnome/desktop/interface/icon-theme", fmt.Sprintf("'%s'", iconTheme))
if err := checkDconf.Run(); err != nil {
return fmt.Errorf("both gsettings and dconf unavailable or failed: %w", err)
}
return nil return nil
} }
@@ -52,11 +52,31 @@ func (m *Manager) initializeScreensaver() error {
return nil return nil
} }
screensaverIface := introspect.Interface{
Name: dbusScreensaverInterface,
Methods: []introspect.Method{
{
Name: "Inhibit",
Args: []introspect.Arg{
{Name: "application_name", Type: "s", Direction: "in"},
{Name: "reason_for_inhibit", Type: "s", Direction: "in"},
{Name: "cookie", Type: "u", Direction: "out"},
},
},
{
Name: "UnInhibit",
Args: []introspect.Arg{
{Name: "cookie", Type: "u", Direction: "in"},
},
},
},
}
introNode := &introspect.Node{ introNode := &introspect.Node{
Name: dbusScreensaverPath, Name: dbusScreensaverPath,
Interfaces: []introspect.Interface{ Interfaces: []introspect.Interface{
introspect.IntrospectData, introspect.IntrospectData,
{Name: dbusScreensaverInterface}, screensaverIface,
}, },
} }
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil { if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
@@ -67,7 +87,7 @@ func (m *Manager) initializeScreensaver() error {
Name: dbusScreensaverPath2, Name: dbusScreensaverPath2,
Interfaces: []introspect.Interface{ Interfaces: []introspect.Interface{
introspect.IntrospectData, introspect.IntrospectData,
{Name: dbusScreensaverInterface}, screensaverIface,
}, },
} }
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil { if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
@@ -32,8 +32,10 @@ type SecretAgent struct {
backend *NetworkManagerBackend backend *NetworkManagerBackend
} }
type nmVariantMap map[string]dbus.Variant type (
type nmSettingMap map[string]nmVariantMap nmVariantMap map[string]dbus.Variant
nmSettingMap map[string]nmVariantMap
)
const introspectXML = ` const introspectXML = `
<node> <node>
@@ -122,7 +124,7 @@ func (a *SecretAgent) GetSecrets(
connType, displayName, vpnSvc := readConnTypeAndName(conn) connType, displayName, vpnSvc := readConnTypeAndName(conn)
ssid := readSSID(conn) ssid := readSSID(conn)
fields := fieldsNeeded(settingName, hints) fields := fieldsNeeded(settingName, hints, conn)
vpnPasswordFlags := readVPNPasswordFlags(conn, settingName) vpnPasswordFlags := readVPNPasswordFlags(conn, settingName)
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags) log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags)
@@ -218,8 +220,16 @@ func (a *SecretAgent) GetSecrets(
out[settingName] = nmVariantMap{} out[settingName] = nmVariantMap{}
return out, nil return out, nil
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 { } else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags) switch settingName {
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) case "802-11-wireless-security":
fields = []string{"psk"}
case "802-1x":
fields = infer8021xFields(conn)
default:
log.Warnf("[SecretAgent] Agent-owned secrets for unhandled setting %s (flags=%d)", settingName, passwordFlags)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
}
log.Infof("[SecretAgent] Agent-owned secrets, inferred fields: %v", fields)
} else { } else {
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags) log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
out := nmSettingMap{} out := nmSettingMap{}
@@ -300,6 +310,63 @@ func (a *SecretAgent) GetSecrets(
return out, nil return out, nil
} }
a.backend.cachedVPNCredsMu.Unlock() a.backend.cachedVPNCredsMu.Unlock()
a.backend.cachedGPSamlMu.Lock()
cachedGPSaml := a.backend.cachedGPSamlCookie
if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid {
a.backend.cachedGPSamlMu.Unlock()
log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid)
return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil
}
a.backend.cachedGPSamlMu.Unlock()
if len(fields) == 1 && fields[0] == "gp-saml" {
gateway := ""
protocol := ""
if vpnSettings, ok := conn["vpn"]; ok {
if dataVariant, ok := vpnSettings["data"]; ok {
if dataMap, ok := dataVariant.Value().(map[string]string); ok {
if gw, ok := dataMap["gateway"]; ok {
gateway = gw
}
if proto, ok := dataMap["protocol"]; ok && proto != "" {
protocol = proto
}
}
}
}
if protocol != "gp" {
return nil, dbus.MakeFailedError(fmt.Errorf("gp-saml auth only supported for GlobalProtect (protocol=gp), got: %s", protocol))
}
log.Infof("[SecretAgent] Starting GlobalProtect SAML authentication for gateway=%s", gateway)
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer samlCancel()
authResult, err := a.backend.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
if err != nil {
log.Warnf("[SecretAgent] GlobalProtect SAML authentication failed: %v", err)
return nil, dbus.MakeFailedError(fmt.Errorf("GlobalProtect SAML authentication failed: %w", err))
}
log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager")
a.backend.cachedGPSamlMu.Lock()
a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{
ConnectionUUID: connUuid,
Cookie: authResult.Cookie,
Host: authResult.Host,
User: authResult.User,
Fingerprint: authResult.Fingerprint,
}
a.backend.cachedGPSamlMu.Unlock()
return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
}
} }
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
@@ -418,8 +485,19 @@ func (a *SecretAgent) GetSecrets(
log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request") log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request")
} }
case "802-1x": case "802-1x":
out[settingName] = sec secretsOnly := nmVariantMap{}
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec)) for k, v := range reply.Secrets {
switch k {
case "password", "private-key-password", "phase2-private-key-password", "pin":
secretsOnly[k] = dbus.MakeVariant(v)
}
}
out[settingName] = secretsOnly
if identity, ok := reply.Secrets["identity"]; ok && identity != "" {
a.save8021xIdentity(path, identity)
}
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(secretsOnly))
default: default:
out[settingName] = sec out[settingName] = sec
} }
@@ -434,63 +512,6 @@ func (a *SecretAgent) GetSecrets(
} }
a.backend.pendingVPNSaveMu.Unlock() a.backend.pendingVPNSaveMu.Unlock()
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds") log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds")
} else if reply.Save && settingName != "vpn" {
// Non-VPN save logic
go func() {
log.Infof("[SecretAgent] Persisting secrets with Update2: path=%s, setting=%s", path, settingName)
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
var existingSettings map[string]map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
log.Warnf("[SecretAgent] GetSettings failed: %v", err)
return
}
settings := make(map[string]map[string]dbus.Variant)
if connSection, ok := existingSettings["connection"]; ok {
settings["connection"] = connSection
}
switch settingName {
case "802-11-wireless-security":
wifiSec, ok := existingSettings["802-11-wireless-security"]
if !ok {
wifiSec = make(map[string]dbus.Variant)
}
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
if psk, ok := reply.Secrets["psk"]; ok {
wifiSec["psk"] = dbus.MakeVariant(psk)
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
}
settings["802-11-wireless-security"] = wifiSec
case "802-1x":
dot1x, ok := existingSettings["802-1x"]
if !ok {
dot1x = make(map[string]dbus.Variant)
}
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
if password, ok := reply.Secrets["password"]; ok {
dot1x["password"] = dbus.MakeVariant(password)
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
}
settings["802-1x"] = dot1x
}
// Call Update2 with correct signature:
// Update2(IN settings, IN flags, IN args) -> OUT result
// flags: 0x1 = to-disk
var result map[string]dbus.Variant
err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result)
if err != nil {
log.Warnf("[SecretAgent] Update2(to-disk) failed: %v", err)
} else {
log.Infof("[SecretAgent] Successfully persisted secrets to disk for %s", settingName)
}
}()
} }
return out, nil return out, nil
@@ -523,6 +544,35 @@ func (a *SecretAgent) Introspect() (string, *dbus.Error) {
return introspectXML, nil return introspectXML, nil
} }
func (a *SecretAgent) save8021xIdentity(path dbus.ObjectPath, identity string) {
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
var existing map[string]map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existing); err != nil {
log.Warnf("[SecretAgent] Failed to get settings for identity save: %v", err)
return
}
settings := make(map[string]map[string]dbus.Variant)
if connSection, ok := existing["connection"]; ok {
settings["connection"] = connSection
}
dot1x, ok := existing["802-1x"]
if !ok {
dot1x = make(map[string]dbus.Variant)
}
dot1x["identity"] = dbus.MakeVariant(identity)
settings["802-1x"] = dot1x
var result map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
log.Warnf("[SecretAgent] Failed to save 802.1x identity: %v", err)
return
}
log.Infof("[SecretAgent] Saved 802.1x identity to connection profile")
}
func readSSID(conn map[string]nmVariantMap) string { func readSSID(conn map[string]nmVariantMap) string {
if w, ok := conn["802-11-wireless"]; ok { if w, ok := conn["802-11-wireless"]; ok {
if v, ok := w["ssid"]; ok { if v, ok := w["ssid"]; ok {
@@ -564,12 +614,15 @@ func readConnTypeAndName(conn map[string]nmVariantMap) (string, string, string)
return connType, name, svc return connType, name, svc
} }
func fieldsNeeded(setting string, hints []string) []string { func fieldsNeeded(setting string, hints []string, conn map[string]nmVariantMap) []string {
switch setting { switch setting {
case "802-11-wireless-security": case "802-11-wireless-security":
return []string{"psk"} return []string{"psk"}
case "802-1x": case "802-1x":
return []string{"identity", "password"} if len(hints) > 0 {
return hints
}
return infer8021xFields(conn)
case "vpn": case "vpn":
return hints return hints
default: default:
@@ -577,6 +630,41 @@ func fieldsNeeded(setting string, hints []string) []string {
} }
} }
func infer8021xFields(conn map[string]nmVariantMap) []string {
dot1x, ok := conn["802-1x"]
if !ok {
return []string{"identity", "password"}
}
var fields []string
if v, ok := dot1x["identity"]; ok {
if id, ok := v.Value().(string); ok && id != "" {
// identity already stored, don't ask again
} else {
fields = append(fields, "identity")
}
} else {
fields = append(fields, "identity")
}
var eapMethods []string
if v, ok := dot1x["eap"]; ok {
if methods, ok := v.Value().([]string); ok {
eapMethods = methods
}
}
switch {
case len(eapMethods) > 0 && eapMethods[0] == "tls":
fields = append(fields, "private-key-password")
default:
fields = append(fields, "password")
}
return fields
}
func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo { func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo {
result := make([]FieldInfo, 0, len(fields)) result := make([]FieldInfo, 0, len(fields))
for _, f := range fields { for _, f := range fields {
@@ -630,12 +718,25 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
switch { switch {
case strings.Contains(vpnService, "openconnect"): case strings.Contains(vpnService, "openconnect"):
protocol := dataMap["protocol"]
authType := dataMap["authtype"] authType := dataMap["authtype"]
userCert := dataMap["usercert"] username := dataMap["username"]
if authType == "cert" && strings.HasPrefix(userCert, "pkcs11:") {
if authType == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:") {
return []string{"key_pass"} return []string{"key_pass"}
} }
if dataMap["username"] == "" {
if needsExternalBrowserAuth(protocol, authType, username, dataMap) {
switch protocol {
case "gp":
log.Infof("[SecretAgent] GlobalProtect SAML auth detected")
return []string{"gp-saml"}
default:
log.Infof("[SecretAgent] External browser auth detected for protocol '%s' but only GlobalProtect (gp) SAML is currently supported, falling back to credentials", protocol)
}
}
if username == "" {
fields = []string{"username", "password"} fields = []string{"username", "password"}
} }
case strings.Contains(vpnService, "openvpn"): case strings.Contains(vpnService, "openvpn"):
@@ -654,8 +755,31 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
return fields return fields
} }
func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool {
if method, ok := data["saml-auth-method"]; ok {
if method == "REDIRECT" || method == "POST" {
return true
}
}
if authType != "" && authType != "password" && authType != "cert" {
return true
}
switch protocol {
case "gp":
if authType == "" && username == "" {
return true
}
}
return false
}
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) { func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
switch field { switch field {
case "gp-saml":
return "GlobalProtect SAML/SSO", false
case "key_pass": case "key_pass":
return "PIN", true return "PIN", true
case "password": case "password":
@@ -756,3 +880,18 @@ func reasonFromFlags(flags uint32) string {
} }
return "required" return "required"
} }
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
out := nmSettingMap{}
vpnSec := nmVariantMap{}
secrets := map[string]string{
"cookie": cookie,
"gateway": host,
"gwcert": fingerprint,
}
vpnSec["secrets"] = dbus.MakeVariant(secrets)
out[settingName] = vpnSec
return out
}
@@ -0,0 +1,355 @@
package network
import (
"testing"
"github.com/godbus/dbus/v5"
"github.com/stretchr/testify/assert"
)
func TestNeedsExternalBrowserAuth(t *testing.T) {
tests := []struct {
name string
protocol string
authType string
username string
data map[string]string
expected bool
}{
{
name: "GP with saml-auth-method REDIRECT",
protocol: "gp",
authType: "password",
username: "user",
data: map[string]string{"saml-auth-method": "REDIRECT"},
expected: true,
},
{
name: "GP with saml-auth-method POST",
protocol: "gp",
authType: "password",
username: "user",
data: map[string]string{"saml-auth-method": "POST"},
expected: true,
},
{
name: "GP with no authtype and no username",
protocol: "gp",
authType: "",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "GP with username and password authtype",
protocol: "gp",
authType: "password",
username: "john",
data: map[string]string{},
expected: false,
},
{
name: "GP with username but no authtype",
protocol: "gp",
authType: "",
username: "john",
data: map[string]string{},
expected: false,
},
{
name: "GP with authtype but no username - should detect SAML",
protocol: "gp",
authType: "",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "pulse with SAML",
protocol: "pulse",
authType: "",
username: "",
data: map[string]string{"saml-auth-method": "REDIRECT"},
expected: true,
},
{
name: "fortinet with non-password authtype",
protocol: "fortinet",
authType: "saml",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "anyconnect with cert",
protocol: "anyconnect",
authType: "cert",
username: "",
data: map[string]string{},
expected: false,
},
{
name: "anyconnect with password",
protocol: "anyconnect",
authType: "password",
username: "user",
data: map[string]string{},
expected: false,
},
{
name: "empty protocol",
protocol: "",
authType: "",
username: "",
data: map[string]string{},
expected: false,
},
{
name: "GP with cert authtype",
protocol: "gp",
authType: "cert",
username: "",
data: map[string]string{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := needsExternalBrowserAuth(tt.protocol, tt.authType, tt.username, tt.data)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBuildGPSamlSecretsResponse(t *testing.T) {
tests := []struct {
name string
settingName string
cookie string
host string
fingerprint string
}{
{
name: "all fields populated",
settingName: "vpn",
cookie: "authcookie=abc123&portal=GATE",
host: "vpn.example.com",
fingerprint: "pin-sha256:ABCD1234",
},
{
name: "empty fingerprint",
settingName: "vpn",
cookie: "authcookie=xyz",
host: "10.0.0.1",
fingerprint: "",
},
{
name: "complex cookie with special chars",
settingName: "vpn",
cookie: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100",
host: "connect.seclore.com",
fingerprint: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildGPSamlSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
assert.NotNil(t, result)
assert.Contains(t, result, tt.settingName)
vpnSec := result[tt.settingName]
assert.NotNil(t, vpnSec)
secretsVariant, ok := vpnSec["secrets"]
assert.True(t, ok, "secrets key should exist")
secrets, ok := secretsVariant.Value().(map[string]string)
assert.True(t, ok, "secrets should be map[string]string")
assert.Equal(t, tt.cookie, secrets["cookie"])
assert.Equal(t, tt.host, secrets["gateway"])
assert.Equal(t, tt.fingerprint, secrets["gwcert"])
})
}
}
func TestVpnFieldMeta_GPSaml(t *testing.T) {
label, isSecret := vpnFieldMeta("gp-saml", "org.freedesktop.NetworkManager.openconnect")
assert.Equal(t, "GlobalProtect SAML/SSO", label)
assert.False(t, isSecret, "gp-saml should not be marked as secret")
}
func TestVpnFieldMeta_StandardFields(t *testing.T) {
tests := []struct {
field string
vpnService string
expectedLabel string
expectedSecret bool
}{
{
field: "username",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "Username",
expectedSecret: false,
},
{
field: "password",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "Password",
expectedSecret: true,
},
{
field: "key_pass",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "PIN",
expectedSecret: true,
},
}
for _, tt := range tests {
t.Run(tt.field, func(t *testing.T) {
label, isSecret := vpnFieldMeta(tt.field, tt.vpnService)
assert.Equal(t, tt.expectedLabel, label)
assert.Equal(t, tt.expectedSecret, isSecret)
})
}
}
func TestInferVPNFields_GPSaml(t *testing.T) {
tests := []struct {
name string
vpnService string
dataMap map[string]string
expectedLen int
shouldHave []string
}{
{
name: "GP with no authtype and no username - should require SAML",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with saml-auth-method REDIRECT",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"saml-auth-method": "REDIRECT",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with saml-auth-method POST",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"saml-auth-method": "POST",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with username and password authtype - should use credentials",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"authtype": "password",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
{
name: "GP with username but no authtype - password only",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
{
name: "GP with PKCS11 cert",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"authtype": "cert",
"usercert": "pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II",
},
expectedLen: 1,
shouldHave: []string{"key_pass"},
},
{
name: "non-GP protocol (anyconnect)",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "anyconnect",
"gateway": "vpn.example.com",
},
expectedLen: 2,
shouldHave: []string{"username", "password"},
},
{
name: "OpenVPN with username",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "password",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Convert dataMap to nmVariantMap
vpnSettings := make(nmVariantMap)
vpnSettings["data"] = dbus.MakeVariant(tt.dataMap)
vpnSettings["service-type"] = dbus.MakeVariant(tt.vpnService)
conn := make(map[string]nmVariantMap)
conn["vpn"] = vpnSettings
fields := inferVPNFields(conn, tt.vpnService)
assert.Len(t, fields, tt.expectedLen, "unexpected number of fields")
if len(tt.shouldHave) > 0 {
for _, expected := range tt.shouldHave {
assert.Contains(t, fields, expected, "should contain field: %s", expected)
}
}
})
}
}
func TestNmVariantMap(t *testing.T) {
// Test that nmVariantMap and nmSettingMap work correctly
settingMap := make(nmSettingMap)
variantMap := make(nmVariantMap)
variantMap["test-key"] = dbus.MakeVariant("test-value")
settingMap["test-setting"] = variantMap
assert.Contains(t, settingMap, "test-setting")
assert.Contains(t, settingMap["test-setting"], "test-key")
value := settingMap["test-setting"]["test-key"].Value()
assert.Equal(t, "test-value", value)
}
@@ -69,12 +69,14 @@ type NetworkManagerBackend struct {
lastFailedTime int64 lastFailedTime int64
failedMutex sync.RWMutex failedMutex sync.RWMutex
pendingVPNSave *pendingVPNCredentials pendingVPNSave *pendingVPNCredentials
pendingVPNSaveMu sync.Mutex pendingVPNSaveMu sync.Mutex
cachedVPNCreds *cachedVPNCredentials cachedVPNCreds *cachedVPNCredentials
cachedVPNCredsMu sync.Mutex cachedVPNCredsMu sync.Mutex
cachedPKCS11PIN *cachedPKCS11PIN cachedPKCS11PIN *cachedPKCS11PIN
cachedPKCS11Mu sync.Mutex cachedPKCS11Mu sync.Mutex
cachedGPSamlCookie *cachedGPSamlCookie
cachedGPSamlMu sync.Mutex
onStateChange func() onStateChange func()
} }
@@ -97,6 +99,14 @@ type cachedPKCS11PIN struct {
PIN string PIN string
} }
type cachedGPSamlCookie struct {
ConnectionUUID string
Cookie string
Host string
User string
Fingerprint string
}
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) { func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
var nm gonetworkmanager.NetworkManager var nm gonetworkmanager.NetworkManager
var err error var err error
@@ -0,0 +1,203 @@
package network
import (
"bufio"
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
type gpSamlAuthResult struct {
Cookie string
Host string
User string
Fingerprint string
}
// runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui.
// Only supports protocol=gp. Other protocols need their own implementations.
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*gpSamlAuthResult, error) {
if gateway == "" {
return nil, fmt.Errorf("GP SAML auth: gateway is empty")
}
if protocol != "gp" {
return nil, fmt.Errorf("only GlobalProtect (protocol=gp) SAML is supported, got: %s", protocol)
}
log.Infof("[GP-SAML] Starting GlobalProtect SAML authentication with gp-saml-gui for gateway=%s", gateway)
gpSamlPath, err := exec.LookPath("gp-saml-gui")
if err != nil {
return nil, fmt.Errorf("GlobalProtect SAML requires gp-saml-gui (install: pip install gp-saml-gui): %w", err)
}
args := []string{
"--gateway",
"--allow-insecure-crypto",
gateway,
}
cmd := exec.CommandContext(ctx, gpSamlPath, args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to create stdout pipe: %w", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to create stderr pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to start gp-saml-gui: %w", err)
}
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
log.Debugf("[GP-SAML] gp-saml-gui: %s", scanner.Text())
}
}()
result := &gpSamlAuthResult{Host: gateway}
var allOutput []string
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
allOutput = append(allOutput, line)
log.Infof("[GP-SAML] stdout: %s", line)
switch {
case strings.HasPrefix(line, "COOKIE="):
result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE="))
case strings.HasPrefix(line, "HOST="):
result.Host = unshellQuote(strings.TrimPrefix(line, "HOST="))
case strings.HasPrefix(line, "USER="):
result.User = unshellQuote(strings.TrimPrefix(line, "USER="))
case strings.HasPrefix(line, "FINGERPRINT="):
result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT="))
default:
parseGPSamlFromCommandLine(line, result)
}
}
if err := cmd.Wait(); err != nil {
if ctx.Err() != nil {
return nil, fmt.Errorf("GP SAML auth timed out or was cancelled: %w", ctx.Err())
}
if result.Cookie == "" {
return nil, fmt.Errorf("GP SAML auth failed: %w (output: %s)", err, strings.Join(allOutput, "\n"))
}
log.Warnf("[GP-SAML] gp-saml-gui exited with error but cookie was captured: %v", err)
}
if result.Cookie == "" {
return nil, fmt.Errorf("GP SAML auth: no cookie in gp-saml-gui output")
}
log.Infof("[GP-SAML] Got prelogin-cookie from gp-saml-gui, converting to openconnect cookie via --authenticate")
// Convert prelogin-cookie to full openconnect cookie format
ocResult, err := convertGPPreloginCookie(ctx, gateway, result.Cookie, result.User)
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to convert prelogin-cookie: %w", err)
}
result.Cookie = ocResult.Cookie
result.Host = ocResult.Host
result.Fingerprint = ocResult.Fingerprint
log.Infof("[GP-SAML] Authentication successful: user=%s, host=%s, cookie_len=%d, has_fingerprint=%v",
result.User, result.Host, len(result.Cookie), result.Fingerprint != "")
return result, nil
}
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*gpSamlAuthResult, error) {
ocPath, err := exec.LookPath("openconnect")
if err != nil {
return nil, fmt.Errorf("openconnect not found: %w", err)
}
args := []string{
"--protocol=gp",
"--usergroup=gateway:prelogin-cookie",
"--user=" + user,
"--passwd-on-stdin",
"--allow-insecure-crypto",
"--authenticate",
gateway,
}
cmd := exec.CommandContext(ctx, ocPath, args...)
cmd.Stdin = strings.NewReader(preloginCookie)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("openconnect --authenticate failed: %w\noutput: %s", err, string(output))
}
result := &gpSamlAuthResult{}
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
switch {
case strings.HasPrefix(line, "COOKIE="):
result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE="))
case strings.HasPrefix(line, "HOST="):
result.Host = unshellQuote(strings.TrimPrefix(line, "HOST="))
case strings.HasPrefix(line, "FINGERPRINT="):
result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT="))
case strings.HasPrefix(line, "CONNECT_URL="):
connectURL := unshellQuote(strings.TrimPrefix(line, "CONNECT_URL="))
if connectURL != "" && result.Host == "" {
result.Host = connectURL
}
}
}
if result.Cookie == "" {
return nil, fmt.Errorf("no COOKIE in openconnect --authenticate output: %s", string(output))
}
log.Infof("[GP-SAML] openconnect --authenticate: cookie_len=%d, host=%s, fingerprint=%s",
len(result.Cookie), result.Host, result.Fingerprint)
return result, nil
}
func unshellQuote(s string) string {
if len(s) >= 2 {
if (s[0] == '\'' && s[len(s)-1] == '\'') ||
(s[0] == '"' && s[len(s)-1] == '"') {
return s[1 : len(s)-1]
}
}
return s
}
func parseGPSamlFromCommandLine(line string, result *gpSamlAuthResult) {
if !strings.Contains(line, "openconnect") {
return
}
for _, part := range strings.Fields(line) {
switch {
case strings.HasPrefix(part, "--cookie="):
if result.Cookie == "" {
result.Cookie = strings.TrimPrefix(part, "--cookie=")
}
case strings.HasPrefix(part, "--servercert="):
if result.Fingerprint == "" {
result.Fingerprint = strings.TrimPrefix(part, "--servercert=")
}
case strings.HasPrefix(part, "--user="):
if result.User == "" {
result.User = strings.TrimPrefix(part, "--user=")
}
}
}
}
@@ -0,0 +1,169 @@
package network
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnshellQuote(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "single quoted",
input: "'hello world'",
expected: "hello world",
},
{
name: "double quoted",
input: `"hello world"`,
expected: "hello world",
},
{
name: "unquoted",
input: "hello",
expected: "hello",
},
{
name: "empty single quotes",
input: "''",
expected: "",
},
{
name: "empty double quotes",
input: `""`,
expected: "",
},
{
name: "single quote only",
input: "'",
expected: "'",
},
{
name: "mismatched quotes",
input: "'hello\"",
expected: "'hello\"",
},
{
name: "with special chars",
input: "'cookie=abc123&user=john'",
expected: "cookie=abc123&user=john",
},
{
name: "complex cookie",
input: `'authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100'`,
expected: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := unshellQuote(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseGPSamlFromCommandLine(t *testing.T) {
tests := []struct {
name string
line string
initialResult *gpSamlAuthResult
expectedCookie string
expectedUser string
expectedFP string
}{
{
name: "full openconnect command",
line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john",
initialResult: &gpSamlAuthResult{},
expectedCookie: "AUTH123",
expectedUser: "john",
expectedFP: "pin-sha256:ABC",
},
{
name: "with equals signs in cookie",
line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane",
initialResult: &gpSamlAuthResult{},
expectedCookie: "authcookie=xyz123&portal=GATE",
expectedUser: "jane",
expectedFP: "",
},
{
name: "non-openconnect line",
line: "some other output",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "",
expectedFP: "",
},
{
name: "preserves existing values",
line: "openconnect --user=newuser",
initialResult: &gpSamlAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
expectedCookie: "existing",
expectedUser: "newuser",
expectedFP: "existing-fp",
},
{
name: "only updates empty fields",
line: "openconnect --cookie=NEW --user=NEW",
initialResult: &gpSamlAuthResult{Cookie: "OLD"},
expectedCookie: "OLD",
expectedUser: "NEW",
expectedFP: "",
},
{
name: "real gp-saml-gui output",
line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "john.doe@example.com",
expectedFP: "",
},
{
name: "with server cert flag",
line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "",
expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.initialResult
parseGPSamlFromCommandLine(tt.line, result)
assert.Equal(t, tt.expectedCookie, result.Cookie, "cookie mismatch")
assert.Equal(t, tt.expectedUser, result.User, "user mismatch")
assert.Equal(t, tt.expectedFP, result.Fingerprint, "fingerprint mismatch")
})
}
}
func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
// Simulate gp-saml-gui output with command line suggestion
lines := []string{
"",
"SAML REDIRECT",
"Got SAML Login URL",
"POST to ACS endpoint...",
"Got 'prelogin-cookie': 'FAKE_cookie_12345'",
"openconnect --protocol=gp --user=john.doe@example.com --usergroup=gateway:prelogin-cookie --passwd-on-stdin vpn.example.com",
"",
}
result := &gpSamlAuthResult{}
for _, line := range lines {
parseGPSamlFromCommandLine(line, result)
}
assert.Equal(t, "john.doe@example.com", result.User)
assert.Empty(t, result.Cookie, "cookie should not be parsed from command line")
assert.Empty(t, result.Fingerprint)
}
@@ -212,32 +212,28 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
} }
} }
var forgetSSID string
b.stateMutex.Lock() b.stateMutex.Lock()
defer b.stateMutex.Unlock()
wasConnecting = b.state.IsConnecting wasConnecting = b.state.IsConnecting
connectingSSID = b.state.ConnectingSSID connectingSSID = b.state.ConnectingSSID
if wasConnecting && connectingSSID != "" { if wasConnecting && connectingSSID != "" {
if connected && ssid == connectingSSID { switch {
case connected && ssid == connectingSSID:
log.Infof("[updateWiFiState] Connection successful: %s", ssid) log.Infof("[updateWiFiState] Connection successful: %s", ssid)
b.state.IsConnecting = false b.state.IsConnecting = false
b.state.ConnectingSSID = "" b.state.ConnectingSSID = ""
b.state.LastError = "" b.state.LastError = ""
} else if failed || (disconnected && !connected) { case failed || (disconnected && !connected):
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state) log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
b.state.IsConnecting = false b.state.IsConnecting = false
b.state.ConnectingSSID = "" b.state.ConnectingSSID = ""
b.state.LastError = reasonCode b.state.LastError = reasonCode
// If user cancelled, delete the connection profile that was just created
if reasonCode == errdefs.ErrUserCanceled { if reasonCode == errdefs.ErrUserCanceled {
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", connectingSSID) forgetSSID = connectingSSID
b.stateMutex.Unlock()
if err := b.ForgetWiFiNetwork(connectingSSID); err != nil {
log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err)
}
b.stateMutex.Lock()
} }
b.failedMutex.Lock() b.failedMutex.Lock()
@@ -254,6 +250,15 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
b.state.WiFiBSSID = bssid b.state.WiFiBSSID = bssid
b.state.WiFiSignal = signal b.state.WiFiSignal = signal
b.stateMutex.Unlock()
if forgetSSID != "" {
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", forgetSSID)
if err := b.ForgetWiFiNetwork(forgetSSID); err != nil {
log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err)
}
}
return nil return nil
} }
@@ -304,6 +304,51 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil { if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
return err return err
} }
case "gp_saml":
gateway := vpnData["gateway"]
protocol := vpnData["protocol"]
if protocol != "gp" {
return fmt.Errorf("GlobalProtect SAML authentication only supported for protocol=gp, got: %s", protocol)
}
log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway)
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
authResult, err := b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
samlCancel()
if err != nil {
errMsg := err.Error()
switch {
case strings.Contains(errMsg, "not installed"):
return fmt.Errorf("gp-saml-gui is not installed (required for GlobalProtect SAML/SSO VPN)")
case strings.Contains(errMsg, "timed out") || strings.Contains(errMsg, "cancelled"):
return fmt.Errorf("GlobalProtect SAML authentication timed out — please try again")
case strings.Contains(errMsg, "no cookie"):
return fmt.Errorf("GlobalProtect SAML login did not complete — browser was closed before authentication finished")
case strings.Contains(errMsg, "convert prelogin-cookie"):
return fmt.Errorf("GlobalProtect VPN authentication succeeded but cookie exchange failed: %w", err)
default:
return fmt.Errorf("GlobalProtect SAML authentication failed: %w", err)
}
}
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = &cachedGPSamlCookie{
ConnectionUUID: targetUUID,
Cookie: authResult.Cookie,
Host: authResult.Host,
User: authResult.User,
Fingerprint: authResult.Fingerprint,
}
b.cachedGPSamlMu.Unlock()
if err := targetConn.ClearSecrets(); err != nil {
log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err)
} else {
log.Infof("[ConnectVPN] Cleared stale stored secrets for %s", connName)
}
log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName)
} }
b.stateMutex.Lock() b.stateMutex.Lock()
@@ -339,6 +384,16 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
} }
switch { switch {
case strings.Contains(serviceType, "openconnect"):
protocol := data["protocol"]
if needsExternalBrowserAuth(protocol, data["authtype"], data["username"], data) {
switch protocol {
case "gp":
return "gp_saml"
default:
log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol)
}
}
case strings.Contains(serviceType, "openvpn"): case strings.Contains(serviceType, "openvpn"):
connType := data["connection-type"] connType := data["connection-type"]
username := data["username"] username := data["username"]
@@ -412,16 +467,6 @@ func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkma
} }
data["username"] = username data["username"] = username
if reply.Save && password != "" {
data["password-flags"] = "0"
secs := make(map[string]string)
secs["password"] = password
vpn["secrets"] = dbus.MakeVariant(secs)
log.Infof("[ConnectVPN] Saving username and password to vpn.data")
} else {
log.Infof("[ConnectVPN] Saving username to vpn.data (password will be prompted)")
}
vpn["data"] = dbus.MakeVariant(data) vpn["data"] = dbus.MakeVariant(data)
settings["vpn"] = vpn settings["vpn"] = vpn
@@ -432,7 +477,7 @@ func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkma
} }
log.Infof("[ConnectVPN] Username saved to connection") log.Infof("[ConnectVPN] Username saved to connection")
if password != "" && !reply.Save { if password != "" {
b.cachedVPNCredsMu.Lock() b.cachedVPNCredsMu.Lock()
b.cachedVPNCreds = &cachedVPNCredentials{ b.cachedVPNCreds = &cachedVPNCredentials{
ConnectionUUID: targetUUID, ConnectionUUID: targetUUID,
@@ -614,11 +659,7 @@ func (b *NetworkManagerBackend) ClearVPNCredentials(uuidOrName string) error {
dataMap["password-flags"] = "1" dataMap["password-flags"] = "1"
vpnSettings["data"] = dataMap vpnSettings["data"] = dataMap
} }
vpnSettings["password-flags"] = uint32(1)
} }
settings["vpn-secrets"] = make(map[string]any)
} }
if err := conn.Update(settings); err != nil { if err := conn.Update(settings); err != nil {
@@ -684,10 +725,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "" b.state.LastError = ""
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN on success // Clear cached PKCS11 PIN and SAML cookie on success
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
b.pendingVPNSaveMu.Lock() b.pendingVPNSaveMu.Lock()
pending := b.pendingVPNSave pending := b.pendingVPNSave
@@ -706,10 +750,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "VPN connection failed" b.state.LastError = "VPN connection failed"
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN on failure // Clear cached PKCS11 PIN and SAML cookie on failure
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
return return
} }
} }
@@ -723,10 +770,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "VPN connection failed" b.state.LastError = "VPN connection failed"
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN // Clear cached PKCS11 PIN and SAML cookie
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
} }
} }
+6 -14
View File
@@ -92,21 +92,13 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
return return
} }
registry, err := themes.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
allThemes, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
return
}
themeMap := make(map[string]themes.Theme) themeMap := make(map[string]themes.Theme)
for _, t := range allThemes { if registry, err := themes.NewRegistry(); err == nil {
themeMap[t.ID] = t if allThemes, err := registry.List(); err == nil {
for _, t := range allThemes {
themeMap[t.ID] = t
}
}
} }
result := make([]ThemeInfo, 0, len(installedIDs)) result := make([]ThemeInfo, 0, len(installedIDs))
+17
View File
@@ -1,6 +1,8 @@
package utils package utils
import ( import (
"slices"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -18,3 +20,18 @@ func IsDBusServiceAvailable(busName string) bool {
} }
return owned return owned
} }
func IsDBusServiceActivatable(busName string) bool {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return false
}
defer conn.Close()
obj := conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
var activatable []string
if err := obj.Call("org.freedesktop.DBus.ListActivatableNames", 0).Store(&activatable); err != nil {
return false
}
return slices.Contains(activatable, busName)
}
+37
View File
@@ -0,0 +1,37 @@
package utils
import (
"os"
"strings"
)
func HasGroup(groupName string) bool {
return HasGroupIn(groupName, "/etc/group")
}
func HasGroupIn(groupName, path string) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
return HasGroupData(groupName, string(data))
}
func HasGroupData(groupName, data string) bool {
prefix := groupName + ":"
for line := range strings.SplitSeq(data, "\n") {
if strings.HasPrefix(line, prefix) {
return true
}
}
return false
}
func FindGroupData(data string, candidates ...string) (string, bool) {
for _, candidate := range candidates {
if HasGroupData(candidate, data) {
return candidate, true
}
}
return "", false
}
+142
View File
@@ -0,0 +1,142 @@
package utils
import "testing"
const testGroupData = `root:x:0:brltty,root
sys:x:3:bin,testuser
mem:x:8:
ftp:x:11:
mail:x:12:
log:x:19:
smmsp:x:25:
proc:x:26:
games:x:50:
lock:x:54:
network:x:90:
floppy:x:94:
scanner:x:96:
power:x:98:
nobody:x:65534:
adm:x:999:daemon
wheel:x:998:testuser
utmp:x:997:
audio:x:996:brltty
disk:x:995:
input:x:994:brltty,testuser,greeter
kmem:x:993:
kvm:x:992:libvirt-qemu,qemu,testuser
lp:x:991:cups,testuser
optical:x:990:
render:x:989:
sgx:x:988:
storage:x:987:
tty:x:5:brltty
uucp:x:986:brltty
video:x:985:cosmic-greeter,greeter,testuser
users:x:984:
groups:x:983:
systemd-journal:x:982:
rfkill:x:981:
bin:x:1:daemon
daemon:x:2:bin
http:x:33:
dbus:x:81:
systemd-coredump:x:980:
systemd-network:x:979:
systemd-oom:x:978:
systemd-journal-remote:x:977:
systemd-resolve:x:976:
systemd-timesync:x:975:
tss:x:974:
uuidd:x:973:
alpm:x:972:
polkitd:x:102:
testuser:x:1000:
avahi:x:971:
git:x:970:
nvidia-persistenced:x:143:
i2c:x:969:testuser
seat:x:968:
rtkit:x:133:
brlapi:x:967:brltty
gdm:x:120:
brltty:x:966:
colord:x:965:
flatpak:x:964:
geoclue:x:963:testuser
gnome-remote-desktop:x:962:
saned:x:961:
usbmux:x:140:
cosmic-greeter:x:960:
greeter:x:959:testuser
openvpn:x:958:
nm-openvpn:x:957:
named:x:40:
_talkd:x:956:
keyd:x:955:
cups:x:209:testuser
docker:x:954:testuser
mysql:x:953:
radicale:x:952:
onepassword:x:1001:
nixbld:x:951:nixbld01,nixbld02,nixbld03,nixbld04,nixbld05,nixbld06,nixbld07,nixbld08,nixbld09,nixbld10
virtlogin:x:940:
libvirt:x:939:testuser
libvirt-qemu:x:938:
qemu:x:937:
dnsmasq:x:936:
clock:x:935:
dms-greeter:x:1002:greeter,testuser
pcscd:x:934:
test:x:1003:
empower:x:933:
`
func TestHasGroupData(t *testing.T) {
tests := []struct {
group string
want bool
}{
{"greeter", true},
{"root", true},
{"docker", true},
{"cosmic-greeter", true},
{"dms-greeter", true},
{"nonexistent", false},
{"greet", false},
}
for _, tt := range tests {
if got := HasGroupData(tt.group, testGroupData); got != tt.want {
t.Errorf("HasGroupData(%q) = %v, want %v", tt.group, got, tt.want)
}
}
}
func TestFindGroupData(t *testing.T) {
tests := []struct {
name string
candidates []string
wantGroup string
wantFound bool
}{
{"first match wins", []string{"greeter", "greetd", "_greeter"}, "greeter", true},
{"fallback to second", []string{"greetd", "greeter"}, "greeter", true},
{"none found", []string{"_greetd", "greetd"}, "", false},
{"single match", []string{"docker"}, "docker", true},
}
for _, tt := range tests {
got, found := FindGroupData(testGroupData, tt.candidates...)
if got != tt.wantGroup || found != tt.wantFound {
t.Errorf("%s: FindGroupData(%v) = (%q, %v), want (%q, %v)",
tt.name, tt.candidates, got, found, tt.wantGroup, tt.wantFound)
}
}
}
func TestHasGroupDataEmpty(t *testing.T) {
if HasGroupData("greeter", "") {
t.Error("expected false for empty data")
}
}
+31
View File
@@ -0,0 +1,31 @@
package utils
import (
"fmt"
"os/exec"
"strings"
)
func dconfPath(schema, key string) string {
return "/" + strings.ReplaceAll(schema, ".", "/") + "/" + key
}
// GsettingsGet reads a gsettings value, falling back to dconf read.
func GsettingsGet(schema, key string) (string, error) {
if out, err := exec.Command("gsettings", "get", schema, key).Output(); err == nil {
return strings.TrimSpace(string(out)), nil
}
out, err := exec.Command("dconf", "read", dconfPath(schema, key)).Output()
if err != nil {
return "", fmt.Errorf("gsettings/dconf get failed for %s %s: %w", schema, key, err)
}
return strings.TrimSpace(string(out)), nil
}
// GsettingsSet writes a gsettings value, falling back to dconf write.
func GsettingsSet(schema, key, value string) error {
if err := exec.Command("gsettings", "set", schema, key, value).Run(); err == nil {
return nil
}
return exec.Command("dconf", "write", dconfPath(schema, key), "'"+value+"'").Run()
}
+16
View File
@@ -38,6 +38,22 @@ func XDGConfigHome() string {
return filepath.Join(home, ".config") return filepath.Join(home, ".config")
} }
func EmacsConfigDir() string {
home, _ := os.UserHomeDir()
emacsD := filepath.Join(home, ".emacs.d")
if info, err := os.Stat(emacsD); err == nil && info.IsDir() {
return emacsD
}
xdgEmacs := filepath.Join(XDGConfigHome(), "emacs")
if info, err := os.Stat(xdgEmacs); err == nil && info.IsDir() {
return xdgEmacs
}
return ""
}
func ExpandPath(path string) (string, error) { func ExpandPath(path string) (string, error) {
expanded := os.ExpandEnv(path) expanded := os.ExpandEnv(path)
expanded = filepath.Clean(expanded) expanded = filepath.Clean(expanded)
+3
View File
@@ -71,6 +71,9 @@ in
"hyprland" "hyprland"
"sway" "sway"
"labwc" "labwc"
"mango"
"scroll"
"miracle"
]; ];
description = "Compositor to run greeter in"; description = "Compositor to run greeter in";
}; };
+1
View File
@@ -50,5 +50,6 @@ in
services.power-profiles-daemon.enable = lib.mkDefault true; services.power-profiles-daemon.enable = lib.mkDefault true;
services.accounts-daemon.enable = lib.mkDefault true; services.accounts-daemon.enable = lib.mkDefault true;
security.polkit.enable = lib.mkDefault true;
}; };
} }
+2 -1
View File
@@ -48,6 +48,7 @@
sonnet sonnet
qtmultimedia qtmultimedia
qtimageformats qtimageformats
kimageformats
]; ];
in in
{ {
@@ -79,7 +80,7 @@
inherit version; inherit version;
pname = "dms-shell"; pname = "dms-shell";
src = ./core; src = ./core;
vendorHash = "sha256-vsfCgpilOHzJbTaJjJfMK/cSvtyFYJsPDjY4m3iuoFg="; vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
+8 -8
View File
@@ -12,7 +12,7 @@ This file provides guidance to AI coding assistants.
* ALWAYS prefer editing an existing file to creating a new one. * ALWAYS prefer editing an existing file to creating a new one.
* NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. * NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
* When you update or modify core context files, also update markdown documentation and memory bank * When you update or modify core context files, also update markdown documentation and memory bank
* When asked to commit changes, exclude CLAUDE.md and CLAUDE-*.md referenced memory bank system files from any commits. * When asked to commit changes, exclude AGENTS.md and AGENTS-*.md referenced memory bank system files from any commits.
## Memory Bank System ## Memory Bank System
@@ -20,18 +20,18 @@ This project uses a structured memory bank system with specialized context files
### Core Context Files ### Core Context Files
* **CLAUDE-activeContext.md** - Current session state, goals, and progress (if exists) * **AGENTS-activeContext.md** - Current session state, goals, and progress (if exists)
* **CLAUDE-patterns.md** - Established code patterns and conventions (if exists) * **AGENTS-patterns.md** - Established code patterns and conventions (if exists)
* **CLAUDE-decisions.md** - Architecture decisions and rationale (if exists) * **AGENTS-decisions.md** - Architecture decisions and rationale (if exists)
* **CLAUDE-troubleshooting.md** - Common issues and proven solutions (if exists) * **AGENTS-troubleshooting.md** - Common issues and proven solutions (if exists)
* **CLAUDE-config-variables.md** - Configuration variables reference (if exists) * **AGENTS-config-variables.md** - Configuration variables reference (if exists)
* **CLAUDE-temp.md** - Temporary scratch pad (only read when referenced) * **AGENTS-temp.md** - Temporary scratch pad (only read when referenced)
**Important:** Always reference the active context file first to understand what's currently being worked on and maintain session continuity. **Important:** Always reference the active context file first to understand what's currently being worked on and maintain session continuity.
### Memory Bank System Backups ### Memory Bank System Backups
When asked to backup Memory Bank System files, you will copy the core context files above and @.claude settings directory to directory @/path/to/backup-directory. If files already exist in the backup directory, you will overwrite them. When asked to backup Memory Bank System files, you will copy the core context files above and @.agents settings directory to directory @/path/to/backup-directory. If files already exist in the backup directory, you will overwrite them.
## Project Overview ## Project Overview
@@ -10,6 +10,7 @@ Singleton {
id: root id: root
property var appUsageRanking: {} property var appUsageRanking: {}
property bool _saving: false
Component.onCompleted: { Component.onCompleted: {
loadSettings(); loadSettings();
@@ -59,7 +60,9 @@ Singleton {
} }
appUsageRanking = currentRanking; appUsageRanking = currentRanking;
_saving = true;
saveSettings(); saveSettings();
_saving = false;
} }
function getRankedApps() { function getRankedApps() {
@@ -97,7 +100,9 @@ Singleton {
if (hasChanges) { if (hasChanges) {
appUsageRanking = currentRanking; appUsageRanking = currentRanking;
_saving = true;
saveSettings(); saveSettings();
_saving = false;
} }
} }
@@ -109,6 +114,8 @@ Singleton {
blockWrites: true blockWrites: true
watchChanges: true watchChanges: true
onLoaded: { onLoaded: {
if (root._saving)
return;
parseSettings(settingsFile.text()); parseSettings(settingsFile.text());
} }
onLoadFailed: error => {} onLoadFailed: error => {}
+27
View File
@@ -178,6 +178,33 @@ Singleton {
} }
} }
function loadLauncherCache() {
try {
var content = launcherCacheFile.text();
if (content && content.trim())
return JSON.parse(content);
} catch (e) {
console.warn("CacheData: Failed to parse launcher cache:", e.message);
}
return null;
}
function saveLauncherCache(sections) {
if (_loading)
return;
launcherCacheFile.setText(JSON.stringify(sections));
}
FileView {
id: launcherCacheFile
path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/launcher_cache.json"
blockLoading: true
blockWrites: true
atomicWrites: true
watchChanges: false
}
FileView { FileView {
id: cacheFile id: cacheFile
+9
View File
@@ -0,0 +1,9 @@
import QtQuick
import qs.Common
// Reusable NumberAnimation wrapper
NumberAnimation {
duration: Theme.expressiveDurations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}
+9
View File
@@ -0,0 +1,9 @@
import QtQuick
import qs.Common
// Reusable ColorAnimation wrapper
ColorAnimation {
duration: Theme.expressiveDurations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}
+284 -265
View File
@@ -57,6 +57,8 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call audio decrement 1", label: "Volume Down (1%)" }, { id: "spawn dms ipc call audio decrement 1", label: "Volume Down (1%)" },
{ id: "spawn dms ipc call audio decrement 5", label: "Volume Down (5%)" }, { id: "spawn dms ipc call audio decrement 5", label: "Volume Down (5%)" },
{ id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" }, { id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" },
{ id: "spawn dms ipc call mpris increment 5", label: "Player Volume Up (5%)" },
{ id: "spawn dms ipc call mpris decrement 5", label: "Player Volume Down (5%)" },
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" }, { id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
{ id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" }, { id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" },
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" }, { id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
@@ -121,7 +123,8 @@ const NIRI_ACTIONS = {
{ id: "expand-column-to-available-width", label: "Expand to Available Width" }, { id: "expand-column-to-available-width", label: "Expand to Available Width" },
{ id: "consume-or-expel-window-left", label: "Consume/Expel Left" }, { id: "consume-or-expel-window-left", label: "Consume/Expel Left" },
{ id: "consume-or-expel-window-right", label: "Consume/Expel Right" }, { id: "consume-or-expel-window-right", label: "Consume/Expel Right" },
{ id: "toggle-column-tabbed-display", label: "Toggle Tabbed" } { id: "toggle-column-tabbed-display", label: "Toggle Tabbed" },
{ id: "toggle-window-rule-opacity", label: "Toggle Window Opacity" }
], ],
"Focus": [ "Focus": [
{ id: "focus-column-left", label: "Focus Left" }, { id: "focus-column-left", label: "Focus Left" },
@@ -168,6 +171,7 @@ const NIRI_ACTIONS = {
"System": [ "System": [
{ id: "toggle-overview", label: "Toggle Overview" }, { id: "toggle-overview", label: "Toggle Overview" },
{ id: "show-hotkey-overlay", label: "Show Hotkey Overlay" }, { id: "show-hotkey-overlay", label: "Show Hotkey Overlay" },
{ id: "do-screen-transition", label: "Screen Transition" },
{ id: "power-off-monitors", label: "Power Off Monitors" }, { id: "power-off-monitors", label: "Power Off Monitors" },
{ id: "power-on-monitors", label: "Power On Monitors" }, { id: "power-on-monitors", label: "Power On Monitors" },
{ id: "toggle-keyboard-shortcuts-inhibit", label: "Toggle Shortcuts Inhibit" }, { id: "toggle-keyboard-shortcuts-inhibit", label: "Toggle Shortcuts Inhibit" },
@@ -420,6 +424,12 @@ const COMPOSITOR_ACTIONS = {
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Tags", "Window", "Move/Resize", "Focus", "Move", "Layout", "Groups", "Monitor", "Scratchpad", "Screenshot", "System", "Pass-through", "Overview", "Alt-Tab", "Other"]; const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Tags", "Window", "Move/Resize", "Focus", "Move", "Layout", "Groups", "Monitor", "Scratchpad", "Screenshot", "System", "Pass-through", "Overview", "Alt-Tab", "Other"];
const NIRI_ACTION_ARGS = { const NIRI_ACTION_ARGS = {
"quit": {
args: [{ name: "skip-confirmation", type: "bool", label: "Skip confirmation" }]
},
"do-screen-transition": {
args: [{ name: "delay-ms", type: "number", label: "Delay (ms)", placeholder: "250" }]
},
"set-column-width": { "set-column-width": {
args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }] args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }]
}, },
@@ -711,6 +721,14 @@ const DMS_ACTION_ARGS = {
base: "spawn dms ipc call audio decrement", base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }] args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
}, },
"player increment": {
base: "spawn dms ipc call mpris increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
},
"player decrement": {
base: "spawn dms ipc call mpris decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "5" }]
},
"brightness increment": { "brightness increment": {
base: "spawn dms ipc call brightness increment", base: "spawn dms ipc call brightness increment",
args: [ args: [
@@ -756,14 +774,14 @@ function getDmsActions(isNiri, isHyprland) {
continue; continue;
} }
switch (action.compositor) { switch (action.compositor) {
case "niri": case "niri":
if (isNiri) if (isNiri)
result.push(action); result.push(action);
break; break;
case "hyprland": case "hyprland":
if (isHyprland) if (isHyprland)
result.push(action); result.push(action);
break; break;
} }
} }
return result; return result;
@@ -856,13 +874,13 @@ function isValidAction(action) {
if (!action) if (!action)
return false; return false;
switch (action) { switch (action) {
case "spawn": case "spawn":
case "spawn ": case "spawn ":
case "spawn sh -c \"\"": case "spawn sh -c \"\"":
case "spawn sh -c ''": case "spawn sh -c ''":
case "spawn_shell": case "spawn_shell":
case "spawn_shell ": case "spawn_shell ":
return false; return false;
} }
return true; return true;
} }
@@ -882,7 +900,7 @@ function buildSpawnAction(command, args) {
return ""; return "";
let parts = [command]; let parts = [command];
if (args && args.length > 0) if (args && args.length > 0)
parts = parts.concat(args.filter(function(a) { return a; })); parts = parts.concat(args.filter(function (a) { return a; }));
return "spawn " + parts.join(" "); return "spawn " + parts.join(" ");
} }
@@ -899,7 +917,7 @@ function parseSpawnCommand(action) {
if (!action || !action.startsWith("spawn ")) if (!action || !action.startsWith("spawn "))
return { command: "", args: [] }; return { command: "", args: [] };
const rest = action.slice(6); const rest = action.slice(6);
const parts = rest.split(" ").filter(function(p) { return p; }); const parts = rest.split(" ").filter(function (p) { return p; });
return { return {
command: parts[0] || "", command: parts[0] || "",
args: parts.slice(1) args: parts.slice(1)
@@ -961,130 +979,138 @@ function parseCompositorActionArgs(compositor, action) {
var argParts = parts.slice(1); var argParts = parts.slice(1);
switch (compositor) { switch (compositor) {
case "niri": case "niri":
switch (base) { switch (base) {
case "move-column-to-workspace": case "move-column-to-workspace":
for (var i = 0; i < argParts.length; i++) { for (var i = 0; i < argParts.length; i++) {
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") { if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
args.focus = argParts[i] === "focus=true"; args.focus = argParts[i] === "focus=true";
} else if (!args.index) { } else if (!args.index) {
args.index = argParts[i]; args.index = argParts[i];
}
}
break;
case "move-column-to-workspace-down":
case "move-column-to-workspace-up":
for (var k = 0; k < argParts.length; k++) {
if (argParts[k] === "focus=true" || argParts[k] === "focus=false")
args.focus = argParts[k] === "focus=true";
}
break;
default:
for (var j = 0; j < argParts.length; j++) {
var kv = argParts[j].split("=");
if (kv.length === 2) {
switch (kv[1]) {
case "true":
args[kv[0]] = true;
break;
case "false":
args[kv[0]] = false;
break;
default:
args[kv[0]] = kv[1];
}
} else {
args.value = args.value ? (args.value + " " + argParts[j]) : argParts[j];
}
}
}
break;
case "mangowc":
if (argConfig.args && argConfig.args.length > 0 && argParts.length > 0) {
var paramStr = argParts.join(" ");
var paramValues = paramStr.split(",");
for (var m = 0; m < argConfig.args.length && m < paramValues.length; m++) {
args[argConfig.args[m].name] = paramValues[m];
} }
} }
break; break;
case "move-column-to-workspace-down": case "hyprland":
case "move-column-to-workspace-up": if (argConfig.args && argConfig.args.length > 0) {
for (var k = 0; k < argParts.length; k++) { switch (base) {
if (argParts[k] === "focus=true" || argParts[k] === "focus=false") case "resizewindowpixel":
args.focus = argParts[k] === "focus=true"; case "movewindowpixel":
var commaIdx = argParts.join(" ").indexOf(",");
if (commaIdx !== -1) {
var fullStr = argParts.join(" ");
args[argConfig.args[0].name] = fullStr.substring(0, commaIdx);
args[argConfig.args[1].name] = fullStr.substring(commaIdx + 1);
} else if (argParts.length > 0) {
args[argConfig.args[0].name] = argParts.join(" ");
}
break;
case "movetoworkspace":
case "movetoworkspacesilent":
case "tagwindow":
case "alterzorder":
if (argParts.length >= 2) {
args[argConfig.args[0].name] = argParts[0];
args[argConfig.args[1].name] = argParts.slice(1).join(" ");
} else if (argParts.length === 1) {
args[argConfig.args[0].name] = argParts[0];
}
break;
case "moveworkspacetomonitor":
case "swapactiveworkspaces":
case "renameworkspace":
case "fullscreenstate":
case "movecursor":
if (argParts.length >= 2) {
args[argConfig.args[0].name] = argParts[0];
args[argConfig.args[1].name] = argParts[1];
} else if (argParts.length === 1) {
args[argConfig.args[0].name] = argParts[0];
}
break;
case "setprop":
if (argParts.length >= 3) {
args.window = argParts[0];
args.property = argParts[1];
args.value = argParts.slice(2).join(" ");
} else if (argParts.length === 2) {
args.window = argParts[0];
args.property = argParts[1];
}
break;
case "sendshortcut":
if (argParts.length >= 3) {
args.mod = argParts[0];
args.key = argParts[1];
args.window = argParts.slice(2).join(" ");
} else if (argParts.length >= 2) {
args.mod = argParts[0];
args.key = argParts[1];
}
break;
case "sendkeystate":
if (argParts.length >= 4) {
args.mod = argParts[0];
args.key = argParts[1];
args.state = argParts[2];
args.window = argParts.slice(3).join(" ");
}
break;
case "signalwindow":
if (argParts.length >= 2) {
args.window = argParts[0];
args.signal = argParts[1];
}
break;
default:
if (argParts.length > 0) {
if (argConfig.args.length === 1) {
args[argConfig.args[0].name] = argParts.join(" ");
} else {
args.value = argParts.join(" ");
}
}
}
} }
break; break;
default: default:
if (base.startsWith("screenshot")) { if (argParts.length > 0)
for (var j = 0; j < argParts.length; j++) {
var kv = argParts[j].split("=");
if (kv.length === 2)
args[kv[0]] = kv[1] === "true";
}
} else if (argParts.length > 0) {
args.value = argParts.join(" "); args.value = argParts.join(" ");
}
}
break;
case "mangowc":
if (argConfig.args && argConfig.args.length > 0 && argParts.length > 0) {
var paramStr = argParts.join(" ");
var paramValues = paramStr.split(",");
for (var m = 0; m < argConfig.args.length && m < paramValues.length; m++) {
args[argConfig.args[m].name] = paramValues[m];
}
}
break;
case "hyprland":
if (argConfig.args && argConfig.args.length > 0) {
switch (base) {
case "resizewindowpixel":
case "movewindowpixel":
var commaIdx = argParts.join(" ").indexOf(",");
if (commaIdx !== -1) {
var fullStr = argParts.join(" ");
args[argConfig.args[0].name] = fullStr.substring(0, commaIdx);
args[argConfig.args[1].name] = fullStr.substring(commaIdx + 1);
} else if (argParts.length > 0) {
args[argConfig.args[0].name] = argParts.join(" ");
}
break;
case "movetoworkspace":
case "movetoworkspacesilent":
case "tagwindow":
case "alterzorder":
if (argParts.length >= 2) {
args[argConfig.args[0].name] = argParts[0];
args[argConfig.args[1].name] = argParts.slice(1).join(" ");
} else if (argParts.length === 1) {
args[argConfig.args[0].name] = argParts[0];
}
break;
case "moveworkspacetomonitor":
case "swapactiveworkspaces":
case "renameworkspace":
case "fullscreenstate":
case "movecursor":
if (argParts.length >= 2) {
args[argConfig.args[0].name] = argParts[0];
args[argConfig.args[1].name] = argParts[1];
} else if (argParts.length === 1) {
args[argConfig.args[0].name] = argParts[0];
}
break;
case "setprop":
if (argParts.length >= 3) {
args.window = argParts[0];
args.property = argParts[1];
args.value = argParts.slice(2).join(" ");
} else if (argParts.length === 2) {
args.window = argParts[0];
args.property = argParts[1];
}
break;
case "sendshortcut":
if (argParts.length >= 3) {
args.mod = argParts[0];
args.key = argParts[1];
args.window = argParts.slice(2).join(" ");
} else if (argParts.length >= 2) {
args.mod = argParts[0];
args.key = argParts[1];
}
break;
case "sendkeystate":
if (argParts.length >= 4) {
args.mod = argParts[0];
args.key = argParts[1];
args.state = argParts[2];
args.window = argParts.slice(3).join(" ");
}
break;
case "signalwindow":
if (argParts.length >= 2) {
args.window = argParts[0];
args.signal = argParts[1];
}
break;
default:
if (argParts.length > 0) {
if (argConfig.args.length === 1) {
args[argConfig.args[0].name] = argParts.join(" ");
} else {
args.value = argParts.join(" ");
}
}
}
}
break;
default:
if (argParts.length > 0)
args.value = argParts.join(" ");
} }
return { base: base, args: args }; return { base: base, args: args };
@@ -1100,125 +1126,118 @@ function buildCompositorAction(compositor, base, args) {
return base; return base;
switch (compositor) { switch (compositor) {
case "niri": case "niri":
switch (base) { switch (base) {
case "move-column-to-workspace": case "move-column-to-workspace":
if (args.index) if (args.index)
parts.push(args.index); parts.push(args.index);
if (args.focus === false) if (args.focus === false)
parts.push("focus=false"); parts.push("focus=false");
break;
case "move-column-to-workspace-down":
case "move-column-to-workspace-up":
if (args.focus === false)
parts.push("focus=false");
break;
default:
if (args.value)
parts.push(args.value);
else if (args.index)
parts.push(args.index);
for (var prop in args) {
switch (prop) {
case "value":
case "index":
continue;
}
var val = args[prop];
if (val === true)
parts.push(prop + "=true");
else if (val === false)
parts.push(prop + "=false");
else if (val !== undefined && val !== null && val !== "")
parts.push(prop + "=" + val);
}
}
break; break;
case "move-column-to-workspace-down": case "mangowc":
case "move-column-to-workspace-up": var compositorArgs = ACTION_ARGS.mangowc;
if (args.focus === false) if (compositorArgs && compositorArgs[base] && compositorArgs[base].args) {
parts.push("focus=false"); var argConfig = compositorArgs[base].args;
var argValues = [];
for (var i = 0; i < argConfig.length; i++) {
var argDef = argConfig[i];
var val = args[argDef.name];
if (val === undefined || val === "")
val = argDef.default || "";
if (val === "" && argValues.length === 0)
continue;
argValues.push(val);
}
if (argValues.length > 0)
parts.push(argValues.join(","));
} else if (args.value) {
parts.push(args.value);
}
break;
case "hyprland":
var hyprArgs = ACTION_ARGS.hyprland;
if (hyprArgs && hyprArgs[base] && hyprArgs[base].args) {
var hyprConfig = hyprArgs[base].args;
switch (base) {
case "resizewindowpixel":
case "movewindowpixel":
if (args[hyprConfig[0].name])
parts.push(args[hyprConfig[0].name]);
if (args[hyprConfig[1].name])
parts[parts.length - 1] += "," + args[hyprConfig[1].name];
break;
case "setprop":
if (args.window)
parts.push(args.window);
if (args.property)
parts.push(args.property);
if (args.value)
parts.push(args.value);
break;
case "sendshortcut":
if (args.mod)
parts.push(args.mod);
if (args.key)
parts.push(args.key);
if (args.window)
parts.push(args.window);
break;
case "sendkeystate":
if (args.mod)
parts.push(args.mod);
if (args.key)
parts.push(args.key);
if (args.state)
parts.push(args.state);
if (args.window)
parts.push(args.window);
break;
case "signalwindow":
if (args.window)
parts.push(args.window);
if (args.signal)
parts.push(args.signal);
break;
default:
for (var j = 0; j < hyprConfig.length; j++) {
var hVal = args[hyprConfig[j].name];
if (hVal !== undefined && hVal !== "")
parts.push(hVal);
}
}
} else if (args.value) {
parts.push(args.value);
}
break; break;
default: default:
switch (base) { if (args.value)
case "screenshot":
if (args["show-pointer"] === true)
parts.push("show-pointer=true");
else if (args["show-pointer"] === false)
parts.push("show-pointer=false");
break;
case "screenshot-screen":
if (args["show-pointer"] === true)
parts.push("show-pointer=true");
else if (args["show-pointer"] === false)
parts.push("show-pointer=false");
if (args["write-to-disk"] === true)
parts.push("write-to-disk=true");
break;
case "screenshot-window":
if (args["write-to-disk"] === true)
parts.push("write-to-disk=true");
break;
}
if (args.value) {
parts.push(args.value); parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
}
}
break;
case "mangowc":
var compositorArgs = ACTION_ARGS.mangowc;
if (compositorArgs && compositorArgs[base] && compositorArgs[base].args) {
var argConfig = compositorArgs[base].args;
var argValues = [];
for (var i = 0; i < argConfig.length; i++) {
var argDef = argConfig[i];
var val = args[argDef.name];
if (val === undefined || val === "")
val = argDef.default || "";
if (val === "" && argValues.length === 0)
continue;
argValues.push(val);
}
if (argValues.length > 0)
parts.push(argValues.join(","));
} else if (args.value) {
parts.push(args.value);
}
break;
case "hyprland":
var hyprArgs = ACTION_ARGS.hyprland;
if (hyprArgs && hyprArgs[base] && hyprArgs[base].args) {
var hyprConfig = hyprArgs[base].args;
switch (base) {
case "resizewindowpixel":
case "movewindowpixel":
if (args[hyprConfig[0].name])
parts.push(args[hyprConfig[0].name]);
if (args[hyprConfig[1].name])
parts[parts.length - 1] += "," + args[hyprConfig[1].name];
break;
case "setprop":
if (args.window)
parts.push(args.window);
if (args.property)
parts.push(args.property);
if (args.value)
parts.push(args.value);
break;
case "sendshortcut":
if (args.mod)
parts.push(args.mod);
if (args.key)
parts.push(args.key);
if (args.window)
parts.push(args.window);
break;
case "sendkeystate":
if (args.mod)
parts.push(args.mod);
if (args.key)
parts.push(args.key);
if (args.state)
parts.push(args.state);
if (args.window)
parts.push(args.window);
break;
case "signalwindow":
if (args.window)
parts.push(args.window);
if (args.signal)
parts.push(args.signal);
break;
default:
for (var j = 0; j < hyprConfig.length; j++) {
var hVal = args[hyprConfig[j].name];
if (hVal !== undefined && hVal !== "")
parts.push(hVal);
}
}
} else if (args.value) {
parts.push(args.value);
}
break;
default:
if (args.value)
parts.push(args.value);
} }
return parts.join(" "); return parts.join(" ");
@@ -1246,22 +1265,22 @@ function parseDmsActionArgs(action) {
for (var i = 0; i < rest.length; i++) { for (var i = 0; i < rest.length; i++) {
var c = rest[i]; var c = rest[i];
switch (c) { switch (c) {
case '"': case '"':
inQuotes = !inQuotes; inQuotes = !inQuotes;
hadQuotes = true; hadQuotes = true;
break; break;
case ' ': case ' ':
if (inQuotes) { if (inQuotes) {
current += c;
} else if (current || hadQuotes) {
tokens.push(current);
current = "";
hadQuotes = false;
}
break;
default:
current += c; current += c;
} else if (current || hadQuotes) { break;
tokens.push(current);
current = "";
hadQuotes = false;
}
break;
default:
current += c;
break;
} }
} }
if (current || hadQuotes) if (current || hadQuotes)
+46
View File
@@ -0,0 +1,46 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
// Reusable ListView/GridView transitions
Singleton {
id: root
readonly property Transition add: Transition {
DankAnim {
property: "opacity"
from: 0
to: 1
duration: Theme.expressiveDurations.expressiveEffects
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
readonly property Transition remove: Transition {
DankAnim {
property: "opacity"
to: 0
duration: Theme.expressiveDurations.fast
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
}
}
readonly property Transition displaced: Transition {
DankAnim {
property: "y"
duration: Theme.expressiveDurations.normal
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
readonly property Transition move: Transition {
DankAnim {
property: "y"
duration: Theme.expressiveDurations.normal
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
}
+14
View File
@@ -9,6 +9,20 @@ Singleton {
property var currentOSDsByScreen: ({}) property var currentOSDsByScreen: ({})
Connections {
target: Quickshell
function onScreensChanged() {
const activeNames = {};
for (let i = 0; i < Quickshell.screens.length; i++)
activeNames[Quickshell.screens[i].name] = true;
for (const screenName in osdManager.currentOSDsByScreen) {
if (activeNames[screenName])
continue;
osdManager.currentOSDsByScreen[screenName] = null;
}
}
}
function showOSD(osd) { function showOSD(osd) {
if (!osd || !osd.screen) if (!osd || !osd.screen)
return; return;
+4 -1
View File
@@ -80,7 +80,10 @@ Singleton {
return Quickshell.iconPath(moddedId, true); return Quickshell.iconPath(moddedId, true);
} }
return desktopEntry && desktopEntry.icon ? Quickshell.iconPath(desktopEntry.icon, true) : ""; if (desktopEntry && desktopEntry.icon) {
return Quickshell.iconPath(desktopEntry.icon, true);
}
return Quickshell.iconPath(appId, true);
} }
function getAppName(appId: string, desktopEntry: var): string { function getAppName(appId: string, desktopEntry: var): string {
+103 -39
View File
@@ -58,6 +58,7 @@ Singleton {
property string wallpaperPathDark: "" property string wallpaperPathDark: ""
property var monitorWallpapersLight: ({}) property var monitorWallpapersLight: ({})
property var monitorWallpapersDark: ({}) property var monitorWallpapersDark: ({})
property var monitorWallpaperFillModes: ({})
property string wallpaperTransition: "fade" property string wallpaperTransition: "fade"
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"] readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none") property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
@@ -121,6 +122,10 @@ Singleton {
property string vpnLastConnected: "" property string vpnLastConnected: ""
property var deviceMaxVolumes: ({})
property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: []
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
loadSettings(); loadSettings();
@@ -1052,6 +1057,49 @@ Singleton {
saveSettings(); saveSettings();
} }
function setDeviceMaxVolume(nodeName, maxPercent) {
if (!nodeName)
return;
const updated = Object.assign({}, deviceMaxVolumes);
const clamped = Math.max(100, Math.min(200, Math.round(maxPercent)));
if (clamped === 100) {
delete updated[nodeName];
} else {
updated[nodeName] = clamped;
}
deviceMaxVolumes = updated;
saveSettings();
}
function setHiddenOutputDeviceNames(deviceNames) {
if (!Array.isArray(deviceNames))
return;
hiddenOutputDeviceNames = deviceNames;
saveSettings();
}
function setHiddenInputDeviceNames(deviceNames) {
if (!Array.isArray(deviceNames))
return;
hiddenInputDeviceNames = deviceNames;
saveSettings();
}
function getDeviceMaxVolume(nodeName) {
if (!nodeName)
return 100;
return deviceMaxVolumes[nodeName] ?? 100;
}
function removeDeviceMaxVolume(nodeName) {
if (!nodeName)
return;
const updated = Object.assign({}, deviceMaxVolumes);
delete updated[nodeName];
deviceMaxVolumes = updated;
saveSettings();
}
function syncWallpaperForCurrentMode() { function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) if (!perModeWallpaper)
return; return;
@@ -1063,11 +1111,7 @@ Singleton {
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark; wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark;
} }
function getMonitorWallpaper(screenName) { function _findMonitorValue(map, screenName) {
if (!perMonitorWallpaper) {
return wallpaperPath;
}
var screen = null; var screen = null;
var screens = Quickshell.screens; var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) { for (var i = 0; i < screens.length; i++) {
@@ -1077,52 +1121,72 @@ Singleton {
} }
} }
if (!screen) { if (!screen)
return monitorWallpapers[screenName] || wallpaperPath; return map[screenName];
if (map[screen.name] !== undefined)
return map[screen.name];
if (screen.model && map[screen.model] !== undefined)
return map[screen.model];
if (typeof SettingsData !== "undefined") {
var displayName = SettingsData.getScreenDisplayName(screen);
if (displayName && map[displayName] !== undefined)
return map[displayName];
}
return undefined;
}
function getMonitorWallpaper(screenName) {
if (!perMonitorWallpaper)
return wallpaperPath;
var value = _findMonitorValue(monitorWallpapers, screenName);
return value !== undefined ? value : wallpaperPath;
}
function getMonitorWallpaperFillMode(screenName) {
var globalFillMode = (typeof SettingsData !== "undefined") ? SettingsData.wallpaperFillMode : "Fill";
if (!perMonitorWallpaper)
return globalFillMode;
var value = _findMonitorValue(monitorWallpaperFillModes, screenName);
return value !== undefined ? value : globalFillMode;
}
function setMonitorWallpaperFillMode(screenName, mode) {
var screen = null;
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i];
break;
}
} }
if (monitorWallpapers[screen.name]) { if (!screen)
return monitorWallpapers[screen.name]; return;
}
if (screen.model && monitorWallpapers[screen.model]) { var identifier = typeof SettingsData !== "undefined" ? SettingsData.getScreenDisplayName(screen) : screen.name;
return monitorWallpapers[screen.model];
var newModes = {};
for (var key in monitorWallpaperFillModes) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model);
if (!isThisScreen)
newModes[key] = monitorWallpaperFillModes[key];
} }
return wallpaperPath; newModes[identifier] = mode;
monitorWallpaperFillModes = newModes;
saveSettings();
} }
function getMonitorCyclingSettings(screenName) { function getMonitorCyclingSettings(screenName) {
var screen = null; var defaults = {
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i];
break;
}
}
if (!screen) {
return monitorCyclingSettings[screenName] || {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
if (monitorCyclingSettings[screen.name]) {
return monitorCyclingSettings[screen.name];
}
if (screen.model && monitorCyclingSettings[screen.model]) {
return monitorCyclingSettings[screen.model];
}
return {
"enabled": false, "enabled": false,
"mode": "interval", "mode": "interval",
"interval": 300, "interval": 300,
"time": "06:00" "time": "06:00"
}; };
var value = _findMonitorValue(monitorCyclingSettings, screenName);
return value !== undefined ? value : defaults;
} }
FileView { FileView {
+243 -8
View File
@@ -60,6 +60,7 @@ Singleton {
property bool _hasLoaded: false property bool _hasLoaded: false
property bool _isReadOnly: false property bool _isReadOnly: false
property bool _hasUnsavedChanges: false property bool _hasUnsavedChanges: false
property bool _selfWrite: false
property var _loadedSettingsSnapshot: null property var _loadedSettingsSnapshot: null
property var pluginSettings: ({}) property var pluginSettings: ({})
property var builtInPluginSettings: ({}) property var builtInPluginSettings: ({})
@@ -79,6 +80,8 @@ Singleton {
saveSettings(); saveSettings();
} }
property bool clipboardEnterToPaste: false
property var launcherPluginVisibility: ({}) property var launcherPluginVisibility: ({})
function getPluginAllowWithoutTrigger(pluginId) { function getPluginAllowWithoutTrigger(pluginId) {
@@ -134,6 +137,7 @@ Singleton {
property string widgetBackgroundColor: "sch" property string widgetBackgroundColor: "sch"
property string widgetColorMode: "default" property string widgetColorMode: "default"
property string controlCenterTileColorMode: "primary" property string controlCenterTileColorMode: "primary"
property string buttonColorMode: "primary"
property real cornerRadius: 12 property real cornerRadius: 12
property int niriLayoutGapsOverride: -1 property int niriLayoutGapsOverride: -1
property int niriLayoutRadiusOverride: -1 property int niriLayoutRadiusOverride: -1
@@ -153,6 +157,14 @@ Singleton {
property bool nightModeEnabled: false property bool nightModeEnabled: false
property int animationSpeed: SettingsData.AnimationSpeed.Short property int animationSpeed: SettingsData.AnimationSpeed.Short
property int customAnimationDuration: 500 property int customAnimationDuration: 500
property bool syncComponentAnimationSpeeds: true
onSyncComponentAnimationSpeedsChanged: saveSettings()
property int popoutAnimationSpeed: SettingsData.AnimationSpeed.Short
property int popoutCustomAnimationDuration: 150
property int modalAnimationSpeed: SettingsData.AnimationSpeed.Short
property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings()
property string wallpaperFillMode: "Fill" property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false property bool blurWallpaperOnOverview: false
@@ -241,6 +253,7 @@ Singleton {
property bool showWorkspacePadding: false property bool showWorkspacePadding: false
property bool workspaceScrolling: false property bool workspaceScrolling: false
property bool showWorkspaceApps: false property bool showWorkspaceApps: false
property bool workspaceDragReorder: true
property bool groupWorkspaceApps: true property bool groupWorkspaceApps: true
property int maxWorkspaceIcons: 3 property int maxWorkspaceIcons: 3
property int workspaceAppIconSizeOffset: 0 property int workspaceAppIconSizeOffset: 0
@@ -260,6 +273,7 @@ Singleton {
property bool scrollTitleEnabled: true property bool scrollTitleEnabled: true
property bool audioVisualizerEnabled: true property bool audioVisualizerEnabled: true
property string audioScrollMode: "volume" property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5
property bool clockCompactMode: false property bool clockCompactMode: false
property bool focusedWindowCompactMode: false property bool focusedWindowCompactMode: false
property bool runningAppsCompactMode: true property bool runningAppsCompactMode: true
@@ -273,8 +287,9 @@ Singleton {
property int appsDockEnlargePercentage: 125 property int appsDockEnlargePercentage: 125
property int appsDockIconSizePercentage: 100 property int appsDockIconSizePercentage: 100
property bool keyboardLayoutNameCompactMode: false property bool keyboardLayoutNameCompactMode: false
property bool runningAppsCurrentWorkspace: false property bool runningAppsCurrentWorkspace: true
property bool runningAppsGroupByApp: false property bool runningAppsGroupByApp: false
property bool runningAppsCurrentMonitor: false
property var appIdSubstitutions: [] property var appIdSubstitutions: []
property string centeringMode: "index" property string centeringMode: "index"
property string clockDateFormat: "" property string clockDateFormat: ""
@@ -300,6 +315,7 @@ Singleton {
property int dankLauncherV2BorderThickness: 2 property int dankLauncherV2BorderThickness: 2
property string dankLauncherV2BorderColor: "primary" property string dankLauncherV2BorderColor: "primary"
property bool dankLauncherV2ShowFooter: true property bool dankLauncherV2ShowFooter: true
property bool dankLauncherV2UnloadOnClose: false
property string _legacyWeatherLocation: "New York, NY" property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060" property string _legacyWeatherCoordinates: "40.7128,-74.0060"
@@ -457,6 +473,8 @@ Singleton {
property bool dockShowOverflowBadge: true property bool dockShowOverflowBadge: true
property bool notificationOverlayEnabled: false property bool notificationOverlayEnabled: false
property bool notificationPopupShadowEnabled: true
property bool notificationPopupPrivacyMode: false
property int overviewRows: 2 property int overviewRows: 2
property int overviewColumns: 5 property int overviewColumns: 5
property real overviewScale: 0.16 property real overviewScale: 0.16
@@ -471,6 +489,7 @@ Singleton {
property bool lockScreenShowPasswordField: true property bool lockScreenShowPasswordField: true
property bool lockScreenShowMediaPlayer: true property bool lockScreenShowMediaPlayer: true
property bool lockScreenPowerOffMonitorsOnLock: false property bool lockScreenPowerOffMonitorsOnLock: false
property bool lockAtStartup: false
property bool enableFprint: false property bool enableFprint: false
property int maxFprintTries: 15 property int maxFprintTries: 15
@@ -485,17 +504,21 @@ Singleton {
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false property bool notificationCompactMode: false
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
property int notificationCustomAnimationDuration: 400
property bool notificationHistoryEnabled: true property bool notificationHistoryEnabled: true
property int notificationHistoryMaxCount: 50 property int notificationHistoryMaxCount: 50
property int notificationHistoryMaxAgeDays: 7 property int notificationHistoryMaxAgeDays: 7
property bool notificationHistorySaveLow: true property bool notificationHistorySaveLow: true
property bool notificationHistorySaveNormal: true property bool notificationHistorySaveNormal: true
property bool notificationHistorySaveCritical: true property bool notificationHistorySaveCritical: true
property var notificationRules: []
property bool osdAlwaysShowValue: false property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter property int osdPosition: SettingsData.Position.BottomCenter
property bool osdVolumeEnabled: true property bool osdVolumeEnabled: true
property bool osdMediaVolumeEnabled: true property bool osdMediaVolumeEnabled: true
property bool osdMediaPlaybackEnabled: true
property bool osdBrightnessEnabled: true property bool osdBrightnessEnabled: true
property bool osdIdleInhibitorEnabled: true property bool osdIdleInhibitorEnabled: true
property bool osdMicMuteEnabled: true property bool osdMicMuteEnabled: true
@@ -992,6 +1015,42 @@ Singleton {
function applyStoredIconTheme() { function applyStoredIconTheme() {
updateGtkIconTheme(); updateGtkIconTheme();
updateQtIconTheme(); updateQtIconTheme();
updateCosmicIconTheme();
}
function updateCosmicIconTheme() {
let cosmicThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme;
if (!cosmicThemeName || cosmicThemeName === "System Default") {
const detectScript = `if command -v gsettings >/dev/null 2>&1; then
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
elif command -v dconf >/dev/null 2>&1; then
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
fi`;
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
if (exitCode !== 0)
return;
const detected = (output || "").trim();
if (!detected || detected === "System Default")
return;
const detectedEscaped = detected.replace(/'/g, "'\\''");
const writeScript = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTk/v1
printf '"%s"\\n' '${detectedEscaped}' > ${_configDir}/cosmic/com.system76.CosmicTk/v1/icon_theme 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", writeScript]);
});
return;
}
const cosmicThemeNameEscaped = cosmicThemeName.replace(/'/g, "'\\''");
const script = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTk/v1
printf '"%s"\\n' '${cosmicThemeNameEscaped}' > ${_configDir}/cosmic/com.system76.CosmicTk/v1/icon_theme 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", script]);
}
function updateCosmicThemeMode(isLightMode) {
const isDark = isLightMode ? "false" : "true";
const script = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTheme.Mode/v1
printf '%s\\n' ${isDark} > ${_configDir}/cosmic/com.system76.CosmicTheme.Mode/v1/is_dark 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", script]);
} }
function updateGtkIconTheme() { function updateGtkIconTheme() {
@@ -1006,6 +1065,7 @@ Singleton {
for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do
settings_file="$config_dir/settings.ini" settings_file="$config_dir/settings.ini"
[ -f "$settings_file" ] && [ ! -w "$settings_file" ] && continue
if [ -f "$settings_file" ]; then if [ -f "$settings_file" ]; then
if grep -q "^gtk-icon-theme-name=" "$settings_file"; then if grep -q "^gtk-icon-theme-name=" "$settings_file"; then
sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file" sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file"
@@ -1184,6 +1244,7 @@ Singleton {
function saveSettings() { function saveSettings() {
if (_loading || _parseError || !_hasLoaded) if (_loading || _parseError || !_hasLoaded)
return; return;
_selfWrite = true;
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2)); settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
if (_isReadOnly) if (_isReadOnly)
_checkSettingsWritable(); _checkSettingsWritable();
@@ -1797,6 +1858,7 @@ Singleton {
iconTheme = themeName; iconTheme = themeName;
updateGtkIconTheme(); updateGtkIconTheme();
updateQtIconTheme(); updateQtIconTheme();
updateCosmicIconTheme();
saveSettings(); saveSettings();
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic)
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
@@ -1855,6 +1917,7 @@ Singleton {
const script = ` const script = `
xresources_file="${xresourcesPath}" xresources_file="${xresourcesPath}"
[ -f "$xresources_file" ] && [ ! -w "$xresources_file" ] && exit 0
theme_name="${themeName}" theme_name="${themeName}"
cursor_size="${size}" cursor_size="${size}"
@@ -2118,6 +2181,143 @@ Singleton {
saveSettings(); saveSettings();
} }
property bool _pendingExpandNotificationRules: false
property int _pendingNotificationRuleIndex: -1
function addNotificationRule() {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
rules.push({
enabled: true,
field: "appName",
pattern: "",
matchType: "contains",
action: "default",
urgency: "default"
});
notificationRules = rules;
saveSettings();
}
function addNotificationRuleForNotification(appName, desktopEntry) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
var pattern = (desktopEntry && desktopEntry !== "") ? desktopEntry : (appName || "");
var field = (desktopEntry && desktopEntry !== "") ? "desktopEntry" : "appName";
var rule = {
enabled: true,
field: pattern ? field : "appName",
pattern: pattern || "",
matchType: pattern ? "exact" : "contains",
action: "default",
urgency: "default"
};
rules.push(rule);
notificationRules = rules;
saveSettings();
var index = rules.length - 1;
_pendingExpandNotificationRules = true;
_pendingNotificationRuleIndex = index;
return index;
}
function addMuteRuleForApp(appName, desktopEntry) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
var pattern = (desktopEntry && desktopEntry !== "") ? desktopEntry : (appName || "");
var field = (desktopEntry && desktopEntry !== "") ? "desktopEntry" : "appName";
if (pattern === "")
return;
rules.push({
enabled: true,
field: field,
pattern: pattern,
matchType: "exact",
action: "mute",
urgency: "default"
});
notificationRules = rules;
saveSettings();
}
function isAppMuted(appName, desktopEntry) {
const rules = notificationRules || [];
const pat = (desktopEntry && desktopEntry !== "" ? desktopEntry : appName || "").toString().toLowerCase();
if (!pat)
return false;
for (let i = 0; i < rules.length; i++) {
const r = rules[i];
if ((r.action || "").toString().toLowerCase() !== "mute" || r.enabled === false)
continue;
const field = (r.field || "appName").toString().toLowerCase();
const rulePat = (r.pattern || "").toString().toLowerCase();
if (!rulePat)
continue;
const useDesktop = field === "desktopentry";
const matches = (useDesktop && desktopEntry) ? (desktopEntry.toString().toLowerCase() === rulePat) : (appName && appName.toString().toLowerCase() === rulePat);
if (matches)
return true;
if (rulePat === pat)
return true;
}
return false;
}
function removeMuteRuleForApp(appName, desktopEntry) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
const app = (appName || "").toString().toLowerCase();
const desktop = (desktopEntry || "").toString().toLowerCase();
if (!app && !desktop)
return;
for (let i = rules.length - 1; i >= 0; i--) {
const r = rules[i];
if ((r.action || "").toString().toLowerCase() !== "mute")
continue;
const rulePat = (r.pattern || "").toString().toLowerCase();
if (!rulePat)
continue;
if (rulePat === app || rulePat === desktop) {
rules.splice(i, 1);
notificationRules = rules;
saveSettings();
return;
}
}
}
function updateNotificationRule(index, ruleData) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
if (index < 0 || index >= rules.length)
return;
var existing = rules[index] || {};
rules[index] = Object.assign({}, existing, ruleData || {});
notificationRules = rules;
saveSettings();
}
function updateNotificationRuleField(index, key, value) {
if (key === undefined || key === null || key === "")
return;
var patch = {};
patch[key] = value;
updateNotificationRule(index, patch);
}
function removeNotificationRule(index) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
if (index < 0 || index >= rules.length)
return;
rules.splice(index, 1);
notificationRules = rules;
saveSettings();
}
function getDefaultNotificationRules() {
return Spec.SPEC.notificationRules.def;
}
function resetNotificationRules() {
notificationRules = JSON.parse(JSON.stringify(Spec.SPEC.notificationRules.def));
saveSettings();
}
function getDefaultAppIdSubstitutions() { function getDefaultAppIdSubstitutions() {
return Spec.SPEC.appIdSubstitutions.def; return Spec.SPEC.appIdSubstitutions.def;
} }
@@ -2143,19 +2343,40 @@ Singleton {
Theme.reloadCustomThemeVariant(); Theme.reloadCustomThemeVariant();
} }
function getRegistryThemeMultiVariant(themeId, defaults) { function getRegistryThemeMultiVariant(themeId, defaults, mode) {
var stored = registryThemeVariants[themeId]; var stored = registryThemeVariants[themeId];
if (stored && typeof stored === "object") if (!stored || typeof stored !== "object")
return stored; return defaults || {};
return defaults || {}; if ((stored.dark && typeof stored.dark === "object") || (stored.light && typeof stored.light === "object")) {
if (!mode)
return stored.dark || stored.light || defaults || {};
var modeData = stored[mode];
if (modeData && typeof modeData === "object")
return modeData;
return defaults || {};
}
return stored;
} }
function setRegistryThemeMultiVariant(themeId, flavor, accent) { function setRegistryThemeMultiVariant(themeId, flavor, accent, mode) {
var variants = JSON.parse(JSON.stringify(registryThemeVariants)); var variants = JSON.parse(JSON.stringify(registryThemeVariants));
variants[themeId] = { var existing = variants[themeId];
var perMode = {};
if (existing && typeof existing === "object") {
if ((existing.dark && typeof existing.dark === "object") || (existing.light && typeof existing.light === "object")) {
perMode = existing;
} else if (typeof existing.flavor === "string") {
perMode.dark = {
flavor: existing.flavor,
accent: existing.accent || ""
};
}
}
perMode[mode || "dark"] = {
flavor: flavor, flavor: flavor,
accent: accent accent: accent
}; };
variants[themeId] = perMode;
registryThemeVariants = variants; registryThemeVariants = variants;
saveSettings(); saveSettings();
if (typeof Theme !== "undefined") if (typeof Theme !== "undefined")
@@ -2355,6 +2576,13 @@ Singleton {
property alias settingsFile: settingsFile property alias settingsFile: settingsFile
Timer {
id: settingsFileReloadDebounce
interval: 50
onTriggered: settingsFile.reload()
repeat: false
}
FileView { FileView {
id: settingsFile id: settingsFile
@@ -2362,7 +2590,14 @@ Singleton {
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
watchChanges: !isGreeterMode watchChanges: true
onFileChanged: {
if (_selfWrite) {
_selfWrite = false;
return;
}
settingsFileReloadDebounce.restart();
}
onLoaded: { onLoaded: {
if (isGreeterMode) if (isGreeterMode)
return; return;
+153 -15
View File
@@ -45,11 +45,12 @@ Singleton {
if (typeof SessionData === "undefined") if (typeof SessionData === "undefined")
return ""; return "";
var monitors = SessionData.monitorWallpapers;
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
var screens = Quickshell.screens; var screens = Quickshell.screens;
if (screens.length > 0) { if (screens.length > 0) {
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name); var s = screens[0];
return firstMonitorWallpaper || SessionData.wallpaperPath; return monitors[s.name] || (s.model ? monitors[s.model] : "") || SessionData.wallpaperPath;
} }
} }
@@ -59,6 +60,7 @@ Singleton {
if (typeof SessionData === "undefined") if (typeof SessionData === "undefined")
return ""; return "";
var monitors = SessionData.monitorWallpapers;
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
var screens = Quickshell.screens; var screens = Quickshell.screens;
if (screens.length > 0) { if (screens.length > 0) {
@@ -72,12 +74,20 @@ Singleton {
} }
} }
if (!targetMonitorExists) { if (!targetMonitorExists)
targetMonitor = screens[0].name; targetMonitor = screens[0].name;
var s = null;
for (var j = 0; j < screens.length; j++) {
if (screens[j].name === targetMonitor) {
s = screens[j];
break;
}
} }
var targetMonitorWallpaper = SessionData.getMonitorWallpaper(targetMonitor); if (s)
return targetMonitorWallpaper || SessionData.wallpaperPath; return monitors[s.name] || (s.model ? monitors[s.model] : "") || SessionData.wallpaperPath;
return monitors[targetMonitor] || SessionData.wallpaperPath;
} }
} }
@@ -178,6 +188,8 @@ Singleton {
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) { if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
switchTheme(SettingsData.currentThemeName, false, false); switchTheme(SettingsData.currentThemeName, false, false);
const currentIsLight = (typeof SessionData !== "undefined") ? SessionData.isLightMode : false;
SettingsData.updateCosmicThemeMode(currentIsLight);
} }
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) { if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
@@ -606,6 +618,58 @@ Singleton {
} }
} }
readonly property color buttonBg: {
switch (SettingsData.buttonColorMode) {
case "primaryContainer":
return primaryContainer;
case "secondary":
return secondary;
case "surfaceVariant":
return surfaceVariant;
default:
return primary;
}
}
readonly property color buttonText: {
switch (SettingsData.buttonColorMode) {
case "primaryContainer":
return primary;
case "secondary":
return surfaceText;
case "surfaceVariant":
return surfaceText;
default:
return primaryText;
}
}
readonly property color buttonHover: {
switch (SettingsData.buttonColorMode) {
case "primaryContainer":
return Qt.rgba(primary.r, primary.g, primary.b, 0.12);
case "secondary":
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.12);
case "surfaceVariant":
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.12);
default:
return primaryHover;
}
}
readonly property color buttonPressed: {
switch (SettingsData.buttonColorMode) {
case "primaryContainer":
return Qt.rgba(primary.r, primary.g, primary.b, 0.16);
case "secondary":
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.16);
case "surfaceVariant":
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.16);
default:
return primaryPressed;
}
}
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08) property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3) property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
@@ -650,13 +714,13 @@ Singleton {
readonly property int currentAnimationSpeed: typeof SettingsData !== "undefined" ? SettingsData.animationSpeed : SettingsData.AnimationSpeed.Short readonly property int currentAnimationSpeed: typeof SettingsData !== "undefined" ? SettingsData.animationSpeed : SettingsData.AnimationSpeed.Short
readonly property var currentDurations: animationDurations[currentAnimationSpeed] || animationDurations[SettingsData.AnimationSpeed.Short] readonly property var currentDurations: animationDurations[currentAnimationSpeed] || animationDurations[SettingsData.AnimationSpeed.Short]
property int shorterDuration: currentDurations.shorter readonly property int shorterDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.shorter
property int shortDuration: currentDurations.short readonly property int shortDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.short
property int mediumDuration: currentDurations.medium readonly property int mediumDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.medium
property int longDuration: currentDurations.long readonly property int longDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.long
property int extraLongDuration: currentDurations.extraLong readonly property int extraLongDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.extraLong
property int standardEasing: Easing.OutCubic readonly property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart readonly property int emphasizedEasing: Easing.OutQuart
readonly property var expressiveCurves: { readonly property var expressiveCurves: {
"emphasized": [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1], "emphasized": [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1],
@@ -714,6 +778,77 @@ Singleton {
}; };
} }
readonly property int notificationAnimationBaseDuration: {
if (typeof SettingsData === "undefined")
return 200;
if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.None)
return 0;
if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.Custom)
return SettingsData.notificationCustomAnimationDuration;
const presetMap = [0, 200, 400, 600];
return presetMap[SettingsData.notificationAnimationSpeed] ?? 200;
}
readonly property int notificationEnterDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 0.875);
}
readonly property int notificationExitDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 0.75);
}
readonly property int notificationExpandDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 1.0);
}
readonly property int notificationCollapseDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 0.85);
}
readonly property real notificationIconSizeNormal: 56
readonly property real notificationIconSizeCompact: 48
readonly property real notificationExpandedIconSizeNormal: 48
readonly property real notificationExpandedIconSizeCompact: 40
readonly property real notificationActionMinWidth: 48
readonly property real notificationButtonCornerRadius: cornerRadius / 2
readonly property real notificationHoverRevealMargin: spacingXL
readonly property real notificationContentSpacing: spacingXS
readonly property real notificationCardPadding: spacingM
readonly property real notificationCardPaddingCompact: spacingS
readonly property real stateLayerHover: 0.08
readonly property real stateLayerFocus: 0.12
readonly property real stateLayerPressed: 0.12
readonly property real stateLayerDrag: 0.16
readonly property int popoutAnimationDuration: {
if (typeof SettingsData === "undefined")
return 150;
if (SettingsData.syncComponentAnimationSpeeds) {
return Math.min(currentAnimationBaseDuration, 1000);
}
const presetMap = [0, 150, 300, 500];
if (SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom)
return SettingsData.popoutCustomAnimationDuration;
return presetMap[SettingsData.popoutAnimationSpeed] ?? 150;
}
readonly property int modalAnimationDuration: {
if (typeof SettingsData === "undefined")
return 150;
if (SettingsData.syncComponentAnimationSpeeds) {
return Math.min(currentAnimationBaseDuration, 1000);
}
const presetMap = [0, 150, 300, 500];
if (SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom)
return SettingsData.modalCustomAnimationDuration;
return presetMap[SettingsData.modalAnimationSpeed] ?? 150;
}
property real cornerRadius: { property real cornerRadius: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.cornerRadius; return GreetdSettings.cornerRadius;
@@ -830,6 +965,9 @@ Singleton {
if (!matugenAvailable) { if (!matugenAvailable) {
PortalService.setLightMode(light); PortalService.setLightMode(light);
} }
if (typeof SettingsData !== "undefined") {
SettingsData.updateCosmicThemeMode(light);
}
generateSystemThemesFromCurrentTheme(); generateSystemThemesFromCurrentTheme();
} }
} }
@@ -885,7 +1023,7 @@ Singleton {
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) { if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
const defaults = themeData.variants.defaults || {}; const defaults = themeData.variants.defaults || {};
const modeDefaults = defaults[colorMode] || defaults.dark || {}; const modeDefaults = defaults[colorMode] || defaults.dark || {};
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults) : modeDefaults; const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
var flavorId = stored.flavor || modeDefaults.flavor || ""; var flavorId = stored.flavor || modeDefaults.flavor || "";
const accentId = stored.accent || modeDefaults.accent || ""; const accentId = stored.accent || modeDefaults.accent || "";
var flavor = findVariant(themeData.variants.flavors, flavorId); var flavor = findVariant(themeData.variants.flavors, flavorId);
@@ -1279,8 +1417,8 @@ Singleton {
const defaults = customThemeRawData.variants.defaults || {}; const defaults = customThemeRawData.variants.defaults || {};
const darkDefaults = defaults.dark || {}; const darkDefaults = defaults.dark || {};
const lightDefaults = defaults.light || defaults.dark || {}; const lightDefaults = defaults.light || defaults.dark || {};
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults) : darkDefaults; const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults) : lightDefaults; const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || ""; const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || ""; const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
const accentId = storedDark.accent || darkDefaults.accent || ""; const accentId = storedDark.accent || darkDefaults.accent || "";
+15 -4
View File
@@ -32,8 +32,15 @@ function markdownToHtml(text) {
return `\x00INLINECODE${inlineIndex++}\x00`; return `\x00INLINECODE${inlineIndex++}\x00`;
}); });
// Now process everything else // Extract plain URLs before escaping so & in query strings is preserved
// Escape HTML entities (but not in code blocks) const urls = [];
let urlIndex = 0;
html = html.replace(/(^|[\s])((?:https?|file):\/\/[^\s]+)/gm, (match, prefix, url) => {
urls.push(url);
return prefix + `\x00URL${urlIndex++}\x00`;
});
// Escape HTML entities (but not in code blocks or URLs)
html = html.replace(/&/g, '&amp;') html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;'); .replace(/>/g, '&gt;');
@@ -64,8 +71,12 @@ function markdownToHtml(text) {
return '<ul>' + match + '</ul>'; return '<ul>' + match + '</ul>';
}); });
// Detect plain URLs and wrap them in anchor tags (but not inside existing <a> or markdown links) // Restore extracted URLs as anchor tags (preserves raw & in href)
html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>'); html = html.replace(/\x00URL(\d+)\x00/g, (_, index) => {
const url = urls[parseInt(index)];
const display = url.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return `<a href="${url}">${display}</a>`;
});
// Restore code blocks and inline code BEFORE line break processing // Restore code blocks and inline code BEFORE line break processing
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => { html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
+6 -1
View File
@@ -12,6 +12,7 @@ var SPEC = {
wallpaperPathDark: { def: "" }, wallpaperPathDark: { def: "" },
monitorWallpapersLight: { def: {} }, monitorWallpapersLight: { def: {} },
monitorWallpapersDark: { def: {} }, monitorWallpapersDark: { def: {} },
monitorWallpaperFillModes: { def: {} },
wallpaperTransition: { def: "fade" }, wallpaperTransition: { def: "fade" },
includedTransitions: { def: ["fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"] }, includedTransitions: { def: ["fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"] },
@@ -72,7 +73,11 @@ var SPEC = {
appOverrides: { def: {} }, appOverrides: { def: {} },
searchAppActions: { def: true }, searchAppActions: { def: true },
vpnLastConnected: { def: "" } vpnLastConnected: { def: "" },
deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] }
}; };
function getValidKeys() { function getValidKeys() {
+21 -1
View File
@@ -20,6 +20,7 @@ var SPEC = {
widgetBackgroundColor: { def: "sch" }, widgetBackgroundColor: { def: "sch" },
widgetColorMode: { def: "default" }, widgetColorMode: { def: "default" },
controlCenterTileColorMode: { def: "primary" }, controlCenterTileColorMode: { def: "primary" },
buttonColorMode: { def: "primary" },
cornerRadius: { def: 12, onChange: "updateCompositorLayout" }, cornerRadius: { def: 12, onChange: "updateCompositorLayout" },
niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" }, niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" }, niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
@@ -39,6 +40,12 @@ var SPEC = {
nightModeEnabled: { def: false }, nightModeEnabled: { def: false },
animationSpeed: { def: 1 }, animationSpeed: { def: 1 },
customAnimationDuration: { def: 500 }, customAnimationDuration: { def: 500 },
syncComponentAnimationSpeeds: { def: true },
popoutAnimationSpeed: { def: 1 },
popoutCustomAnimationDuration: { def: 150 },
modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true },
wallpaperFillMode: { def: "Fill" }, wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false }, blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false }, blurWallpaperOnOverview: { def: false },
@@ -98,6 +105,7 @@ var SPEC = {
showWorkspacePadding: { def: false }, showWorkspacePadding: { def: false },
workspaceScrolling: { def: false }, workspaceScrolling: { def: false },
showWorkspaceApps: { def: false }, showWorkspaceApps: { def: false },
workspaceDragReorder: { def: true },
maxWorkspaceIcons: { def: 3 }, maxWorkspaceIcons: { def: 3 },
workspaceAppIconSizeOffset: { def: 0 }, workspaceAppIconSizeOffset: { def: 0 },
groupWorkspaceApps: { def: true }, groupWorkspaceApps: { def: true },
@@ -117,6 +125,7 @@ var SPEC = {
scrollTitleEnabled: { def: true }, scrollTitleEnabled: { def: true },
audioVisualizerEnabled: { def: true }, audioVisualizerEnabled: { def: true },
audioScrollMode: { def: "volume" }, audioScrollMode: { def: "volume" },
audioWheelScrollAmount: { def: 5 },
clockCompactMode: { def: false }, clockCompactMode: { def: false },
focusedWindowCompactMode: { def: false }, focusedWindowCompactMode: { def: false },
runningAppsCompactMode: { def: true }, runningAppsCompactMode: { def: true },
@@ -130,8 +139,9 @@ var SPEC = {
appsDockEnlargePercentage: { def: 125 }, appsDockEnlargePercentage: { def: 125 },
appsDockIconSizePercentage: { def: 100 }, appsDockIconSizePercentage: { def: 100 },
keyboardLayoutNameCompactMode: { def: false }, keyboardLayoutNameCompactMode: { def: false },
runningAppsCurrentWorkspace: { def: false }, runningAppsCurrentWorkspace: { def: true },
runningAppsGroupByApp: { def: false }, runningAppsGroupByApp: { def: false },
runningAppsCurrentMonitor: { def: false },
appIdSubstitutions: { appIdSubstitutions: {
def: [ def: [
{ pattern: "Spotify", replacement: "spotify", type: "exact" }, { pattern: "Spotify", replacement: "spotify", type: "exact" },
@@ -163,6 +173,7 @@ var SPEC = {
dankLauncherV2BorderThickness: { def: 2 }, dankLauncherV2BorderThickness: { def: 2 },
dankLauncherV2BorderColor: { def: "primary" }, dankLauncherV2BorderColor: { def: "primary" },
dankLauncherV2ShowFooter: { def: true }, dankLauncherV2ShowFooter: { def: true },
dankLauncherV2UnloadOnClose: { def: false },
useAutoLocation: { def: false }, useAutoLocation: { def: false },
weatherEnabled: { def: true }, weatherEnabled: { def: true },
@@ -286,6 +297,8 @@ var SPEC = {
dockShowOverflowBadge: { def: true }, dockShowOverflowBadge: { def: true },
notificationOverlayEnabled: { def: false }, notificationOverlayEnabled: { def: false },
notificationPopupShadowEnabled: { def: true },
notificationPopupPrivacyMode: { def: false },
overviewRows: { def: 2, persist: false }, overviewRows: { def: 2, persist: false },
overviewColumns: { def: 5, persist: false }, overviewColumns: { def: 5, persist: false },
overviewScale: { def: 0.16, persist: false }, overviewScale: { def: 0.16, persist: false },
@@ -300,6 +313,7 @@ var SPEC = {
lockScreenShowPasswordField: { def: true }, lockScreenShowPasswordField: { def: true },
lockScreenShowMediaPlayer: { def: true }, lockScreenShowMediaPlayer: { def: true },
lockScreenPowerOffMonitorsOnLock: { def: false }, lockScreenPowerOffMonitorsOnLock: { def: false },
lockAtStartup: { def: false },
enableFprint: { def: false }, enableFprint: { def: false },
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
@@ -313,17 +327,21 @@ var SPEC = {
notificationTimeoutCritical: { def: 0 }, notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false }, notificationCompactMode: { def: false },
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
notificationAnimationSpeed: { def: 1 },
notificationCustomAnimationDuration: { def: 400 },
notificationHistoryEnabled: { def: true }, notificationHistoryEnabled: { def: true },
notificationHistoryMaxCount: { def: 50 }, notificationHistoryMaxCount: { def: 50 },
notificationHistoryMaxAgeDays: { def: 7 }, notificationHistoryMaxAgeDays: { def: 7 },
notificationHistorySaveLow: { def: true }, notificationHistorySaveLow: { def: true },
notificationHistorySaveNormal: { def: true }, notificationHistorySaveNormal: { def: true },
notificationHistorySaveCritical: { def: true }, notificationHistorySaveCritical: { def: true },
notificationRules: { def: [] },
osdAlwaysShowValue: { def: false }, osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 }, osdPosition: { def: 5 },
osdVolumeEnabled: { def: true }, osdVolumeEnabled: { def: true },
osdMediaVolumeEnabled: { def: true }, osdMediaVolumeEnabled: { def: true },
osdMediaPlaybackEnabled: { def: true },
osdBrightnessEnabled: { def: true }, osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true }, osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true }, osdMicMuteEnabled: { def: true },
@@ -456,6 +474,8 @@ var SPEC = {
desktopWidgetGroups: { def: [] }, desktopWidgetGroups: { def: [] },
builtInPluginSettings: { def: {} }, builtInPluginSettings: { def: {} },
clipboardEnterToPaste: { def: false },
launcherPluginVisibility: { def: {} }, launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] } launcherPluginOrder: { def: [] }
}; };
+19 -4
View File
@@ -251,13 +251,20 @@ Item {
active: false active: false
asynchronous: false asynchronous: false
Component.onCompleted: {
PopoutService.dankDashPopoutLoader = dankDashPopoutLoader;
}
onLoaded: {
if (item) {
PopoutService.dankDashPopout = item;
PopoutService._onDankDashPopoutLoaded();
}
}
sourceComponent: Component { sourceComponent: Component {
DankDashPopout { DankDashPopout {
id: dankDashPopout id: dankDashPopout
Component.onCompleted: {
PopoutService.dankDashPopout = dankDashPopout;
}
} }
} }
} }
@@ -841,6 +848,14 @@ Item {
} }
} }
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: MediaPlaybackOSD {
modelData: item
}
}
Variants { Variants {
model: SettingsData.getFilteredScreens("osd") model: SettingsData.getFilteredScreens("osd")
+88 -3
View File
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Services.SystemTray
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Modules.Settings.DisplayConfig import qs.Modules.Settings.DisplayConfig
@@ -196,7 +197,7 @@ Item {
if (CompositorService.isNiri && NiriService.currentOutput) { if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput; return NiriService.currentOutput;
} }
if ((CompositorService.isSway || CompositorService.isScroll) && I3.workspaces?.values) { if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && I3.workspaces?.values) {
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true); const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || ""; return focusedWs?.monitor?.name || "";
} }
@@ -338,6 +339,36 @@ Item {
} }
} }
function increment(step: string): string {
if (MprisController.activePlayer && MprisController.activePlayer.volumeSupported) {
const currentVolume = Math.round(MprisController.activePlayer.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue));
MprisController.activePlayer.volume = newVolume / 100;
return `Player volume increased to ${newVolume}%`;
}
}
function decrement(step: string): string {
if (MprisController.activePlayer && MprisController.activePlayer.volumeSupported) {
const currentVolume = Math.round(MprisController.activePlayer.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue));
MprisController.activePlayer.volume = newVolume / 100;
return `Player volume decreased to ${newVolume}%`;
}
}
function setvolume(percentage: string): string {
if (MprisController.activePlayer && MprisController.activePlayer.volumeSupported) {
const clampedVolume = Math.max(0, Math.min(100, percentage));
MprisController.activePlayer.volume = clampedVolume / 100;
return `Player volume set to ${clampedVolume}%`;
}
}
target: "mpris" target: "mpris"
} }
@@ -941,8 +972,10 @@ Item {
if (!PluginService.availablePlugins[pluginId]) if (!PluginService.availablePlugins[pluginId])
return `PLUGIN_NOT_FOUND: ${pluginId}`; return `PLUGIN_NOT_FOUND: ${pluginId}`;
if (!PluginService.isPluginLoaded(pluginId)) if (!PluginService.isPluginLoaded(pluginId)) {
return `PLUGIN_NOT_LOADED: ${pluginId}`; const success = PluginService.enablePlugin(pluginId);
return success ? `PLUGIN_RELOAD_SUCCESS: ${pluginId}` : `PLUGIN_RELOAD_FAILED: ${pluginId}`;
}
const success = PluginService.reloadPlugin(pluginId); const success = PluginService.reloadPlugin(pluginId);
return success ? `PLUGIN_RELOAD_SUCCESS: ${pluginId}` : `PLUGIN_RELOAD_FAILED: ${pluginId}`; return success ? `PLUGIN_RELOAD_SUCCESS: ${pluginId}` : `PLUGIN_RELOAD_FAILED: ${pluginId}`;
@@ -1530,4 +1563,56 @@ Item {
target: "outputs" target: "outputs"
} }
IpcHandler {
function findTrayItem(itemId: string): var {
if (!itemId)
return null;
return SystemTray.items.values.find(item => {
const id = item?.id || "";
const title = item?.tooltipTitle || "";
const fullKey = title ? `${id}::${title}` : id;
return fullKey === itemId || id === itemId;
});
}
function list(): string {
const items = SystemTray.items.values;
if (items.length === 0)
return "No tray items available";
return items.map(item => {
const id = item?.id || "";
const title = item?.tooltipTitle || "";
const fullKey = title ? `${id}::${title}` : id;
const hasMenu = item?.hasMenu ? " [menu]" : "";
return fullKey + hasMenu;
}).join("\n");
}
function activate(itemId: string): string {
const item = findTrayItem(itemId);
if (!item)
return `ERROR: Tray item not found: ${itemId}`;
item.activate();
return `SUCCESS: Activated ${itemId}`;
}
function status(itemId: string): string {
const item = findTrayItem(itemId);
if (!item)
return `ERROR: Tray item not found: ${itemId}`;
const id = item?.id || "";
const title = item?.tooltipTitle || "";
const hasMenu = item?.hasMenu || false;
const onlyMenu = item?.onlyMenu || false;
return `id: ${id}\ntitle: ${title}\nhasMenu: ${hasMenu}\nonlyMenu: ${onlyMenu}`;
}
target: "tray"
}
} }
@@ -65,7 +65,7 @@ Column {
StyledText { StyledText {
id: codenameText id: codenameText
anchors.centerIn: parent anchors.centerIn: parent
text: "Spicy Miso" text: "Saffron Bloom"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.primary color: Theme.primary
@@ -74,7 +74,7 @@ Column {
} }
StyledText { StyledText {
text: "Desktop widgets, theme registry, native clipboard & more" text: "New launcher, enhanced plugin system, KDE Connect, & more"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
@@ -108,67 +108,76 @@ Column {
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "widgets" iconName: "space_dashboard"
title: "Desktop Widgets" title: "Dank Launcher V2"
description: "Widgets on your desktop" description: "New capabilities & plugins"
onClicked: PopoutService.openSettingsWithTab("desktop_widgets") onClicked: PopoutService.openDankLauncherV2()
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "palette" iconName: "smartphone"
title: "Theme Registry" title: "Phone Connect"
description: "Community themes" description: "KDE Connect & Valent"
onClicked: PopoutService.openSettingsWithTab("theme") onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "content_paste" iconName: "monitor_heart"
title: "Native Clipboard" title: "System Monitor"
description: "Zero-dependency history" description: "Redesigned process list"
onClicked: PopoutService.openSettingsWithTab("clipboard") onClicked: PopoutService.showProcessListModal()
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "display_settings" iconName: "window"
title: "Monitor Config" title: "Window Rules"
description: "Full display setup" description: "niri window rule manager"
onClicked: PopoutService.openSettingsWithTab("display_config") visible: CompositorService.isNiri
onClicked: PopoutService.openSettingsWithTab("window_rules")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "notifications_active" iconName: "notifications_active"
title: "Notifications" title: "Enhanced Notifications"
description: "History & gestures" description: "Configurable rules & styling"
visible: !CompositorService.isNiri
onClicked: PopoutService.openSettingsWithTab("notifications") onClicked: PopoutService.openSettingsWithTab("notifications")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "healing" iconName: "dock_to_bottom"
title: "DMS Doctor" title: "Dock Enhancements"
description: "Diagnose issues" description: "Bar dock widget & more"
onClicked: FirstLaunchService.showDoctor() onClicked: PopoutService.openSettingsWithTab("dock")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "keyboard" iconName: "volume_up"
title: "Keybinds Editor" title: "Audio Aliases"
description: "niri, Hyprland, & MangoWC" description: "Custom device names"
visible: KeybindsService.available onClicked: PopoutService.openSettingsWithTab("audio")
onClicked: PopoutService.openSettingsWithTab("keybinds")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "search" iconName: "extension"
title: "Settings Search" title: "Enhanced Plugin System"
description: "Find settings fast" description: "Enables new types of plugins"
onClicked: PopoutService.openSettings() onClicked: PopoutService.openSettingsWithTab("plugins")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "light_mode"
title: "Auto Light/Dark"
description: "Automatic mode switching"
onClicked: PopoutService.openSettingsWithTab("theme")
} }
} }
} }
@@ -221,26 +230,21 @@ Column {
ChangelogUpgradeNote { ChangelogUpgradeNote {
width: parent.width width: parent.width
text: "Ghostty theme path changed to ~/.config/ghostty/themes/danktheme" text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
} }
ChangelogUpgradeNote { ChangelogUpgradeNote {
width: parent.width width: parent.width
text: "VS Code theme reinstall required" text: "Plugin API updated — third-party plugins may need updates"
}
ChangelogUpgradeNote {
width: parent.width
text: "Clipboard history migration available from cliphist"
} }
} }
} }
StyledText { // StyledText {
text: "See full release notes for migration steps" // text: "See full release notes for migration steps"
font.pixelSize: Theme.fontSizeSmall // font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText // color: Theme.surfaceVariantText
width: parent.width // width: parent.width
} // }
} }
} }
@@ -7,6 +7,7 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: root id: root
property bool disablePopupTransparency: true
readonly property int modalWidth: 680 readonly property int modalWidth: 680
readonly property int modalHeight: screen ? Math.min(720, screen.height - 80) : 720 readonly property int modalHeight: screen ? Math.min(720, screen.height - 80) : 720
@@ -128,7 +129,7 @@ FloatingWindow {
iconName: "open_in_new" iconName: "open_in_new"
backgroundColor: Theme.surfaceContainerHighest backgroundColor: Theme.surfaceContainerHighest
textColor: Theme.surfaceText textColor: Theme.surfaceText
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-2-release") onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
} }
DankButton { DankButton {
@@ -164,6 +164,7 @@ Item {
} }
visible: modal.activeTab === "saved" visible: modal.activeTab === "saved"
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
spacing: Theme.spacingXS spacing: Theme.spacingXS
interactive: true interactive: true
flickDeceleration: 1500 flickDeceleration: 1500
@@ -173,6 +174,26 @@ Item {
pressDelay: 0 pressDelay: 0
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
function ensureVisible(index) {
if (index < 0 || index >= count) {
return;
}
const itemHeight = ClipboardConstants.itemHeight + spacing;
const itemY = index * itemHeight;
const itemBottom = itemY + itemHeight;
if (itemY < contentY) {
contentY = itemY;
} else if (itemBottom > contentY + height) {
contentY = itemBottom - height;
}
}
onCurrentIndexChanged: {
if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
ensureVisible(currentIndex);
}
}
StyledText { StyledText {
text: I18n.tr("No saved clipboard entries") text: I18n.tr("No saved clipboard entries")
anchors.centerIn: parent anchors.centerIn: parent
@@ -190,7 +211,7 @@ Item {
entry: modelData entry: modelData
entryIndex: index + 1 entryIndex: index + 1
itemIndex: index itemIndex: index
isSelected: false isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
modal: clipboardContent.modal modal: clipboardContent.modal
listView: savedListView listView: savedListView
onCopyRequested: clipboardContent.modal.copyEntry(modelData) onCopyRequested: clipboardContent.modal.copyEntry(modelData)
@@ -247,6 +268,7 @@ Item {
sourceComponent: ClipboardKeyboardHints { sourceComponent: ClipboardKeyboardHints {
wtypeAvailable: modal.wtypeAvailable wtypeAvailable: modal.wtypeAvailable
enterToPaste: SettingsData.clipboardEnterToPaste
} }
} }
} }
+90 -72
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
@@ -19,6 +20,7 @@ Rectangle {
readonly property string entryType: modal ? modal.getEntryType(entry) : "text" readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : "" readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
readonly property bool hasPinnedDuplicate: !entry.pinned && ClipboardService.hashedPinnedEntry(entry.hash)
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
@@ -28,91 +30,43 @@ Rectangle {
return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
} }
Row { DankRipple {
anchors.fill: parent id: rippleLayer
anchors.margins: Theme.spacingM rippleColor: Theme.surfaceText
anchors.rightMargin: Theme.spacingS cornerRadius: root.radius
spacing: Theme.spacingL }
Rectangle { Rectangle {
width: 24 id: indexBadge
height: 24 anchors.left: parent.left
radius: 12 anchors.leftMargin: Theme.spacingM
color: Theme.primarySelected anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent.verticalCenter width: 24
height: 24
radius: 12
color: Theme.primarySelected
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: entryIndex.toString() text: entryIndex.toString()
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold font.weight: Font.Bold
color: Theme.primary color: Theme.primary
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 110
spacing: Theme.spacingM
ClipboardThumbnail {
width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
entry: root.entry
entryType: root.entryType
modal: root.modal
listView: root.listView
itemIndex: root.itemIndex
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize) - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: {
switch (entryType) {
case "image":
return I18n.tr("Image") + " • " + entryPreview;
case "long_text":
return I18n.tr("Long Text");
default:
return I18n.tr("Text");
}
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: entryPreview
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight
}
}
} }
} }
Row { Row {
id: actionButtons
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankActionButton { DankActionButton {
iconName: "push_pin" iconName: "push_pin"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: entry.pinned ? Theme.primary : Theme.surfaceText iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
backgroundColor: entry.pinned ? Theme.primarySelected : "transparent" backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
onClicked: entry.pinned ? unpinRequested() : pinRequested() onClicked: entry.pinned ? unpinRequested() : pinRequested()
} }
@@ -124,12 +78,76 @@ Rectangle {
} }
} }
Item {
anchors.left: indexBadge.right
anchors.leftMargin: Theme.spacingM
anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
height: contentColumn.implicitHeight
clip: true
ClipboardThumbnail {
id: thumbnail
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
entry: root.entry
entryType: root.entryType
modal: root.modal
listView: root.listView
itemIndex: root.itemIndex
}
Column {
id: contentColumn
anchors.left: thumbnail.right
anchors.leftMargin: Theme.spacingM
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: {
switch (entryType) {
case "image":
return I18n.tr("Image") + " • " + entryPreview;
case "long_text":
return I18n.tr("Long Text");
default:
return I18n.tr("Text");
}
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: entryPreview
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight
}
}
}
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
anchors.rightMargin: 80 anchors.rightMargin: 80
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => {
const pos = mouseArea.mapToItem(root, mouse.x, mouse.y);
rippleLayer.trigger(pos.x, pos.y);
}
onClicked: copyRequested() onClicked: copyRequested()
} }
} }
@@ -18,6 +18,10 @@ DankModal {
} }
property string activeTab: "recents" property string activeTab: "recents"
onActiveTabChanged: {
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
}
property bool showKeyboardHints: false property bool showKeyboardHints: false
property Component clipboardContent property Component clipboardContent
property int activeImageLoads: 0 property int activeImageLoads: 0
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import qs.Common
import qs.Services import qs.Services
QtObject { QtObject {
@@ -13,15 +14,17 @@ QtObject {
} }
function selectNext() { function selectNext() {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0) { const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0) {
return; return;
} }
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = Math.min(ClipboardService.selectedIndex + 1, ClipboardService.clipboardEntries.length - 1); ClipboardService.selectedIndex = Math.min(ClipboardService.selectedIndex + 1, entries.length - 1);
} }
function selectPrevious() { function selectPrevious() {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0) { const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0) {
return; return;
} }
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
@@ -29,19 +32,25 @@ QtObject {
} }
function copySelected() { function copySelected() {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= ClipboardService.clipboardEntries.length) { const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
return; return;
} }
const selectedEntry = ClipboardService.clipboardEntries[ClipboardService.selectedIndex]; const selectedEntry = entries[ClipboardService.selectedIndex];
modal.copyEntry(selectedEntry); modal.copyEntry(selectedEntry);
} }
function deleteSelected() { function deleteSelected() {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= ClipboardService.clipboardEntries.length) { const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
return; return;
} }
const selectedEntry = ClipboardService.clipboardEntries[ClipboardService.selectedIndex]; const selectedEntry = entries[ClipboardService.selectedIndex];
modal.deleteEntry(selectedEntry); if (modal.activeTab === "saved") {
modal.deletePinnedEntry(selectedEntry);
} else {
modal.deleteEntry(selectedEntry);
}
} }
function handleKey(event) { function handleKey(event) {
@@ -125,7 +134,11 @@ QtObject {
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
if (ClipboardService.keyboardNavigationActive) { if (ClipboardService.keyboardNavigationActive) {
modal.pasteSelected(); if (SettingsData.clipboardEnterToPaste) {
copySelected();
} else {
modal.pasteSelected();
}
event.accepted = true; event.accepted = true;
} }
return; return;
@@ -136,7 +149,11 @@ QtObject {
switch (event.key) { switch (event.key) {
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
copySelected(); if (SettingsData.clipboardEnterToPaste) {
modal.pasteSelected();
} else {
copySelected();
}
event.accepted = true; event.accepted = true;
return; return;
case Qt.Key_Delete: case Qt.Key_Delete:
@@ -6,7 +6,12 @@ Rectangle {
id: keyboardHints id: keyboardHints
property bool wtypeAvailable: false property bool wtypeAvailable: false
readonly property string hintsText: wtypeAvailable ? I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close") : I18n.tr("Shift+Del: Clear All • Esc: Close") property bool enterToPaste: false
readonly property string hintsText: {
if (!wtypeAvailable)
return I18n.tr("Shift+Del: Clear All • Esc: Close");
return enterToPaste ? I18n.tr("Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close");
}
height: ClipboardConstants.keyboardHintsHeight height: ClipboardConstants.keyboardHintsHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -21,7 +26,7 @@ Rectangle {
spacing: 2 spacing: 2
StyledText { StyledText {
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help" text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
+9 -14
View File
@@ -3,7 +3,6 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -26,7 +25,7 @@ Item {
property bool closeOnEscapeKey: true property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true property bool closeOnBackgroundClick: true
property string animationType: "scale" property string animationType: "scale"
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial property int animationDuration: Theme.modalAnimationDuration
property real animationScaleCollapsed: 0.96 property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.spacingL property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
@@ -132,7 +131,7 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: animationDuration + 120 interval: animationDuration + 50
onTriggered: { onTriggered: {
if (shouldBeVisible) if (shouldBeVisible)
return; return;
@@ -284,9 +283,8 @@ Item {
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: root.animationDuration duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -332,27 +330,24 @@ Item {
Behavior on animX { Behavior on animX {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: root.animationDuration duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: root.animationDuration duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: root.animationDuration duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -382,11 +377,11 @@ Item {
} }
} }
DankRectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: root.backgroundColor color: root.backgroundColor
borderColor: root.borderColor border.color: root.borderColor
borderWidth: root.borderWidth border.width: root.borderWidth
radius: root.cornerRadius radius: root.cornerRadius
} }
+395 -72
View File
@@ -26,6 +26,10 @@ Item {
property string activePluginId: "" property string activePluginId: ""
property var collapsedSections: ({}) property var collapsedSections: ({})
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property bool active: false
property var _modeSectionsCache: ({})
property bool _queryDrivenSearch: false
property bool _diskCacheConsumed: false
property var sectionViewModes: ({}) property var sectionViewModes: ({})
property var pluginViewPreferences: ({}) property var pluginViewPreferences: ({})
property int gridColumns: SettingsData.appLauncherGridColumns property int gridColumns: SettingsData.appLauncherGridColumns
@@ -38,16 +42,39 @@ Item {
signal viewModeChanged(string sectionId, string mode) signal viewModeChanged(string sectionId, string mode)
signal searchQueryRequested(string query) signal searchQueryRequested(string query)
onActiveChanged: {
if (!active) {
sections = [];
flatModel = [];
selectedItem = null;
_clearModeCache();
}
}
Connections { Connections {
target: SettingsData target: SettingsData
function onSortAppsAlphabeticallyChanged() { function onSortAppsAlphabeticallyChanged() {
AppSearchService.invalidateLauncherCache(); AppSearchService.invalidateLauncherCache();
_clearModeCache();
}
}
Connections {
target: AppSearchService
function onCacheVersionChanged() {
if (!active)
return;
_clearModeCache();
if (!searchQuery && searchMode === "all")
performSearch();
} }
} }
Connections { Connections {
target: PluginService target: PluginService
function onRequestLauncherUpdate(pluginId) { function onRequestLauncherUpdate(pluginId) {
if (!active)
return;
if (activePluginId === pluginId) { if (activePluginId === pluginId) {
if (activePluginCategories.length <= 1) if (activePluginCategories.length <= 1)
loadPluginCategories(pluginId); loadPluginCategories(pluginId);
@@ -204,7 +231,7 @@ Item {
} }
function setPluginViewPreference(pluginId, mode, enforced) { function setPluginViewPreference(pluginId, mode, enforced) {
var prefs = pluginViewPreferences; var prefs = Object.assign({}, pluginViewPreferences);
prefs[pluginId] = { prefs[pluginId] = {
mode: mode, mode: mode,
enforced: enforced || false enforced: enforced || false
@@ -230,7 +257,7 @@ Item {
if (pref && pref.mode) { if (pref && pref.mode) {
setPluginViewPreference(sectionId, pref.mode, pref.enforced); setPluginViewPreference(sectionId, pref.mode, pref.enforced);
} else { } else {
var prefs = pluginViewPreferences; var prefs = Object.assign({}, pluginViewPreferences);
delete prefs[sectionId]; delete prefs[sectionId];
pluginViewPreferences = prefs; pluginViewPreferences = prefs;
} }
@@ -247,13 +274,22 @@ Item {
} }
property int _searchVersion: 0 property int _searchVersion: 0
property bool _pluginPhasePending: false
property bool _pluginPhaseForceFirst: false
property var _phase1Items: []
Timer { Timer {
id: searchDebounce id: searchDebounce
interval: searchMode === "all" && searchQuery.length > 0 ? 90 : 60 interval: 60
onTriggered: root.performSearch() onTriggered: root.performSearch()
} }
Timer {
id: pluginPhaseTimer
interval: 1
onTriggered: root._performPluginPhase()
}
Timer { Timer {
id: fileSearchDebounce id: fileSearchDebounce
interval: 200 interval: 200
@@ -266,6 +302,10 @@ Item {
function setSearchQuery(query) { function setSearchQuery(query) {
_searchVersion++; _searchVersion++;
_queryDrivenSearch = true;
_pluginPhasePending = false;
_phase1Items = [];
pluginPhaseTimer.stop();
searchQuery = query; searchQuery = query;
searchDebounce.restart(); searchDebounce.restart();
@@ -324,6 +364,12 @@ Item {
activePluginCategory = ""; activePluginCategory = "";
pluginFilter = ""; pluginFilter = "";
collapsedSections = {}; collapsedSections = {};
_clearModeCache();
_queryDrivenSearch = false;
_pluginPhasePending = false;
_pluginPhaseForceFirst = false;
_phase1Items = [];
pluginPhaseTimer.stop();
} }
function loadPluginCategories(pluginId) { function loadPluginCategories(pluginId) {
@@ -369,7 +415,11 @@ Item {
return false; return false;
} }
function preserveSelectionAfterUpdate() { function preserveSelectionAfterUpdate(forceFirst) {
if (forceFirst)
return function () {
return getFirstItemIndex();
};
var previousSelectedId = selectedItem?.id || ""; var previousSelectedId = selectedItem?.id || "";
return function (newFlatModel) { return function (newFlatModel) {
if (!previousSelectedId) if (!previousSelectedId)
@@ -385,24 +435,60 @@ Item {
function performSearch() { function performSearch() {
var currentVersion = _searchVersion; var currentVersion = _searchVersion;
isSearching = true; isSearching = true;
var restoreSelection = preserveSelectionAfterUpdate(); var shouldResetSelection = _queryDrivenSearch;
_queryDrivenSearch = false;
var restoreSelection = preserveSelectionAfterUpdate(shouldResetSelection);
var cachedSections = AppSearchService.getCachedDefaultSections(); var cachedSections = AppSearchService.getCachedDefaultSections();
if (!cachedSections && !_diskCacheConsumed && !searchQuery && searchMode === "all" && !pluginFilter) {
_diskCacheConsumed = true;
var diskSections = _loadDiskCache();
if (diskSections) {
activePluginId = "";
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
clearActivePluginViewPreference();
for (var i = 0; i < diskSections.length; i++) {
if (collapsedSections[diskSections[i].id] !== undefined)
diskSections[i].collapsed = collapsedSections[diskSections[i].id];
}
_applyHighlights(diskSections, "");
flatModel = Scorer.flattenSections(diskSections);
sections = diskSections;
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false;
searchCompleted();
return;
}
}
if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) { if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
activePluginId = ""; activePluginId = "";
activePluginName = ""; activePluginName = "";
activePluginCategories = []; activePluginCategories = [];
activePluginCategory = ""; activePluginCategory = "";
clearActivePluginViewPreference(); clearActivePluginViewPreference();
sections = cachedSections.map(function (s) { var modeCache = _getCachedModeData("all");
var copy = Object.assign({}, s, { if (modeCache) {
items: s.items ? s.items.slice() : [] _applyHighlights(modeCache.sections, "");
sections = modeCache.sections;
flatModel = modeCache.flatModel;
} else {
var newSections = cachedSections.map(function (s) {
var copy = Object.assign({}, s, {
items: s.items ? s.items.slice() : []
});
if (collapsedSections[s.id] !== undefined)
copy.collapsed = collapsedSections[s.id];
return copy;
}); });
if (collapsedSections[s.id] !== undefined) _applyHighlights(newSections, "");
copy.collapsed = collapsedSections[s.id]; flatModel = Scorer.flattenSections(newSections);
return copy; sections = newSections;
}); _setCachedModeData("all", sections, flatModel);
flatModel = Scorer.flattenSections(sections); }
selectedFlatIndex = restoreSelection(flatModel); selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem(); updateSelectedItem();
isSearching = false; isSearching = false;
@@ -423,7 +509,8 @@ Item {
loadPluginCategories(triggerMatch.pluginId); loadPluginCategories(triggerMatch.pluginId);
var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query); var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query);
allItems = allItems.concat(pluginItems); for (var k = 0; k < pluginItems.length; k++)
allItems.push(pluginItems[k]);
if (triggerMatch.isBuiltIn) { if (triggerMatch.isBuiltIn) {
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query); var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
@@ -435,17 +522,19 @@ Item {
var dynamicDefs = buildDynamicSectionDefs(allItems); var dynamicDefs = buildDynamicSectionDefs(allItems);
var scoredItems = Scorer.scoreItems(allItems, triggerMatch.query, getFrecencyForItem); var scoredItems = Scorer.scoreItems(allItems, triggerMatch.query, getFrecencyForItem);
var sortAlpha = !triggerMatch.query && SettingsData.sortAppsAlphabetically; var sortAlpha = !triggerMatch.query && SettingsData.sortAppsAlphabetically;
sections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500); var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500);
for (var sid in collapsedSections) { for (var sid in collapsedSections) {
for (var i = 0; i < sections.length; i++) { for (var i = 0; i < newSections.length; i++) {
if (sections[i].id === sid) { if (newSections[i].id === sid) {
sections[i].collapsed = collapsedSections[sid]; newSections[i].collapsed = collapsedSections[sid];
} }
} }
} }
flatModel = Scorer.flattenSections(sections); _applyHighlights(newSections, triggerMatch.query);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel); selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem(); updateSelectedItem();
@@ -475,18 +564,28 @@ Item {
if (searchMode === "apps") { if (searchMode === "apps") {
var cachedSections = AppSearchService.getCachedDefaultSections(); var cachedSections = AppSearchService.getCachedDefaultSections();
if (cachedSections && !searchQuery) { if (cachedSections && !searchQuery) {
var appSectionIds = ["favorites", "apps"]; var modeCache = _getCachedModeData("apps");
sections = cachedSections.filter(function (s) { if (modeCache) {
return appSectionIds.indexOf(s.id) !== -1; _applyHighlights(modeCache.sections, "");
}).map(function (s) { sections = modeCache.sections;
var copy = Object.assign({}, s, { flatModel = modeCache.flatModel;
items: s.items ? s.items.slice() : [] } else {
var appSectionIds = ["favorites", "apps"];
var newSections = cachedSections.filter(function (s) {
return appSectionIds.indexOf(s.id) !== -1;
}).map(function (s) {
var copy = Object.assign({}, s, {
items: s.items ? s.items.slice() : []
});
if (collapsedSections[s.id] !== undefined)
copy.collapsed = collapsedSections[s.id];
return copy;
}); });
if (collapsedSections[s.id] !== undefined) _applyHighlights(newSections, "");
copy.collapsed = collapsedSections[s.id]; flatModel = Scorer.flattenSections(newSections);
return copy; sections = newSections;
}); _setCachedModeData("apps", sections, flatModel);
flatModel = Scorer.flattenSections(sections); }
selectedFlatIndex = restoreSelection(flatModel); selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem(); updateSelectedItem();
isSearching = false; isSearching = false;
@@ -501,17 +600,19 @@ Item {
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
sections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500); var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500);
for (var sid in collapsedSections) { for (var sid in collapsedSections) {
for (var i = 0; i < sections.length; i++) { for (var i = 0; i < newSections.length; i++) {
if (sections[i].id === sid) { if (newSections[i].id === sid) {
sections[i].collapsed = collapsedSections[sid]; newSections[i].collapsed = collapsedSections[sid];
} }
} }
} }
flatModel = Scorer.flattenSections(sections); _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel); selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem(); updateSelectedItem();
@@ -523,13 +624,15 @@ Item {
if (searchMode === "plugins") { if (searchMode === "plugins") {
if (!searchQuery && !pluginFilter) { if (!searchQuery && !pluginFilter) {
var browseItems = getPluginBrowseItems(); var browseItems = getPluginBrowseItems();
allItems = allItems.concat(browseItems); for (var k = 0; k < browseItems.length; k++)
allItems.push(browseItems[k]);
} else if (pluginFilter) { } else if (pluginFilter) {
var isBuiltInFilter = !!AppSearchService.builtInPlugins[pluginFilter]; var isBuiltInFilter = !!AppSearchService.builtInPlugins[pluginFilter];
applyActivePluginViewPreference(pluginFilter, isBuiltInFilter); applyActivePluginViewPreference(pluginFilter, isBuiltInFilter);
var filterItems = getPluginItems(pluginFilter, searchQuery); var filterItems = getPluginItems(pluginFilter, searchQuery);
allItems = allItems.concat(filterItems); for (var k = 0; k < filterItems.length; k++)
allItems.push(filterItems[k]);
var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery); var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery);
for (var j = 0; j < builtInItems.length; j++) { for (var j = 0; j < builtInItems.length; j++) {
@@ -540,7 +643,8 @@ Item {
for (var i = 0; i < emptyTriggerPlugins.length; i++) { for (var i = 0; i < emptyTriggerPlugins.length; i++) {
var pluginId = emptyTriggerPlugins[i]; var pluginId = emptyTriggerPlugins[i];
var pItems = getPluginItems(pluginId, searchQuery); var pItems = getPluginItems(pluginId, searchQuery);
allItems = allItems.concat(pItems); for (var k = 0; k < pItems.length; k++)
allItems.push(pItems[k]);
} }
var builtInLauncherPlugins = getBuiltInEmptyTriggerLaunchers(); var builtInLauncherPlugins = getBuiltInEmptyTriggerLaunchers();
@@ -556,17 +660,19 @@ Item {
var dynamicDefs = buildDynamicSectionDefs(allItems); var dynamicDefs = buildDynamicSectionDefs(allItems);
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
sections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500); var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500);
for (var sid in collapsedSections) { for (var sid in collapsedSections) {
for (var i = 0; i < sections.length; i++) { for (var i = 0; i < newSections.length; i++) {
if (sections[i].id === sid) { if (newSections[i].id === sid) {
sections[i].collapsed = collapsedSections[sid]; newSections[i].collapsed = collapsedSections[sid];
} }
} }
} }
flatModel = Scorer.flattenSections(sections); _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel); selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem(); updateSelectedItem();
@@ -577,31 +683,26 @@ Item {
var calculatorResult = evaluateCalculator(searchQuery); var calculatorResult = evaluateCalculator(searchQuery);
if (calculatorResult) { if (calculatorResult) {
calculatorResult._preScored = 12000;
allItems.push(calculatorResult); allItems.push(calculatorResult);
} }
var apps = searchApps(searchQuery); var apps = searchApps(searchQuery);
allItems = allItems.concat(apps); for (var i = 0; i < apps.length; i++) {
if (searchQuery)
apps[i]._preScored = 11000 - i;
allItems.push(apps[i]);
}
if (searchMode === "all") { if (searchMode === "all") {
var includePlugins = !searchQuery || searchQuery.length >= 2; if (searchQuery && searchQuery.length >= 2) {
if (searchQuery && includePlugins) { _pluginPhasePending = true;
var allPluginsOrdered = getAllVisiblePluginsOrdered(); _pluginPhaseForceFirst = shouldResetSelection;
var maxPerPlugin = 10; _phase1Items = allItems;
for (var i = 0; i < allPluginsOrdered.length; i++) { pluginPhaseTimer.restart();
var plugin = allPluginsOrdered[i]; isSearching = true;
if (plugin.isBuiltIn) { searchCompleted();
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); return;
var blLimit = Math.min(blItems.length, maxPerPlugin);
for (var j = 0; j < blLimit; j++)
allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id));
} else {
var pItems = getPluginItems(plugin.id, searchQuery);
if (pItems.length > maxPerPlugin)
pItems = pItems.slice(0, maxPerPlugin);
allItems = allItems.concat(pItems);
}
}
} else if (!searchQuery) { } else if (!searchQuery) {
var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered(); var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
for (var i = 0; i < emptyTriggerOrdered.length; i++) { for (var i = 0; i < emptyTriggerOrdered.length; i++) {
@@ -612,12 +713,14 @@ Item {
allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id)); allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id));
} else { } else {
var pItems = getPluginItems(plugin.id, searchQuery); var pItems = getPluginItems(plugin.id, searchQuery);
allItems = allItems.concat(pItems); for (var j = 0; j < pItems.length; j++)
allItems.push(pItems[j]);
} }
} }
var browseItems = getPluginBrowseItems(); var browseItems = getPluginBrowseItems();
allItems = allItems.concat(browseItems); for (var i = 0; i < browseItems.length; i++)
allItems.push(browseItems[i]);
} }
} }
@@ -644,16 +747,76 @@ Item {
} }
} }
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections; sections = newSections;
flatModel = Scorer.flattenSections(sections);
if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) { if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) {
AppSearchService.setCachedDefaultSections(sections, flatModel); AppSearchService.setCachedDefaultSections(sections, flatModel);
_saveDiskCache(sections);
} }
selectedFlatIndex = restoreSelection(flatModel); selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem(); updateSelectedItem();
isSearching = _pluginPhasePending;
searchCompleted();
}
function _performPluginPhase() {
_pluginPhasePending = false;
if (!searchQuery || searchQuery.length < 2 || searchMode !== "all")
return;
var currentVersion = _searchVersion;
var restoreSelection = preserveSelectionAfterUpdate(_pluginPhaseForceFirst);
var allItems = _phase1Items;
_phase1Items = [];
var allPluginsOrdered = getAllVisiblePluginsOrdered();
var maxPerPlugin = 10;
for (var i = 0; i < allPluginsOrdered.length; i++) {
if (currentVersion !== _searchVersion)
return;
var plugin = allPluginsOrdered[i];
if (plugin.isBuiltIn) {
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
var blLimit = Math.min(blItems.length, maxPerPlugin);
for (var j = 0; j < blLimit; j++) {
var item = transformBuiltInLauncherItem(blItems[j], plugin.id);
item._preScored = 900 - j;
allItems.push(item);
}
} else {
var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin);
for (var j = 0; j < pItems.length; j++) {
pItems[j]._preScored = 900 - j;
allItems.push(pItems[j]);
}
}
}
if (currentVersion !== _searchVersion)
return;
var dynamicDefs = buildDynamicSectionDefs(allItems);
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, false, 50);
if (currentVersion !== _searchVersion)
return;
for (var i = 0; i < newSections.length; i++) {
var sid = newSections[i].id;
if (collapsedSections[sid] !== undefined)
newSections[i].collapsed = collapsedSections[sid];
}
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = restoreSelection(flatModel);
updateSelectedItem();
isSearching = false; isSearching = false;
searchCompleted(); searchCompleted();
} }
@@ -704,7 +867,8 @@ Item {
icon: "folder", icon: "folder",
priority: 4, priority: 4,
items: fileItems, items: fileItems,
collapsed: collapsedSections["files"] || false collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
}; };
var newSections; var newSections;
@@ -723,9 +887,9 @@ Item {
newSections.sort(function (a, b) { newSections.sort(function (a, b) {
return a.priority - b.priority; return a.priority - b.priority;
}); });
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections; sections = newSections;
flatModel = Scorer.flattenSections(sections);
if (selectedFlatIndex >= flatModel.length) { if (selectedFlatIndex >= flatModel.length) {
selectedFlatIndex = getFirstItemIndex(); selectedFlatIndex = getFirstItemIndex();
} }
@@ -921,11 +1085,12 @@ Item {
return sortPluginIdsByOrder(visible); return sortPluginIdsByOrder(visible);
} }
function getPluginItems(pluginId, query) { function getPluginItems(pluginId, query, limit) {
var items = AppSearchService.getPluginItemsForPlugin(pluginId, query); var items = AppSearchService.getPluginItemsForPlugin(pluginId, query);
var count = limit > 0 && limit < items.length ? limit : items.length;
var transformed = []; var transformed = [];
for (var i = 0; i < items.length; i++) { for (var i = 0; i < count; i++) {
transformed.push(transformPluginItem(items[i], pluginId)); transformed.push(transformPluginItem(items[i], pluginId));
} }
@@ -1055,6 +1220,105 @@ Item {
return Nav.getFirstItemIndex(flatModel); return Nav.getFirstItemIndex(flatModel);
} }
function _getCachedModeData(mode) {
return _modeSectionsCache[mode] || null;
}
function _setCachedModeData(mode, sectionsData, flatModelData) {
var cache = Object.assign({}, _modeSectionsCache);
cache[mode] = {
sections: sectionsData,
flatModel: flatModelData
};
_modeSectionsCache = cache;
}
function _clearModeCache() {
_modeSectionsCache = {};
}
function _saveDiskCache(sectionsData) {
var serializable = [];
for (var i = 0; i < sectionsData.length; i++) {
var s = sectionsData[i];
var items = [];
var srcItems = s.items || [];
for (var j = 0; j < srcItems.length; j++) {
var it = srcItems[j];
items.push({
id: it.id,
type: it.type,
name: it.name || "",
subtitle: it.subtitle || "",
icon: it.icon || "",
iconType: it.iconType || "image",
iconFull: it.iconFull || "",
section: it.section || "",
isCore: it.isCore || false,
isBuiltInLauncher: it.isBuiltInLauncher || false,
pluginId: it.pluginId || ""
});
}
serializable.push({
id: s.id,
title: s.title || "",
icon: s.icon || "",
priority: s.priority || 0,
items: items
});
}
CacheData.saveLauncherCache(serializable);
}
function _loadDiskCache() {
var cached = CacheData.loadLauncherCache();
if (!cached || !Array.isArray(cached) || cached.length === 0)
return null;
var sectionsData = [];
for (var i = 0; i < cached.length; i++) {
var s = cached[i];
var items = [];
var srcItems = s.items || [];
for (var j = 0; j < srcItems.length; j++) {
var it = srcItems[j];
items.push({
id: it.id || "",
type: it.type || "app",
name: it.name || "",
subtitle: it.subtitle || "",
icon: it.icon || "",
iconType: it.iconType || "image",
iconFull: it.iconFull || "",
section: it.section || "",
isCore: it.isCore || false,
isBuiltInLauncher: it.isBuiltInLauncher || false,
pluginId: it.pluginId || "",
data: {
id: it.id
},
actions: [],
primaryAction: null,
_diskCached: true,
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
});
}
sectionsData.push({
id: s.id || "",
title: s.title || "",
icon: s.icon || "",
priority: s.priority || 0,
items: items,
collapsed: false,
flatStartIndex: 0
});
}
return sectionsData;
}
function updateSelectedItem() { function updateSelectedItem() {
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) { if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
var entry = flatModel[selectedFlatIndex]; var entry = flatModel[selectedFlatIndex];
@@ -1064,6 +1328,48 @@ Item {
} }
} }
function _applyHighlights(sectionsData, query) {
if (!query || query.length === 0) {
for (var i = 0; i < sectionsData.length; i++) {
var items = sectionsData[i].items;
for (var j = 0; j < items.length; j++) {
var item = items[j];
item._hName = item.name || "";
item._hSub = item.subtitle || "";
item._hRich = false;
}
}
return;
}
var highlightColor = Theme.primary;
var nameColor = Theme.surfaceText;
var subColor = Theme.surfaceVariantText;
var lowerQuery = query.toLowerCase();
for (var i = 0; i < sectionsData.length; i++) {
var items = sectionsData[i].items;
for (var j = 0; j < items.length; j++) {
var item = items[j];
item._hName = _highlightField(item.name || "", lowerQuery, query.length, nameColor, highlightColor);
item._hSub = _highlightField(item.subtitle || "", lowerQuery, query.length, subColor, highlightColor);
item._hRich = true;
}
}
}
function _highlightField(text, lowerQuery, queryLen, baseColor, highlightColor) {
if (!text)
return "";
var idx = text.toLowerCase().indexOf(lowerQuery);
if (idx === -1)
return text;
var before = text.substring(0, idx);
var match = text.substring(idx, idx + queryLen);
var after = text.substring(idx + queryLen);
return '<span style="color:' + baseColor + '">' + before + '</span><span style="color:' + highlightColor + '; font-weight:600">' + match + '</span><span style="color:' + baseColor + '">' + after + '</span>';
}
function getCurrentSectionViewMode() { function getCurrentSectionViewMode() {
if (selectedFlatIndex < 0 || selectedFlatIndex >= flatModel.length) if (selectedFlatIndex < 0 || selectedFlatIndex >= flatModel.length)
return "list"; return "list";
@@ -1077,8 +1383,14 @@ Item {
return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns); return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns);
} }
function _cancelPendingSelectionReset() {
_queryDrivenSearch = false;
_pluginPhaseForceFirst = false;
}
function selectNext() { function selectNext() {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
@@ -1088,6 +1400,7 @@ Item {
function selectPrevious() { function selectPrevious() {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
@@ -1097,6 +1410,7 @@ Item {
function selectRight() { function selectRight() {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode); var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode);
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
@@ -1106,6 +1420,7 @@ Item {
function selectLeft() { function selectLeft() {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode); var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode);
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
@@ -1115,6 +1430,7 @@ Item {
function selectNextSection() { function selectNextSection() {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex); var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex);
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
@@ -1124,6 +1440,7 @@ Item {
function selectPreviousSection() { function selectPreviousSection() {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex); var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex);
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
@@ -1133,6 +1450,7 @@ Item {
function selectPageDown(visibleItems) { function selectPageDown(visibleItems) {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems); var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems);
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
@@ -1142,6 +1460,7 @@ Item {
function selectPageUp(visibleItems) { function selectPageUp(visibleItems) {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems); var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems);
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
@@ -1158,6 +1477,7 @@ Item {
} }
function toggleSection(sectionId) { function toggleSection(sectionId) {
_clearModeCache();
var newCollapsed = Object.assign({}, collapsedSections); var newCollapsed = Object.assign({}, collapsedSections);
var currentState = newCollapsed[sectionId]; var currentState = newCollapsed[sectionId];
@@ -1181,10 +1501,9 @@ Item {
}); });
} }
} }
flatModel = Scorer.flattenSections(newSections);
sections = newSections; sections = newSections;
flatModel = Scorer.flattenSections(sections);
if (selectedFlatIndex >= flatModel.length) { if (selectedFlatIndex >= flatModel.length) {
selectedFlatIndex = getFirstItemIndex(); selectedFlatIndex = getFirstItemIndex();
} }
@@ -1192,6 +1511,10 @@ Item {
} }
function executeSelected() { function executeSelected() {
if (searchDebounce.running) {
searchDebounce.stop();
performSearch();
}
if (!selectedItem) if (!selectedItem)
return; return;
executeItem(selectedItem); executeItem(selectedItem);
@@ -4,7 +4,6 @@ import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -14,10 +13,14 @@ Item {
property bool spotlightOpen: false property bool spotlightOpen: false
property bool keyboardActive: false property bool keyboardActive: false
property bool contentVisible: false property bool contentVisible: false
property alias spotlightContent: launcherContent property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false property bool openedFromOverview: false
property bool isClosing: false property bool isClosing: false
property bool _windowEnabled: true property bool _windowEnabled: true
property bool _pendingInitialize: false
property string _pendingQuery: ""
property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: launcherWindow.screen readonly property var effectiveScreen: launcherWindow.screen
@@ -76,7 +79,22 @@ Item {
signal dialogClosed signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) {
_pendingQuery = query || "";
_pendingMode = mode || "";
_pendingInitialize = true;
contentVisible = true;
launcherContentLoader.active = true;
if (spotlightContent) {
_initializeAndShow(_pendingQuery, _pendingMode);
_pendingInitialize = false;
}
}
function _initializeAndShow(query, mode) { function _initializeAndShow(query, mode) {
if (!spotlightContent)
return;
contentVisible = true; contentVisible = true;
spotlightContent.searchField.forceActiveFocus(); spotlightContent.searchField.forceActiveFocus();
@@ -90,6 +108,8 @@ Item {
spotlightContent.controller.activePluginName = ""; spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = ""; spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.collapsedSections = {}; spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null;
if (query) { if (query) {
spotlightContent.controller.setSearchQuery(query); spotlightContent.controller.setSearchQuery(query);
} else { } else {
@@ -120,7 +140,7 @@ Item {
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
_initializeAndShow(""); _ensureContentLoadedAndInitialize("", "");
} }
function showWithQuery(query) { function showWithQuery(query) {
@@ -138,7 +158,7 @@ Item {
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
_initializeAndShow(query); _ensureContentLoadedAndInitialize(query, "");
} }
function hide() { function hide() {
@@ -175,7 +195,7 @@ Item {
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
_initializeAndShow("", mode); _ensureContentLoadedAndInitialize("", mode);
} }
function toggleWithMode(mode) { function toggleWithMode(mode) {
@@ -196,10 +216,12 @@ Item {
Timer { Timer {
id: closeCleanupTimer id: closeCleanupTimer
interval: Theme.expressiveDurations.expressiveFastSpatial + 50 interval: Theme.modalAnimationDuration + 50
repeat: false repeat: false
onTriggered: { onTriggered: {
isClosing = false; isClosing = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed(); dialogClosed();
} }
} }
@@ -262,7 +284,7 @@ Item {
PanelWindow { PanelWindow {
id: launcherWindow id: launcherWindow
visible: root._windowEnabled visible: root._windowEnabled && (!root.unloadContentOnClose || spotlightOpen || isClosing)
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
@@ -307,10 +329,9 @@ Item {
visible: contentVisible || opacity > 0 visible: contentVisible || opacity > 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { DankAnim {
duration: Theme.expressiveDurations.expressiveFastSpatial duration: Theme.modalAnimationDuration
easing.type: Easing.BezierSpline easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.emphasized
} }
} }
} }
@@ -343,26 +364,24 @@ Item {
transformOrigin: Item.Center transformOrigin: Item.Center
Behavior on opacity { Behavior on opacity {
NumberAnimation { DankAnim {
duration: Theme.expressiveDurations.fast duration: Theme.modalAnimationDuration
easing.type: Easing.BezierSpline easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
} }
} }
Behavior on scale { Behavior on scale {
NumberAnimation { DankAnim {
duration: Theme.expressiveDurations.fast duration: Theme.modalAnimationDuration
easing.type: Easing.BezierSpline easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
} }
} }
DankRectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: root.backgroundColor color: root.backgroundColor
borderColor: root.borderColor border.color: root.borderColor
borderWidth: root.borderWidth border.width: root.borderWidth
radius: root.cornerRadius radius: root.cornerRadius
} }
@@ -375,10 +394,22 @@ Item {
anchors.fill: parent anchors.fill: parent
focus: keyboardActive focus: keyboardActive
LauncherContent { Loader {
id: launcherContent id: launcherContentLoader
anchors.fill: parent anchors.fill: parent
parentModal: root active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
} }
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
+14 -2
View File
@@ -38,6 +38,12 @@ Rectangle {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent" color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
DankRipple {
id: rippleLayer
rippleColor: Theme.surfaceText
cornerRadius: root.radius
}
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
@@ -55,11 +61,13 @@ Rectangle {
materialIconSizeAdjustment: root.computedIconSize * 0.3 materialIconSizeAdjustment: root.computedIconSize * 0.3
} }
StyledText { Text {
width: parent.width width: parent.width
text: root.item?.name ?? "" text: root.item?._hName ?? root.item?.name ?? ""
textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
font.family: Theme.fontFamily
color: root.isSelected ? Theme.primary : Theme.surfaceText color: root.isSelected ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -75,6 +83,10 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: mouse => {
if (mouse.button === Qt.LeftButton)
rippleLayer.trigger(mouse.x, mouse.y);
}
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
var scenePos = mapToItem(null, mouse.x, mouse.y); var scenePos = mapToItem(null, mouse.x, mouse.y);
@@ -1,6 +1,6 @@
.pragma library .pragma library
.import "ControllerUtils.js" as Utils .import "ControllerUtils.js" as Utils
function transformApp(app, override, defaultActions, primaryActionLabel) { function transformApp(app, override, defaultActions, primaryActionLabel) {
var appId = app.id || app.execString || app.exec || ""; var appId = app.id || app.execString || app.exec || "";
@@ -31,7 +31,11 @@ function transformApp(app, override, defaultActions, primaryActionLabel) {
name: primaryActionLabel, name: primaryActionLabel,
icon: "open_in_new", icon: "open_in_new",
action: "launch" action: "launch"
} },
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
}; };
} }
@@ -66,7 +70,11 @@ function transformCoreApp(app, openLabel) {
name: openLabel, name: openLabel,
icon: "open_in_new", icon: "open_in_new",
action: "launch" action: "launch"
} },
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
}; };
} }
@@ -100,7 +108,11 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
name: openLabel, name: openLabel,
icon: "open_in_new", icon: "open_in_new",
action: "execute" action: "execute"
} },
_hName: "",
_hSub: "",
_hRich: false,
_preScored: item._preScored
}; };
} }
@@ -133,7 +145,11 @@ function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
name: openLabel, name: openLabel,
icon: "open_in_new", icon: "open_in_new",
action: "open" action: "open"
} },
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
}; };
} }
@@ -166,7 +182,11 @@ function transformPluginItem(item, pluginId, selectLabel) {
name: selectLabel, name: selectLabel,
icon: "check", icon: "check",
action: "execute" action: "execute"
} },
_hName: "",
_hSub: "",
_hRich: false,
_preScored: item._preScored
}; };
} }
@@ -188,7 +208,11 @@ function createCalculatorItem(calc, query, copyLabel) {
name: copyLabel, name: copyLabel,
icon: "content_copy", icon: "content_copy",
action: "copy" action: "copy"
} },
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
}; };
} }
@@ -218,6 +242,10 @@ function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed,
name: browseLabel, name: browseLabel,
icon: "arrow_forward", icon: "arrow_forward",
action: "browse_plugin" action: "browse_plugin"
} },
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
}; };
} }
@@ -86,6 +86,7 @@ FocusScope {
Controller { Controller {
id: controller id: controller
active: root.parentModal?.spotlightOpen ?? true
viewModeContext: root.viewModeContext viewModeContext: root.viewModeContext
onItemExecuted: { onItemExecuted: {
@@ -287,7 +288,7 @@ FocusScope {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: -Theme.cornerRadius anchors.topMargin: -Theme.cornerRadius
color: Theme.surfaceContainerHigh color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
} }

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