1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

Compare commits

...

796 Commits

Author SHA1 Message Date
LuckShiba
17ec547737 core: remove unused function 2026-01-15 19:40:14 -03:00
LuckShiba
f9a2ed025f doctor: show docs URL for failed checks 2026-01-15 19:24:57 -03:00
LuckShiba
f8cfbdcd90 doctor: use dbus for checking on services 2026-01-15 19:15:06 -03:00
Ivan Molodetskikh
623eec3689 Add screencast indicator for niri (#1361)
* niri: Handle new Cast events

* bar: Add screen sharing indicator

Configurable like other icons; on by default.

* lockscreen: Add screen sharing indicator
2026-01-15 00:55:21 -05:00
bbedward
53a033fe35 dankdash: fix weather open IPC
fixes #1367
2026-01-14 22:29:29 -05:00
bbedward
c490ee24f4 matugen: fix nvim ID in skipTemplates 2026-01-14 22:27:07 -05:00
bbedward
cc1e49294e i18n: update terms 2026-01-14 22:22:27 -05:00
purian23
e6fa46ae26 dankdash: Center Media Art & Controls 2026-01-14 18:03:16 -05:00
purian23
35fe774a1b Update OBS Choice selection 2026-01-13 17:51:55 -05:00
purian23
1e6a0f9423 Update OBS DMS Stable workflow 2026-01-13 17:31:01 -05:00
bbedward
cc1877aadb modals: fix wifi passowrd, polkit, and VPN import 2026-01-13 17:21:16 -05:00
bbedward
f1eb1fa9ba settings: fix child windows on newer quickshell-git 2026-01-13 16:57:23 -05:00
Lucas
bdd01e335d settings: fix modal not opening on latest quickshell (#1357) 2026-01-13 16:41:54 -05:00
Lucas
4b7baf82cd nix: escape version string (#1353) 2026-01-13 11:24:51 -05:00
purian23
15c88ce1d2 quickshell: Despace Versioning 2026-01-13 11:07:12 -05:00
bbedward
8891c388d0 bump to v1.4-unstable 2026-01-13 08:40:56 -05:00
bbedward
cd9d92d884 update changelog link and VERSION 2026-01-13 08:31:50 -05:00
Lucas
1b69a5e62b nix: add wtype dependency (#1346) 2026-01-13 08:27:46 -05:00
bbedward
61d311b157 widgets: fix running apps positioning and popup manager 2026-01-13 08:26:29 -05:00
bbedward
6b76b86930 notifications: remove redundant trimStored and add null safety 2026-01-12 23:37:49 -05:00
bbedward
dcfb947c36 desktop widgets: sync position across screens option, clickthrough
option, grouping in settings, repositioning, new IPCs for control
fixes #1300
fixes #1301
2026-01-12 15:31:34 -05:00
bbedward
59893b7f44 notifications: use Theme.primary to represent do not distrub in bar 2026-01-12 11:57:42 -05:00
bbedward
d2c62f5533 matugen: add support for vscode-insiders 2026-01-12 11:46:29 -05:00
bbedward
2bbe9a0c45 core/wlcontext: use infinite poll timeout 2026-01-12 11:26:35 -05:00
bbedward
4e2ce82c0a notifications: swipe to dismiss on history 2026-01-12 11:08:22 -05:00
bbedward
104762186f widgets: respect radius for inactive DankButtonGroup i tems 2026-01-12 10:26:50 -05:00
bbedward
f1233ab1e3 matugen: add post_hook for mango 2026-01-12 10:05:19 -05:00
bbedward
d6b407ec37 settings: fix wallpaper preview cache update on per-mode change 2026-01-12 09:58:58 -05:00
bbedward
022b4b4bb3 enable changelog 2026-01-12 09:46:50 -05:00
bbedward
49b322582d keybinds: fix sh, fix screenshot-window options, empty args
part of #914
2026-01-12 09:35:30 -05:00
bbedward
1280bd047d settings: fix sidebar binding when clicked by emitting signal 2026-01-11 22:43:29 -05:00
bbedward
6f206d7523 dankdash: fix 24H format in weather tab
fixes #1283
2026-01-11 21:45:28 -05:00
bbedward
2e58283859 dgop: use used mem directly from API
- conditionally because it depends on newer dgop
2026-01-11 17:32:36 -05:00
Marcus Ramberg
99a5721fe8 settings: extract tab headings for search (#1333)
* settings: extract tab headings for search

* fix pre-commit

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-11 17:14:45 -05:00
bbedward
5302ebd840 notifications: spacing improvements
fixes #1241
2026-01-11 14:35:34 -05:00
bbedward
fa427ea1ac settings: fix clipping of generic color selector
fixes #1242
2026-01-11 14:04:48 -05:00
bbedward
7027bd1646 systemtray: use Theme radius for menu options
fixes #1331
2026-01-11 14:03:23 -05:00
bbedward
3c38e17472 notifications: add compact mode, expansion in history, expansion in
popup
fixes #1282
2026-01-11 12:11:44 -05:00
shalevc1098
510ea5d2e4 feat: configurable app id substitutions (#1317)
* feat: add configurable app ID substitutions setting

* feat: add live icon updates when substitutions change

* fix: cursor not showing on headerActions in non-collapsible cards

* fix: address PR review feedback

- add tags for search index
- remove hardcoded height from text fields
2026-01-10 21:00:15 -05:00
bbedward
bb2234d328 cc: dont show preference flip if not on ethernet and wifi 2026-01-10 10:35:48 -05:00
bbedward
edbdeb0fb8 widgets: add artix and void NF mappings 2026-01-10 10:18:09 -05:00
Kostiantyn To
19541fc573 update-service: add Artix Linux to supported distributions list (#1318) 2026-01-10 10:18:00 -05:00
bbedward
7c936cacfb niri: fix effectiveScreenAssignment in modal 2026-01-10 10:13:41 -05:00
bbedward
c60cd3a341 modals/auth: add show password option
fixes #1311
2026-01-09 22:20:18 -05:00
shalevc1098
e37135f80d feat: map steam_app_ID to steam_icon_ID for actual game icons (#1312)
Steam Proton games use window class steam_app_XXXXX. Steam installs
icons as steam_icon_XXXXX. This maps between them so actual game
icons display instead of generic controller fallback.
2026-01-09 21:40:35 -05:00
bbedward
aac937cbcc settingns: fix missing help text on desktop widgets 2026-01-09 19:07:37 -05:00
bbedward
4b46d022af workspaces: add color options, add focus follows monitor, remove
per-monitor option (was misleading)
relevant to #1207
2026-01-09 14:10:57 -05:00
bbedward
7f0181b310 matugen/vscode: fix selection contrast 2026-01-09 10:16:03 -05:00
bbedward
6a109274f8 hyprland: always use single window 2026-01-09 09:57:31 -05:00
bbedward
0f09cc693a lock: handle case where session lock is rejected 2026-01-09 09:46:39 -05:00
bbedward
af0166a553 dankbar: add bar get/setPosition IPC 2026-01-09 00:09:49 -05:00
bbedward
a283017f26 audio: recreate media players on pipewire device change 2026-01-08 23:35:42 -05:00
bbedward
5ae2cd1dfb i18n: fix RTL in plugin settings 2026-01-08 19:16:55 -05:00
bbedward
eece811fb0 i18n: more RTL repairs 2026-01-08 18:45:38 -05:00
bbedward
1ff1f3a7f2 i18n: more RTL layout enhancements 2026-01-08 16:11:30 -05:00
bbedward
a21a846bf5 wallpaper: encode image URIs
fixes #1306
2026-01-08 14:32:12 -05:00
Anton Kesy
f5f21e738a fix typos (#1304) 2026-01-08 14:10:24 -05:00
bbedward
033e62418a hyprland: fix cursor setting 2026-01-08 09:30:52 -05:00
bbedward
3c69e8b1cc revert readme 2026-01-07 22:59:28 -05:00
bbedward
118be27796 update readme 2026-01-07 22:56:16 -05:00
bbedward
721d35d417 readme:update vid url 2026-01-07 22:54:38 -05:00
bbedward
7bc3d5910d settings: fade to lock and monitor off by default on 2026-01-07 21:31:12 -05:00
bbedward
ccc7047be0 welcome: make the first page stuff clickable
fixes #1295
2026-01-07 21:22:15 -05:00
bbedward
a5e107c89d changelog: capability to display new release message 2026-01-07 20:15:50 -05:00
bbedward
646d60dcbf displays: fix text-alignment in model mode 2026-01-07 16:54:31 -05:00
bbedward
5dc7c0d797 core: add resolve-include recursive
fixes #1294
2026-01-07 16:45:31 -05:00
bbedward
db1de9df38 keybinds: fix empty string args, more writable provider options 2026-01-07 15:38:44 -05:00
bbedward
3dd21382ba network: support hidden SSIDs 2026-01-07 14:13:03 -05:00
bbedward
ec2b3d0d4b vpn: aggregate all import errors
- we are dumb about importing by just trying to import everythting
- that caused errors to not be represented correctly
- just aggregate them all and present them in toast details
- Better would be to detect the type of file being imported, but this is
  better than nothing
2026-01-07 13:22:56 -05:00
bbedward
a205df1bd6 keybinds: initial support for writable hyprland and mangoWC
fixes #1204
2026-01-07 12:15:38 -05:00
bbedward
e822fa73da cursor: make min/max wider 2026-01-07 10:04:47 -05:00
bbedward
634e75b80c plugins: improve version check 2026-01-07 09:46:55 -05:00
bbedward
ec5b507efc greeter: change hypr startup to exec-once 2026-01-07 09:18:32 -05:00
bbedward
e6d289d48c workflow: update stable workflow to use GH app 2026-01-06 22:45:34 -05:00
bbedward
745d7f26ce cursor: create/update XResources for XWL apps 2026-01-06 22:06:01 -05:00
bbedward
ad43053b94 cursor: hypr, mango, and dankinstall support for configs 2026-01-06 20:35:22 -05:00
purian23
721700190b feat: DMS Cursor Control - Size & Theme in niri 2026-01-06 19:08:05 -05:00
bbedward
8c9c936d0e clipboard: add cliphist-migrate CLI 2026-01-06 16:49:18 -05:00
dms-ci[bot]
842bf6e3ff nix: update vendorHash for go.mod changes 2026-01-06 21:03:38 +00:00
bbedward
c1fbeb3f5e network: listen to NM Wired interface + use nmcli for route metrics
- Some other misc floating window change, too lazy to separate the
  commit
2026-01-06 16:01:28 -05:00
bbedward
c45eb2cccf plugins: ipc visibility conditions 2026-01-06 13:22:36 -05:00
bbedward
1b5abca83a launcher remove right key 2026-01-06 10:37:58 -05:00
bbedward
45818b202f launcher: support for plugins to define context menus
fixes #1279
2026-01-06 10:08:22 -05:00
Ethan Todd
1c8ce46f25 notifications: fix notifications being completely transient if history is disabled (#1284) 2026-01-06 09:39:33 -05:00
bbedward
f762f9ae49 theme: fix gtk apply button on empty file
fixes #1280
2026-01-05 21:56:50 -05:00
bbedward
4484f6bd61 launcher: built-in plugins, add settings search plugin with ? default
trigger
2026-01-05 21:46:12 -05:00
purian23
0076c45496 shell: dmsCoreApp updates 2026-01-05 20:31:06 -05:00
bbedward
ab071e12aa icons: fix transmission-gtk modded app ID again 2026-01-05 16:44:31 -05:00
bbedward
8386b40c50 launcher: F10 as alt for menu key 2026-01-05 14:58:50 -05:00
bbedward
03a985228d dankbar: add shadow option
fixes #916
2026-01-05 13:43:15 -05:00
bbedward
ef7d7ec13d desktop widgets: niri overview only option + grid on overlay when on
overview
2026-01-05 13:01:10 -05:00
bbedward
824792cca7 notifications: add support for none, count, app name, and full detail
for lock screen
fixes #557
2026-01-05 12:22:05 -05:00
bbedward
850e5b6572 session: handle hibernate error
fixes #308
2026-01-05 12:01:17 -05:00
bbedward
64310854a6 compositor+matugen: border override, hypr/mango layout overrides, new
templates, respect XDG paths
- Add Hyprland and MangoWC templates
- Add GUI gaps, window radius, and border thickness overrides for niri,
  Hyprland, and MangoWC
- Add replacement support in matugen templates for DATA_DIR, CACHE_DIR,
  CONFIG_DIR
fixes #1274
fixes #1273
2026-01-05 11:25:13 -05:00
bbedward
4005a55bf2 session: blockLoading true 2026-01-05 09:11:19 -05:00
bbedward
0236fe3276 session: fix persist on empty file 2026-01-05 09:07:00 -05:00
bbedward
c1d95a3086 launcher: fix invalid icon rendering wrong icon 2026-01-04 22:58:20 -05:00
bbedward
9b027df1d5 doctor: add links to dr command 2026-01-04 22:44:19 -05:00
purian23
5e03afe7f0 feat: Implement DMS Core Persistent Apps 2026-01-04 22:33:50 -05:00
bbedward
145a974b6d welcome: add IPC targets and button on about page 2026-01-04 21:45:02 -05:00
bbedward
d23fc9f2df welcome: add a first launch welcome page with doctor integration
fixes #760
2026-01-04 19:07:34 -05:00
bbedward
7ac5191e8d matugen: fix app checking
- double nil for flatpak + bin required to skip
2026-01-04 17:53:47 -05:00
bbedward
29d27ebd6d mautgen: update vscode package 2026-01-04 17:19:51 -05:00
bbedward
e45075dd84 launcher: fix binding loop 2026-01-04 17:19:35 -05:00
bbedward
80bc87e76b clock: fixed width chars in vertical mode 2026-01-04 13:20:20 -05:00
bbedward
76d88517ec matugen: publish vscode theme to marketplace/ovsix 2026-01-04 13:07:23 -05:00
bbedward
151d695212 launcher: optimize bindings and filters 2026-01-04 11:49:24 -05:00
Ethan Todd
2e1bed5fb5 nix: update home-manager module to remove default*, add clsettings (#1233)
* nix: update home-manager module, add clsettings

* nix: resolve message and rename clsettings->clipboardSettings.

* nix: fix home-manager plugin_settings management. add option for whether plugin settings should be managed by nix.
2026-01-04 11:18:28 -03:00
Lucas
f163b97c17 doctor: add json output (#1263)
* doctor: add json output

* doctor: fix systemd failed state as ok
2026-01-03 20:41:10 -05:00
bbedward
436c99927e settings: detect read-only on save attempts 2026-01-03 20:34:36 -05:00
bbedward
aa72eacae7 notifications: add image persistence 2026-01-03 19:56:08 -05:00
bbedward
913bb2ff67 niri: ensure outputs.kdl and binds.kdl exist 2026-01-03 18:31:36 -05:00
Lucas
3bb2696263 Add doctor command (#1259)
* feat: doctor command

* doctor: use console.warn for quickshell feature logs

* doctor: show compositor, quickshell and cli path in verbose

* doctor: show useful env variables

* doctor: indicate if config files are readonly

* doctor: add power-profiles-daemon and i2c

* doctor: cleanup code

* doctor: fix icon theme env variable

* doctor: use builtin config/cache dir functions

* doctor: refactor to use DoctorStatus struct and 'enum'

* doctor: use network backend detector
2026-01-03 18:28:23 -05:00
bbedward
166843ded4 niri: preserve remaining settings when turning off output 2026-01-03 16:17:16 -05:00
Ryan Bateman
02166a4ca5 feat: matugen detects flatpak installations of zenbrowser and vesktop (#1251)
* feat: matugen detects flatpak installations of zenbrowser and vesktop

* fix: add flatpak deps on precommit runner

* fix: address short circuit conditions
2026-01-03 15:28:39 -05:00
bbedward
f0f2e6ef72 i18n: update terms 2026-01-03 15:20:34 -05:00
bbedward
8d8d5de5fd matugen: update vscode template
- yaml/toml highlighting colors
- fix scrollbar contrast
- fix command-search marker
2026-01-03 15:10:38 -05:00
bbedward
6d76f0b476 power: add fade to monitor off option
fixes #558
2026-01-03 15:00:12 -05:00
bbedward
f3f720bb37 settings: fix network refresh button animation behavior
fixes #1258
2026-01-03 14:37:27 -05:00
bbedward
2bf85bc4dd motifications: add support for configurable persistent history
fixes #929
2026-01-03 13:08:48 -05:00
bbedward
faddc46185 core: respect QT_LOGGING_RULES var 2026-01-03 11:05:47 -05:00
bbedward
2991aac82e printers: fix input field height
fixes #1254
2026-01-03 10:54:53 -05:00
bbedward
e1817027b1 settings: add existence check in addition to RO check 2026-01-02 22:36:37 -05:00
bbedward
ba2d51bcbb core: initialize fd pipes in tests and increase queue size in test 2026-01-02 22:30:42 -05:00
Sparsh Mishra
7f10d6a9b8 Add media control bindings for audio playback (#1240)
* Add media control bindings for audio playback

* Update niri-binds.kdl for audio controls

Added play pause prev next controls for niri too
2026-01-02 22:25:21 -05:00
bbedward
405749aa98 theme: unconditionally load dms-colors.json 2026-01-02 22:01:04 -05:00
bbedward
77681fd387 launcher: allow terminal apps 2026-01-02 21:56:56 -05:00
bbedward
8253ec4496 theme: add dank16 to dms matugen template 2026-01-02 21:37:48 -05:00
bbedward
a1e001e640 i18n: update terms 2026-01-02 19:35:02 -05:00
bbedward
3a65ea21ba plugins: fix first plugin install reactivity 2026-01-02 19:22:04 -05:00
NikSne
7d761c4c9a feat(distro/nix/niri): add a hack for config includes with niri flake (#1239)
It works fine but needs all dms-generated config files to be present
2026-01-03 00:43:39 +01:00
Phil Jackson
4cb90c5367 Bar (mediaplayer): Mouse wheel options for media player widget (#1248)
* Add different options for scroll on media widget.

* Nicer lookup code.

* Remove some checks I didn't need.

* Update the search tags.

* EOF.
2026-01-02 17:08:42 -05:00
Ryan Bateman
1c7d15db0b util: add flatpak introspection utilities (#1234)
ci: run apt as sudo

ci: fix flatpak remote in runner

ci: flatpak install steps in runner

ci: specific version of freedesktop

ci: freedesktop install perms
2026-01-02 16:07:32 -05:00
vha
7268a3fe7f feat: Add group workspace apps toggle (#1238)
* Add group workspace apps toggle

* wording

* fix pre-commit

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-02 15:55:51 -05:00
pcortellezzi
d2c4391514 feat: Persistent Plugins & Async Updates (#1231)
- PluginService: maintain persistent instances for Launcher plugins
- AppSearchService: reuse persistent instances for queries
- Added requestLauncherUpdate signal for async UI refreshes
2026-01-02 15:49:04 -05:00
sweenu
69b1d0c2da bar(ws): add option to show name (#1223) 2026-01-02 15:47:33 -05:00
sweenu
ba28767492 bar(clock): respect compact mode on vertical bar (#1222) 2026-01-02 15:46:33 -05:00
bbedward
6cff5f1146 settings: prevent overwrites if parse called with null object 2026-01-02 15:45:31 -05:00
bbedward
3e1c6534bd matugen: add GTKTheme method on type alias 2026-01-01 23:22:13 -05:00
bbedward
c1d57946d9 matugen: fix adw-gtk3 setting in light mode
- and add models.Get/GetOr helpers
2026-01-01 23:13:12 -05:00
bbedward
5e111d89a5 gamma: recreate controls on resume 2026-01-01 22:50:25 -05:00
Phil Jackson
1a98da22b2 Larger option for the media player widget. (#1236) 2026-01-01 22:12:37 -05:00
johngalt
618ccbcb2f zen-userchrome.css - fixing workspaces container color (#1194) 2026-01-01 22:03:59 -05:00
Body
d3a79a055e tweak background and popout colors to be brighter and more similar to adwaita (#1237) 2026-01-01 21:44:50 -05:00
bbedward
bae32e51ff core: skip display filtering in IPC 2026-01-01 15:24:55 -05:00
bbedward
edfda965e9 core: prevent stale path file 2026-01-01 14:04:58 -05:00
bbedward
a547966b23 vpn: wrap secrets in secrets key, cache pkcs11 pin input 2026-01-01 13:43:22 -05:00
bbedward
f6279b1b2e greeter: simplify start-hyprland check 2026-01-01 13:17:01 -05:00
bbedward
957c89a85d settings: refactor for read-only handling
- Remove default-* copying logic
- Allow in-memory changes of settings/session datas
- Convert SessionData to newer spec pattern
- Migrate weather coords to Session data
- Bricks home manager (temporarily)
2026-01-01 13:13:35 -05:00
bbedward
571a9dabcd dock: fix tooltip positioning with adjacent bars 2026-01-01 12:04:49 -05:00
bbedward
51ca9a7686 cachingimage: dont depend on sha256sum 2026-01-01 11:47:26 -05:00
bbedward
c141ad1e34 settings: guard saving before load completed 2026-01-01 11:30:09 -05:00
bbedward
37f972d075 vpn: update pksc11 handling 2025-12-31 15:42:41 -05:00
Oscar R.
7d8de6e6f0 Improving the logic for start-hyprland wrapper use (#1220)
* Adding a way to use the start-hyprland wrapper when it's needed from Hyprland 0.53 it's recommended because offers more security if happens a fail

* Deleting unnecessary things and doing verifications

* fix pre-commit

* Changing to not depend on hyprctl to obtain version and avoid posible problems

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-31 13:27:05 -05:00
bbedward
7ff751f8a2 vpn: attempt to support pkcs11 prompts 2025-12-31 10:03:49 -05:00
bbedward
651672afe2 gamma: allow steps of 100 with slider
fixes #1216
2025-12-31 09:31:16 -05:00
bbedward
2dbadfe1b5 clipboard: single disable + read-only history option 2025-12-31 09:14:35 -05:00
purian23
621710bd86 Update & Replace all issue templates 2025-12-30 23:03:50 -05:00
bbedward
1edecb05bb widgets: dynamic DankToggle height 2025-12-30 22:24:48 -05:00
bbedward
f1a876301b dankbar: fix reveal on overview/niri when auto-hide on 2025-12-30 22:19:25 -05:00
bbedward
97a07c399a greeter: use folderlistmodel for session iteration, add launch timeout 2025-12-30 11:49:00 -05:00
Oscar R.
18f095cb23 feat: implement smart compositor entry point (start-hyprland vs Hyprland) (#1211)
* Adding a way to use the start-hyprland wrapper when it's needed from Hyprland 0.53 it's recommended because offers more security if happens a fail

* Deleting unnecessary things and doing verifications

* fix pre-commit

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-30 11:20:33 -05:00
bbedward
d95d516d64 settings: fix desktop widget accordion row height
fixes #1214
2025-12-30 10:56:20 -05:00
purian23
45ba64ab02 About versioning 2025-12-29 22:47:33 -05:00
bbedward
9501d66af6 matugen: fix skip 2025-12-29 17:46:32 -05:00
bbedward
2127fc339a core: update hypr config test 2025-12-29 14:59:02 -05:00
bbedward
7962fee0bd dankinstall: update hyprland reference config for 0.53
fixes #913
2025-12-29 14:55:12 -05:00
bbedward
d5c7b5c0cc workspace: update scroll accumulator logic 2025-12-29 12:11:37 -05:00
vha
5f77d69dd8 feat: accept numpad's enter key to finish screenshot selection (#1210)
* added reverse scrolling to settings and widget

* added support for dankbar scrolling

* Better settings description

* removed isNiri conditional from search index

* Added numpad enter key to finish screenshot selection
2025-12-29 11:13:57 -05:00
bbedward
60034be06a dankbar: copy high-dpi scrolling logic from DankListView 2025-12-29 10:52:33 -05:00
bbedward
518a5d38aa settings: show parse error message 2025-12-29 10:46:03 -05:00
Eduardo Ribeiro
2eeaf8ff62 feat: allow adjusting notification volume (#1199) 2025-12-29 10:41:12 -05:00
bbedward
cffee0fae6 matugen: make check codition an array 2025-12-29 10:36:24 -05:00
bbedward
f08e2ef5b8 hypr: add disable output option 2025-12-28 23:15:43 -05:00
Joaquim S.
2b0070c31a matugen/template: Soothing neovim theme (#1201) 2025-12-28 21:49:44 -05:00
Marcus Ramberg
ae82716afa core: apply gopls automatic modernizers (#1198) 2025-12-28 21:48:56 -05:00
johngalt
c281bf3b53 Adding Zen Browser matugen template (#1181)
* Adding Zen Browser matugen template

* Fixing indentation for matugen.go edits

* Trying to fix linting again..

* Tweaking contrasting surface colors in css, renamed file to match how firefox userchrome is named, also changed output directory to DMS config directory (like firefox)

* Modifing Zen userChrome again: removing unused css stuff, tweaking colors to better align with how pywalfox handles backgrounds/toolbars

* Last few tweaks on CSS - changing url bar highlight color, changing contrast on selected urls in dropdown

* matugen.go: fix check command for zen browser

* search_index: add zen browser setting
2025-12-28 12:52:41 -05:00
bbedward
45b8b2a895 clipboard: don't store sensitive mime types in history
fixes #1185
2025-12-28 11:13:34 -05:00
Tacticalsmooth
7b9ba840fb fixed lambda issue on nixos (#1188) 2025-12-28 12:50:07 +01:00
xxyangyoulin
84fb567ff5 Notification: Right-click to toggle Do Not Disturb (#1174) 2025-12-28 00:34:52 -05:00
Lucas
bbd57e0673 nix: remove dgop input; expose quickshell package (#1183)
* nix: remove dgop input

* nix: update quickshell

* nix: expose quickshell in outputs
2025-12-27 20:41:26 -05:00
vha
b1632a0a03 Fix #1179 normal scrolling direction (#1182)
* added reverse scrolling to settings and widget

* added support for dankbar scrolling

* Better settings description

* removed isNiri conditional from search index

* fix #1179 breaking normal scroll direction
2025-12-27 18:23:31 -05:00
bbedward
7aff1182c8 desktop widgets: easier copy/delete 2025-12-27 14:13:29 -05:00
vha
fbe362cd20 feat: Added reverse workspace scrolling (#1179)
* added reverse scrolling to settings and widget

* added support for dankbar scrolling

* Better settings description

* removed isNiri conditional from search index
2025-12-27 13:47:50 -05:00
bbedward
e7f94c94cc i18n: fix RTL alignment of settings sidebar 2025-12-27 12:51:10 -05:00
bbedward
7523190b16 i18n: add farsi 2025-12-27 12:46:40 -05:00
bbedward
da37e16b6e window: remove unused signal 2025-12-27 12:32:42 -05:00
xxyangyoulin
5c420ab50b AppSearch: Add ID search fallback for non-English / non-standard apps (#1173) 2025-12-27 12:26:43 -05:00
Joaquim S.
4493b7c231 matugen/template: Improve on generated theme + using dank16 instead (#1177) 2025-12-27 11:14:18 -05:00
bbedward
40a96c6eaf vpn: initialize slices so they dont serialize as null 2025-12-27 10:52:34 -05:00
bbedward
23a93082c6 fix trailing whitespace 2025-12-27 09:43:42 -05:00
Tacticalsmooth
f7650b5e1f Added mirroring option to display configuration (#1156)
* <quickshell/Modules/Settings/DisplayConfig>: Added mirroring option to the displayconfiguration

* removed niri option for mirroring

* Fix trailing whitespace

* removed emty rows

---------

Co-authored-by: Postboote1 <stoessel.matthias>
2025-12-27 09:27:19 -05:00
bbedward
3ebdd5631c dankdash: fix binding breaking when clicking overview card
fixes #1170
2025-12-27 00:58:47 -05:00
Linken Quy Dinh
6c4caf121a add seconds to wallpaper cycling (#1169) 2025-12-27 00:53:48 -05:00
bbedward
89788e9ca7 workspace: chagne pill hover color 2025-12-27 00:53:01 -05:00
bbedward
0787c63fed bar: change widget base hover blend logic 2025-12-27 00:49:11 -05:00
bbedward
9fc0d5efff settings: add index extractor script for search 2025-12-26 22:21:20 -05:00
bbedward
6611dfbe05 settings: fix search height 2025-12-26 20:44:35 -05:00
bbedward
8a71ead51d themes: remove catpuccin, support accent colors 2025-12-26 20:28:55 -05:00
bbedward
d9d6ab5776 settings: add search
- because tabs arent loaded at runtime, we have to have a separate index
- Less ideal, but functional enough for now
2025-12-26 19:19:47 -05:00
bbedward
d6fe7bea27 vpn: remove redundant property definitions 2025-12-26 19:02:16 -05:00
xxyangyoulin
1194f3ffb8 media: add scroll wheel behavior configuration (#1160) 2025-12-26 14:43:47 -05:00
bbedward
5ac81e6dd6 dankbar: dont apply exclusive zone to popup positioning 2025-12-26 14:38:34 -05:00
Lucas
987856a1de nix: update flake inputs (#1161) 2025-12-26 14:15:22 -05:00
bbedward
ef52ce0990 themes: support for variants 2025-12-26 14:00:14 -05:00
bbedward
06b14a5869 dankinstall: fix plasma session collision 2025-12-26 13:06:21 -05:00
bbedward
fd839059c0 popout: use mapToItem instead of mapToGlobal for popout positioning
fixes #1152
2025-12-25 12:50:26 -05:00
bbedward
ec6db7962a i18n: sync terms 2025-12-25 12:09:40 -05:00
Aaron Tulino
adf92cbc46 Add battery charge limit (#1151) 2025-12-25 12:09:09 -05:00
Aaron Tulino
6b6f51cd1f Add volume and brightness percentages (#1148) 2025-12-25 12:07:29 -05:00
Aaron Tulino
df6c60213f Use volume_mute icon for volume==0 (#1150) 2025-12-25 11:31:05 -05:00
Aaron Tulino
6303304a10 Allow toggling mute with right-click on bar (#1147) 2025-12-24 16:12:47 -05:00
Aaron Tulino
8e76789119 Fix touchpad scrolling behavior (#1146)
* Fix touchpad scrolling behavior

* Make touchpad scroll smoothly
For a normal mouse wheel, adjusting by 5% per scroll makes sense. For a touchpad, however, it should adjust by the smallest increment possible for a smooth experience.
2025-12-24 16:12:34 -05:00
Aaron Tulino
10e81cfdd3 Clear lock screen textbox on Escape key press (#1139) 2025-12-24 09:47:55 -05:00
Aaron Tulino
03fd3a4f16 Add Do Not Disturb to IPC (#1140) 2025-12-24 09:47:44 -05:00
bbedward
8fdc748ed2 weather: fix icons 2025-12-24 00:33:54 -05:00
bbedward
6c56d23b93 themes: fix terminals always dark with custom themes 2025-12-23 22:33:33 -05:00
bbedward
45d34dcb5b themes: consistent usage of primaryPressed 2025-12-23 21:45:06 -05:00
bbedward
d7ac0d50fa launcher: use primaryPressed for hover 2025-12-23 21:36:59 -05:00
bbedward
1d4d145187 desktop plugins: enable by default 2025-12-23 14:10:55 -05:00
bbedward
a5b9ff98c0 displays: explicitly write scale 1 for niri
fixes #1116
2025-12-23 11:53:23 -05:00
bbedward
6feaecd92e niri: add gaps and radius override 2025-12-23 11:00:20 -05:00
bbedward
b066a25308 dankdash: use CachingImage in wallpaper tab
fixes #1130
2025-12-23 10:47:50 -05:00
bbedward
777a552b57 spotlight: restore darken background option
fixes #1126
2025-12-23 10:45:11 -05:00
bbedward
7dbe608c28 settings: fix theme application of default-settings json 2025-12-23 10:33:14 -05:00
bbedward
61630e447b desktop-widgets: add overlay IPC and overview option 2025-12-22 20:29:01 -05:00
bbedward
91385e7c83 dankbar: option to show when bar is hidden and no windows 2025-12-22 19:54:53 -05:00
bbedward
04648fcca7 spotlight: remove darken bg opt, improve performance 2025-12-22 16:07:40 -05:00
bbedward
080fc7e44e i18n: term update 2025-12-22 14:37:47 -05:00
bbedward
0b60da3d6d dock: add isolate runninig apps by display option 2025-12-22 14:35:04 -05:00
claymorwan
a4492b90e7 matugen: fix equibop theme not working (#1122)
* matugen: equibop theme

* style: withespace apparently

* matugen: fix equibop theme not working
2025-12-22 14:19:57 -05:00
bbedward
c9331b7338 dropdown: improve perf + add fuzzy search to printers 2025-12-22 14:18:24 -05:00
bbedward
4982ea53dd window: add support for startSystemMove, resize, maximize to floating
windows
2025-12-22 13:18:37 -05:00
claymorwan
c703cb6504 matugen: equibop theme (#1119)
* matugen: equibop theme

* style: withespace apparently
2025-12-22 12:03:20 -05:00
bbedward
a7494971fd desktop widgets: centralize config in desktop widgets tab, variants
always available
2025-12-22 10:39:19 -05:00
purian23
c548255bfc ubuntu: DMS-Greeter 2025-12-21 23:56:17 -05:00
purian23
9656c7afd7 ubuntu: Update hardcoded arcs 2025-12-21 23:37:08 -05:00
purian23
414b8c8272 Ubuntu: DMS - add ARM64 support 2025-12-21 22:31:59 -05:00
bbedward
b4f83d09d4 themes: incorporate theme registry, browser, dms URI scheme handling 2025-12-21 22:03:48 -05:00
purian23
67ee74ac20 core: Fix Debian Architecture logic 2025-12-21 21:59:20 -05:00
purian23
93539d2b6b core: Debian Sid/OpenSuse Leap, Slowroll support 2025-12-21 21:36:33 -05:00
bbedward
524d967745 matugen: remove bad kitty tab option
fixes #1109
2025-12-21 17:42:20 -05:00
bbedward
0effbebbb6 matugen: fix GTK4 light mode
fixes #1110
fixes #1056
2025-12-21 12:52:01 -05:00
bbedward
dca07a70f8 desktop widgets: put grid on bottom layer 2025-12-20 09:58:01 -05:00
bbedward
02936c97fd desktop widget: handle key events in widget 2025-12-20 09:34:52 -05:00
Ethan Todd
8f7e732827 notifications: add modal IPC command for dismissing all popups. rename clearAllPopups() to dismissAllPopups(), since clear is otherwise used to mean eliminated entirely rather than just sent to the notification center. (#1100) 2025-12-20 08:13:26 -05:00
johngalt
5ffe563b7d adding gruvbox material custom theme varieties (#1098) 2025-12-19 22:38:22 -05:00
Joaquim S.
6ef08c3d54 matugen/template: Added neovim to matugen pipeline (#1097) 2025-12-19 14:16:45 -05:00
bbedward
908b4b58cd desktop widgets: add grid/grid size hints 2025-12-19 14:05:04 -05:00
purian23
f2611e0de0 fedora: Remove cliphist on dms-git 2025-12-18 23:55:09 -05:00
purian23
ea75a9d351 distro: Convert DMS Greeter to Stable on Fedora Copr 2025-12-18 23:00:13 -05:00
bbedward
3a744d7d68 core: new line on version 2025-12-18 22:12:50 -05:00
purian23
195d312ae2 distro: Decople Fedora DMS Stable spec 2025-12-18 20:03:23 -05:00
Omar Abragh
76006a7377 matugen: Set cursor color for theme (#1088) 2025-12-18 17:23:42 -05:00
bbedward
11536da512 fix missing import 2025-12-18 17:22:01 -05:00
bbedward
2a91bc41f7 i18n: general term cleanup, add missing terms, interpolate some 2025-12-18 16:19:27 -05:00
bbedward
baf23157fc i18n: sync translations 2025-12-18 14:51:57 -05:00
bbedward
83b81be825 keybinds: add log if ShortcutInhibitor is missing 2025-12-18 14:15:54 -05:00
bbedward
4aefa0f1f7 core: skip replacing niri/dms configs
fixes #1072
2025-12-18 11:58:45 -05:00
bbedward
e53a7cee97 matugen: wrap pywalfox in sh 2025-12-18 11:51:37 -05:00
bbedward
8437e1aa7b desktop widgets: use preview window instead of margin shift for non-niri 2025-12-18 10:05:38 -05:00
bbedward
632f40cc0a desktop plugins: use mapToGlobal on moving widgets 2025-12-18 08:57:58 -05:00
Ethan Todd
7d81445341 notifications: add modal function for clearing all (#1082) 2025-12-18 08:28:58 -05:00
bbedward
78a5f401d7 core: remove ascii art from version 2025-12-18 00:14:02 -05:00
bbedward
8745f98c95 matugen: fix vscode editor color reload 2025-12-17 23:40:09 -05:00
bbedward
f0f5bcc630 matugen: add color reload capability to vscode theme 2025-12-17 23:30:06 -05:00
purian23
8a3c513605 distro: Relocate Ubuntu dgop/dsearch to danklinux 2025-12-17 23:08:37 -05:00
bbedward
145d2636dd clock: make desktop clock not use precision seconds always 2025-12-17 21:00:04 -05:00
bbedward
f2b9dc8988 displays: add adaptiveSyncSupported to wlroutput API 2025-12-17 20:36:54 -05:00
bbedward
2e4d56728b niri: track open modals in modal manager for focus transfers 2025-12-17 20:21:34 -05:00
bbedward
18231ed324 niri: don't rely on text field length for launching 2025-12-17 16:40:54 -05:00
bbedward
d0b61d8ed1 niri: release focus for popouts on overview 2025-12-17 16:17:44 -05:00
bbedward
d385a44949 notifications: attempt to minimize rapid window creation/destruction 2025-12-17 16:10:25 -05:00
bbedward
d97392d46e clipboard: remove ownership option 2025-12-17 15:34:40 -05:00
bbedward
6abb2c73fd desktop: fix widget display toggle 2025-12-17 14:24:13 -05:00
bbedward
7e141c6b36 dankbar/vpn: right click to quick connect 2025-12-17 14:16:11 -05:00
bbedward
53553c1f62 clock: add analog seconds option for desktop widget 2025-12-17 14:04:14 -05:00
bbedward
523ccc6bf8 i18n: WIP initial RTL support
- notifications
- color picker
- process list
- settings
- control center, dash
- launcher

part of #1059
2025-12-17 13:50:06 -05:00
bbedward
811e89fcfa matugen: change pywalfox post hook 2025-12-17 12:43:05 -05:00
bbedward
5d5be4d9d7 lock: different pam fallback 2025-12-17 12:40:28 -05:00
bbedward
88457ab139 lock: add pam login fallback locally 2025-12-17 12:31:45 -05:00
bbedward
0034926df7 plugins/desktop-widgets: create a new "desktop" widget plugin type
- Draggable per-monitor background layer widgets
- Add basic dms version checks on plugins
- Clock: built-in clock desktop plugin
- dgop: built-in system monitor desktop plugin
2025-12-17 12:08:03 -05:00
musjj
d082d41ab9 nix: refactor module structure and flake output (#1014)
- The program module is now called dank-material-shell
- The homeModules structure is flattened
2025-12-17 08:12:30 -03:00
purian23
b7911475b6 distros: Prefer stable quickshell 2025-12-16 23:52:37 -05:00
bbedward
672754b0b5 dankdash: fix weather tooltips
fixes #1065
2025-12-16 15:27:44 -05:00
bbedward
0d1553123b binds: accidentally deleted import 2025-12-16 15:16:44 -05:00
bbedward
ba6c51c102 core: exit non-zero when SIGUSR1 is received (for systemd r estart) 2025-12-16 14:47:46 -05:00
bbedward
d64206a9ff core: detect quickshell crash on SIGTERM 2025-12-16 14:44:22 -05:00
bbedward
d9a1089039 displays: add hyprland HDR options 2025-12-16 14:12:51 -05:00
bbedward
55fe463405 displays: break monolith config down and allow floats/fix integer
writing (niri)
2025-12-16 13:36:00 -05:00
bbedward
e84210e962 displays: fix niri hot corner config 2025-12-16 12:54:26 -05:00
bbedward
ff506548d3 displays: add niri-specific layout options to configurator 2025-12-16 12:23:34 -05:00
arfan
f6b09751e9 fix: update getWorkspaceIndex function to include index parameter also fix workspace padding number (#1062) 2025-12-16 11:32:21 -05:00
bbedward
3d863979c4 core: preserve quickshell exit code 2025-12-16 09:01:13 -05:00
purian23
2947ff4131 distro: Revise server side file handling 2025-12-16 01:08:12 -05:00
purian23
b8fca10896 Remove auto run on tags 2025-12-16 00:17:13 -05:00
purian23
33e45794d2 No run on push 2025-12-15 23:29:36 -05:00
purian23
42cc88ca65 Workflow update 2025-12-15 23:24:16 -05:00
purian23
0b7f2416ca distro: Bring up Stable 2025-12-15 23:10:24 -05:00
purian23
5d5c745ee5 Push the logs 2025-12-15 22:18:35 -05:00
purian23
e0429e4c60 distro: Re-add suffix 2025-12-15 21:31:13 -05:00
bbedward
0bece5287e dock: improve pinned app re-ordering feedback, fix vertical dock
ordering
fixes #1046
fixes #938
2025-12-15 20:46:36 -05:00
purian23
60b5e47836 update gitignore env 2025-12-15 19:06:43 -05:00
purian23
aa75b44790 distro: OBS version matching 2025-12-15 18:03:58 -05:00
bbedward
769f58caa9 displays: fix reverted state for position 2025-12-15 17:43:52 -05:00
bbedward
e7facf740d update CHANGELOG 2025-12-15 17:18:59 -05:00
Austin Farmer
04921eef62 Move Ghostty Application Theming (#1047)
* Moved ghostty config

First test. Seems to work but probably broke something.

* Updated test
2025-12-15 17:16:46 -05:00
Oliver Portee
8863c42879 fix light mode/dark mode switch for stock themes (#1057) 2025-12-15 17:16:23 -05:00
bbedward
2745116ac5 displays: add configurator for niri, Hyprland, and MangoWC
- Configure position, VRR, orientation, resolution, refresh rate
- Split Display section into Configuration, Gamma, and Widgets
- MangoWC omits VRR because it doesnt have per-display VRR
- HDR configuration not present for Hyprland
2025-12-15 16:36:14 -05:00
bbedward
bafe1c5fee niri: handle window urgency event
fixes #1033
2025-12-15 12:16:43 -05:00
bbedward
306d7b2ce0 gamma: guard against application
- QML will sync its desired state with GO, when IE settings are changed
  or opened. Go was applying gamma even if unchanged
- Track last applied gamma to avoid sends
2025-12-15 11:43:16 -05:00
bbedward
e9f6583c60 workspaces: add scroll handler to widget itself 2025-12-15 11:12:27 -05:00
redybcs
42a2835929 Update flake.nix to fix Hash Mismatch (#1035)
Looks like there hasn't been any go.mod updates since the workflow to fix the hash was repaired.
2025-12-15 14:14:29 +01:00
purian23
c2c90c680e distro: OBS edgecase 2025-12-15 01:28:00 -05:00
purian23
cd01f6378c Revise OBS / PPA Workflows 2025-12-15 00:37:42 -05:00
purian23
6033075de6 distro: Revise builds to use API variants 2025-12-15 00:32:40 -05:00
tsukasa
79794d3441 dankmodal: removed backgroundWindow to fix clicking twice (#1030)
* dankmodal: removed backgroundWindow

removed 'backgroundWindow' but combined it with 'contentWindow'

* made single window behavior specific to hyprland

this should keep other compositor behavior the same and fix double
clicking to exit out of Spotlight/ClipboardHist/Powermenu
2025-12-14 19:52:06 -05:00
bbedward
031f86b417 Revert "Fixed having to click twice to exit out of Spotlight/Cliphist/Powermenu (#1022)"
This reverts commit ca5fe6f7db.
2025-12-14 19:09:04 -05:00
bbedward
891f53cf6f battery: fix button group sclaing 2025-12-14 17:22:54 -05:00
bbedward
848991cf5b idle: implement screensaver interface
- Mainly used to create the idle inhibitor when an app requests
  screensaver inhibit
2025-12-14 16:49:59 -05:00
bbedward
d37ddd1d41 vpn: optim cc and dankbar widget 2025-12-14 16:12:46 -05:00
Pi Home Server
00d12acd5e Add hide option for updater widget (#1028) 2025-12-14 15:55:47 -05:00
bbedward
3bbc78a44f dankbar: make control center widget per-instance not global
fixes #1017
2025-12-14 15:52:46 -05:00
bbedward
b0a6652cc6 ci: simplify changelog handling 2025-12-14 14:23:27 -05:00
bbedward
cb710b2e5f notifications: fix redundant height animation 2025-12-14 13:40:21 -05:00
tsukasa
ca5fe6f7db Fixed having to click twice to exit out of Spotlight/Cliphist/Powermenu (#1022)
There's possibly more but this fix the need of having to click the
background twice to close those modals.
2025-12-14 11:16:25 -05:00
bbedward
fb75f4c68b lock/greeter: fix font alignment
fixes #1018
2025-12-14 11:13:48 -05:00
bbedward
5e2a418485 binds: fix to scale with arbitrary font sizes 2025-12-14 10:56:03 -05:00
bbedward
24fe215067 ci: pull changelogs from obs/launchpad APIs
- Get changelog from OBS/Launchpad API endpoints, instead of storing in
  git
2025-12-14 10:42:00 -05:00
bbedward
ab2e8875ac runningapps: round icon margin to integer 2025-12-14 10:25:36 -05:00
dms-ci[bot]
dec5740c74 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 15:22:12 +00:00
bbedward
208266dfa3 dwl: fix layout popout 2025-12-14 10:17:58 -05:00
dms-ci[bot]
32f218d58c ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 04:07:07 +00:00
dms-ci[bot]
6fdaab2ccd ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 03:58:50 +00:00
purian23
d336866f44 distro: Let the workflow run 2025-12-13 22:54:58 -05:00
purian23
b40df5f1c4 distro: Unify options across repos 2025-12-13 22:38:25 -05:00
dms-ci[bot]
3c9886ad1b ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 01:55:34 +00:00
bbedward
ea205ebd12 wallpaper: pause cycling when locked, clean state when changing modes 2025-12-13 20:29:02 -05:00
bbedward
30dad46c94 dankbar: add scroll wheel behavior configuration 2025-12-13 20:12:21 -05:00
dms-ci[bot]
fbf79e62e9 ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 21:23:58 +00:00
dms-ci[bot]
efcf72bc08 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 21:19:59 +00:00
bbedward
3b511e2f55 i18n: add hungarian 2025-12-13 14:03:49 -05:00
dms-ci[bot]
e4e20fb43a ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 15:26:22 +00:00
dms-ci[bot]
48ccff67a6 ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 15:25:01 +00:00
Souyama
a783d6507b Change DPMS off to DPMS toggle in hyprland.conf (#1011) 2025-12-13 10:07:11 -05:00
bbedward
fd94e60797 cava: dont set method/source 2025-12-13 10:04:20 -05:00
bbedward
a1bcb7ea30 vpn: just try and import all types on errors 2025-12-13 10:02:57 -05:00
bbedward
31b67164c7 clipboard: re-add ownership option 2025-12-13 09:45:04 -05:00
bbedward
786c13f892 clipboard: fix mime type selection 2025-12-13 09:35:55 -05:00
bbedward
c652659d54 wallpaper: scale texture to physical pixels
- reverts a regression
2025-12-13 08:43:46 -05:00
dms-ci[bot]
ca39196f13 ci: Auto-update OBS packages [dms,dms-git]
🤖 Automated by GitHub Actions
2025-12-13 07:00:01 +00:00
dms-ci[bot]
f02dd8fd4b ci: Auto-update PPA packages [dms,dms-git,dms-greeter]
🤖 Automated by GitHub Actions
2025-12-13 06:51:47 +00:00
purian23
0f89886ce7 distro: Break the loop 2025-12-13 01:44:20 -05:00
dms-ci[bot]
1118404192 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:34:59 +00:00
dms-ci[bot]
f011ea6cce ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:30:45 +00:00
dms-ci[bot]
b2ac9c6c1a ci: Auto-update OBS packages [dms,dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:06:44 +00:00
dms-ci[bot]
fbab41abd6 ci: Auto-update PPA packages [dms,dms-git,dms-greeter]
🤖 Automated by GitHub Actions
2025-12-13 05:58:14 +00:00
bbedward
82f881af5b matugen: scrub the never implemented dynamic contrast palette 2025-12-13 00:51:51 -05:00
purian23
68de9b437d distro: Switch to dms-ci 2025-12-13 00:50:42 -05:00
bbedward
830a715b6d wlcontext: use poll with wake pipe instead of read deadlines 2025-12-13 00:46:30 -05:00
bbedward
ce4aca9a72 fix shellcheck 2025-12-13 00:29:20 -05:00
bbedward
7641171a01 clipboard: move cl receive to main wlcontext goroutine 2025-12-13 00:16:56 -05:00
purian23
119e084e52 distro: Remove PR tests 2025-12-13 00:15:37 -05:00
bbedward
7c6d52913e niri: fix test 2025-12-12 23:57:50 -05:00
bbedward
f63ab5cf7c ci: add workflow for pushing stable tag 2025-12-12 23:57:50 -05:00
purian23
50f1bc5017 distros: Remove false path dir 2025-12-12 23:52:31 -05:00
bbedward
c3ab409b6a clipboard: scrap persist, optimize mime-type handling 2025-12-12 23:48:07 -05:00
purian23
44f6ab4878 distro: Reformat workflow newlines 2025-12-12 23:35:37 -05:00
bbedward
5fda6e0f12 clipboard: allow configuration even when disabled 2025-12-12 23:17:55 -05:00
purian23
38068e78c9 distros: PR writeback 2025-12-12 23:02:52 -05:00
purian23
66d22727e9 distros: Enhance build automation 2025-12-12 22:41:51 -05:00
Lucas
db2f68e35d nix: fix qt-plugins path (#1005) 2025-12-13 01:34:25 +01:00
Marcus Ramberg
352277ec15 notifications: add ipc call for toggleDoNotDisturb (#1002) 2025-12-12 18:21:00 -05:00
bbedward
d6043e64f2 osd: increase shadow buffer
accounts for percentage view
2025-12-12 18:11:31 -05:00
bbedward
d3f5b8f32e niri: fix gap reactivity 2025-12-12 16:58:07 -05:00
bbedward
6c3c722674 niri: add warnings on auto-generated files 2025-12-12 16:53:52 -05:00
purian23
5b8edb13d8 distro: OBS updates 2025-12-12 15:42:21 -05:00
bbedward
c595727b94 osd: optimize surface damage
fixes #994
2025-12-12 15:37:39 -05:00
bbedward
d46302588a clipboard: add shift+enter to paste from clipboard history modal
fixes #358
2025-12-12 15:29:10 -05:00
bbedward
0ff9fdb365 notifications: add swipe to dismiss functionality
fixes #927
2025-12-12 14:39:51 -05:00
purian23
e95f7ce367 Update Copr specs 2025-12-12 12:30:18 -05:00
Pi Home Server
df1a8f4066 Add lock screen layout settings (#981)
* Add lock screen layout settings

* Update translation keys
2025-12-12 11:45:00 -05:00
bbedward
32e6c1660f wallpaper: clamp max texture size 2025-12-12 11:17:28 -05:00
bbedward
d6b9b72e9b ci: disable pkg builds from main release wf 2025-12-12 10:16:24 -05:00
bbedward
179ad03fa4 ci: switch to dispatch-based release flow 2025-12-12 10:01:44 -05:00
bbedward
c3cb82c84e dankinstall: call add-wants for niri/hyprland with dms service 2025-12-12 09:58:12 -05:00
bbedward
4b52e2ed9e niri: fix keybind handling of cooldown-ms parameter 2025-12-12 09:52:35 -05:00
bbedward
77fd61f81e workspaces: make icons scale with bar size, fixi valign of numbers
fixes #990
2025-12-12 00:23:40 -05:00
Lucas
c3ffb7f83b nix: remove wl-clipboard and cliphist dependencies (#991) 2025-12-11 21:44:36 -05:00
Lucas
89dcd72d70 nix: let paths be used instead of only packages in plugins (#988) 2025-12-11 23:57:22 +01:00
bbedward
5c3346aa9d core: fix test 2025-12-11 16:33:31 -05:00
bbedward
7c4b383477 clipboard: persistence off by default
- It's a little risky and messy of a default
2025-12-11 16:28:56 -05:00
bbedward
bdc0e8e0fc clipboard: dont take ownership on nil offers 2025-12-11 15:55:42 -05:00
bbedward
6d66f93565 core: mock wayland context for tests & add i18n guidance to CONTRIBUTING 2025-12-11 14:50:02 -05:00
Lucas
9cac93b724 nix: fix pre-commit hook in dev-shell (#987) 2025-12-11 14:40:19 -05:00
bbedward
0709f263af core: add test coverage for some of the wayland stack
- mostly targeting any race issue detection
2025-12-11 13:47:18 -05:00
Lucas
4e4effd8b1 nix: fix home-manager module plugins (#984) 2025-12-11 19:36:32 +01:00
bbedward
f9632cba61 core: remove hyprpicker remnant 2025-12-11 13:05:07 -05:00
bbedward
38db6a41d5 gamma: fix initial night mode enablement 2025-12-11 12:27:58 -05:00
bbedward
7c6f0432c8 clipboard: add copyEntry (by id) handler 2025-12-11 12:00:47 -05:00
bbedward
56ff9368be matugen: add option to disable DMS templates
fixes #983
2025-12-11 11:48:59 -05:00
bbedward
597e21d44d clipboard: remove wl-copy references 2025-12-11 11:10:27 -05:00
bbedward
5bf54632be media: add option to disable visualizer in bar widget
fixes #978
2025-12-11 10:55:32 -05:00
bbedward
3a8d3ee515 core: use stdlib for xdg dirs 2025-12-11 10:15:23 -05:00
bbedward
1c1cf866e2 settings: make default height screen-aware 2025-12-11 09:51:44 -05:00
bbedward
ccc1df75f1 nix: update vendorHash 2025-12-11 09:50:50 -05:00
bbedward
d2c3f87656 ci: fix nix vendor-hash workflow 2025-12-11 09:46:57 -05:00
bbedward
6d62229b5f clipboard: introduce native clipboard, clip-persist, clip-storage functionality 2025-12-11 09:41:07 -05:00
Marcus Ramberg
7c88865d67 Refactor pre-commit hooks to use prek (#976)
* ci: change to prek for pre-commit

* refactor: fix shellcheck warnings for the scripts

* chore: unify whitespace formatting

* nix: add prek to dev shell
2025-12-11 09:11:12 -05:00
bbedward
c8cfe0cb5a dwl: fix layout popout not opening
fixes #980
2025-12-11 09:05:53 -05:00
Lucas
e573bdba92 nix: add QML dependencies to dms-shell package (#967) 2025-12-11 09:19:43 +01:00
Lucas
d8cd15d361 nix: add plugins in NixOS module (#970)
* nix: remove unnecessary /etc/xdg/quickshell/dms and .config/quickshell/dms

* nix: add plugins in NixOS module
2025-12-11 09:03:22 +01:00
Lucas
1db3907838 nix: fix greeter per-monitor and per-mode wallpapers (#974) 2025-12-11 09:01:14 +01:00
Lucas
72cfd37ab7 nix: fix niri module (#969) 2025-12-10 23:21:52 -05:00
bbedward
1e67ee995e plugins: hide uninstall and update buttons for system plugins 2025-12-10 19:30:58 -05:00
bbedward
6c26b4080c core: fix socket reported CLI version 2025-12-10 16:48:44 -05:00
purian23
0dbd59b223 Manual Changelog versioning 2025-12-10 13:52:29 -05:00
Lucas
b2066c60d1 nix: drop unnecessary dependencies and enable power and accounts daemons (#963) 2025-12-10 19:35:58 +01:00
bbedward
8d7ae324ff Revert "distro: update ppa-build script to ref right version"
This reverts commit c0d3c4f875.
2025-12-10 13:31:15 -05:00
bbedward
c0d3c4f875 distro: update ppa-build script to ref right version 2025-12-10 13:28:34 -05:00
purian23
27a771648a Ubuntu workflow tweak 2025-12-10 12:37:56 -05:00
purian23
86affc7304 Add WorkDIR to build steps 2025-12-10 12:33:41 -05:00
purian23
d939b99628 Workflow build increment logic 2025-12-10 12:27:10 -05:00
purian23
1fcf777f3d Bump OBS spec 2025-12-10 12:13:43 -05:00
purian23
7a8e23faa9 Update build scripts 2025-12-10 12:06:28 -05:00
bbedward
73a4dd3321 change codename 2025-12-10 11:18:08 -05:00
purian23
13ce873a69 Update dms stable systemd & desktop path 2025-12-10 11:16:03 -05:00
bbedward
406dc64aba wf: disable update-versions job 2025-12-10 10:50:47 -05:00
dms-ci[bot]
af5d6a2015 chore: bump version to v1.0.0 2025-12-10 15:43:36 +00:00
bbedward
61c6f509ae i18n: update translations 2025-12-10 09:32:57 -05:00
Marcus Ramberg
98769ecd88 nix: switch to standard nixpkgs rfc formatting (#962) 2025-12-10 04:55:45 -03:00
bbedward
8615950bd6 cc: allow 75 width sliders 2025-12-10 00:48:27 -05:00
bbedward
1bec8dfc48 vpn: make import modal floating variant 2025-12-10 00:30:45 -05:00
bbedward
460486fe25 media: fix media player updates 2025-12-09 23:59:04 -05:00
bbedward
318c50bc6c media: block scrolling media volume in widget when no player vol avail 2025-12-09 23:45:01 -05:00
purian23
3e08bac7f3 distros: Prep dms-git build versioning 2025-12-09 23:25:34 -05:00
bbedward
c3d64ab185 scrollwm: fix keybind provider registration 2025-12-09 20:14:07 -05:00
bbedward
2b73077b50 cc: add small disk usage variant
fixes #958
2025-12-09 16:09:13 -05:00
bbedward
f953bd5488 i18n: update translations 2025-12-09 16:01:05 -05:00
Varshit
f94011cf05 feat: add scroll compositor support (#959)
* added scroll support

* import QuickShell.i3

* update scroll provider registration logic

* improve scroll support for workspace switcher

* update title for scroll keybinds

* add scroll to dms-greeter

* fix: formatting & sway keybind provider

* readme update

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-09 15:57:46 -05:00
bbedward
aeacf109eb core: add slices, paths, exec utils 2025-12-09 15:34:13 -05:00
purian23
e307de83e2 packages: Update manual changelogs 2025-12-09 14:17:53 -05:00
bbedward
85968ec417 core/server: refactory to use shared params/request structs 2025-12-09 14:13:20 -05:00
bbedward
993f14a31f widgets: make dank icon picker a popup 2025-12-09 13:41:12 -05:00
purian23
566d617508 Re-adjust systemd debian/ubuntu 2025-12-09 13:40:59 -05:00
purian23
542a279fcb Add systemd debian/ubuntu packages 2025-12-09 12:39:56 -05:00
purian23
e784bb89e1 Version lock dms fedora/opensuse packages 2025-12-09 12:39:21 -05:00
bbedward
f680ace258 keybinds: fix dms args for some commands, some XF86 mappings 2025-12-09 12:21:20 -05:00
bbedward
7aa5976e07 media: fix padding issues with long titles 2025-12-09 11:46:50 -05:00
bbedward
f88f1ea951 gamma: display automation state in UI 2025-12-09 11:26:28 -05:00
bbedward
da4561cb35 keybinds: support more keys, allow Super+Alt 2025-12-09 10:41:39 -05:00
bbedward
1f89ae9813 popout: fix sizing on older QT 2025-12-09 09:57:31 -05:00
bbedward
5647323449 gamma: switch to wlsunset-style transitions 2025-12-09 09:44:16 -05:00
Karsten Zeides
bc27253cbf fix(README): fixes documentation link to include trailing slash (#920)
fixes same issue as described in AvengeMedia/DankLinux-Docs#25
2025-12-09 08:13:33 +01:00
Lucas
0672b711f3 nix: fix greeter custom theme (#954) 2025-12-09 07:14:13 +01:00
bbedward
ed9ee6e347 gamma: fix transition on enable 2025-12-09 00:46:49 -05:00
bbedward
7ad23ad4a2 gamma: fix night mode toggling 2025-12-09 00:35:52 -05:00
bbedward
8a83f03cc1 keybinds: fix provider loading via IPC 2025-12-09 00:30:14 -05:00
bbedward
0be9ac4097 keybinds: fix cheatsheet on non niri
- separate read only logic from writeread
2025-12-09 00:03:39 -05:00
bbedward
ba5be6b516 wallpaper: cleanup transitions 2025-12-08 23:53:50 -05:00
bbedward
c4aea6d326 themes: dont handle custom themes in onCompleted
- Defer entirley to FileView
2025-12-08 23:44:04 -05:00
bbedward
858c6407a9 dankinstall: ;remove keyring file on debian 2025-12-08 23:37:13 -05:00
bbedward
c4313395b5 dankinstall: use gpg batch for deb 2025-12-08 23:36:14 -05:00
bbedward
a32aec3d59 dankinstall: fix other debian sudo cmd 2025-12-08 23:31:08 -05:00
bbedward
696bcfe8fa dankinstall: fix deb sudo command 2025-12-08 23:30:03 -05:00
bbedward
2f3a253c6a wallpaper: fix per-monitor wallpaper in dash 2025-12-08 23:25:02 -05:00
bbedward
e41fbe0188 misc: change transmission icon override 2025-12-08 23:11:17 -05:00
bbedward
ef9d28597b dankinstall: don't fail suse if addrepo fails 2025-12-08 23:03:46 -05:00
bbedward
6f3c4c89ab keybinds: show fallback as action 2025-12-08 22:18:40 -05:00
bbedward
60c577a61e core: hyprland session on all distros, dms setup systemd prompt 2025-12-08 22:04:04 -05:00
bbedward
f3276c3039 notification: fix closing popout from escape
fixes #953
2025-12-08 20:46:22 -05:00
bbedward
37a843323d dankisntall: add hyprland session target, disable hyprland-git variant
universally
2025-12-08 20:40:13 -05:00
bbedward
95c780ca8c Revert "dankinstall: remove systemd path for Hyprland"
This reverts commit 0435a805c7.
2025-12-08 20:24:58 -05:00
bbedward
d60d5b154a dankinstall: switch to yalter/niri copr 2025-12-08 20:04:48 -05:00
bbedward
0435a805c7 dankinstall: remove systemd path for Hyprland 2025-12-08 19:48:07 -05:00
bbedward
f406a977e0 Revert "dankinstall: update hyprland syntax"
This reverts commit 54b253099d.
2025-12-08 19:35:05 -05:00
bbedward
18db1e1ecb dankinstall: update postinstall message 2025-12-08 19:13:32 -05:00
bbedward
6bd1beb719 dankinstall: pin arch to quickshell-git 2025-12-08 19:05:29 -05:00
bbedward
1293aecbca dankinstall: nuke polkit 2025-12-08 19:03:11 -05:00
Marcus Ramberg
8a10c2e112 nixos: fix fprintd unlock (#952)
* nixos: fix fprintd unlock

* ci: this workflow doesn't need a token
2025-12-08 19:14:51 -03:00
bbedward
c21d777269 screenshot: flip bits for RGB888 2025-12-08 15:38:49 -05:00
bbedward
d864094f48 screenshot/colorpicker: handle 24-bit frames from compositor 2025-12-08 14:56:01 -05:00
bbedward
deaac3fdf0 list: approve mouse detection 2025-12-08 14:11:44 -05:00
bbedward
b7062fe40c windows: dont close on esc
fixes #911
2025-12-08 14:02:58 -05:00
bbedward
64d5e99b9d dock: ensure creation after bars
fixes #919
2025-12-08 13:54:44 -05:00
bbedward
f9d8a7d22b greeter: fix weather setting
fixes #921
2025-12-08 13:45:26 -05:00
bbedward
52fcd3ad98 lock: make VPN icon white to be consistent with others
fixes #926
2025-12-08 13:24:53 -05:00
bbedward
9d1e0ee29b fix color picker color space 2025-12-08 12:59:24 -05:00
bbedward
de62f48f50 screenshot: handle transformed displays 2025-12-08 12:45:05 -05:00
bbedward
f47b19274c media: fix position/bar awareness
- shift media control column so it doesnt go off screen
fixes #942
2025-12-08 11:51:40 -05:00
bbedward
bb7f7083b9 meta: transparency fixes
- fixes #949 - transparency not working > 95%
- fixes #947 - dont apply opacity to windows, defer to window-rules
2025-12-08 11:43:29 -05:00
Yuxiang Qiu
cd580090dc evdev: improve capslock detection for no led device (#923)
* evdev: improve capslock detection for no led device

* style: fmt
2025-12-08 11:16:43 -05:00
Marcus Ramberg
ddb74b598d ci: add flake check (#951) 2025-12-08 11:15:35 -05:00
bbedward
29571fc3aa screenshot: use wlr-output-management on DWL for x/y offsets 2025-12-08 10:53:08 -05:00
bbedward
57ee0fb2bd bump: failed fprint tries 2025-12-08 10:02:53 -05:00
osscar
3ef10e73a5 nix: remove leading dot in nativeBuildInputs (#948)
Co-authored-by: osscar <osscar.unheard025@passmail.net>
2025-12-08 15:52:32 +01:00
bbedward
dc40492fc7 cc: fix audio slider binding 2025-12-08 09:45:25 -05:00
bbedward
e606a76a86 screenshot: add screenshot-window support for DWL/MangoWC 2025-12-08 09:39:42 -05:00
Lucas
8838fd67b9 nix: add dev-shell (#944)
* nix: add dev-shell

* docs: add Nix dev shell in contributing docs
2025-12-08 12:22:07 +01:00
Lucas
c570e20308 nix: use quickshell from source by default in greeter (#941) 2025-12-08 07:37:29 +01:00
bbedward
0a00ef39e3 ipc: fix bar widget IPCs when screens change 2025-12-07 23:15:24 -05:00
bbedward
9a08b81214 dankinstall: swap to systemd by default, use 90-dms.conf for vars 2025-12-07 22:51:22 -05:00
bbedward
c617ae26a2 niri: fix some keybind tab issues
- Fix args for screenshot
- move-column stuff is focus=true by default
- Parsing fixes
part of #914
2025-12-07 22:41:01 -05:00
Lucas
f6a776a692 nix: use by default quickshell from source (#939) 2025-12-07 21:11:22 -05:00
bbedward
54b253099d dankinstall: update hyprland syntax
fixes #913
2025-12-07 21:03:24 -05:00
bbedward
f662aca58c dankinstall: replace grim+slurp+grimblast with dms 2025-12-07 20:59:46 -05:00
bbedward
76e7755496 consistent icon sizing 2025-12-07 20:21:07 -05:00
bbedward
e05ad81c13 displays: remove system tray per-display opt
- superceded by omegabar
2025-12-07 20:13:40 -05:00
bbedward
cffb16d7f7 matugen: make signalByName helper not use exec 2025-12-07 20:10:31 -05:00
bbedward
18ca571944 matugen: scrap shell script for proper backend implementation with queue
system
2025-12-07 20:00:43 -05:00
bbedward
3ae1973e21 screenshot/colorpicker: fix scaling, update go-wayland to fix object
destruction, fix hyprland window detection
2025-12-07 13:44:35 -05:00
bbedward
308c8c3ea7 lock screen: fix inconsistency with network status, add VPN
maybe fix #926
2025-12-07 12:33:29 -05:00
bbedward
f49b5dd037 media player: replace color quantizer with album art 2025-12-07 12:23:00 -05:00
bbedward
f245ba82ad gamma: fix non-automation toggling
fixes #924
2025-12-07 12:02:50 -05:00
arfan
60d22d6973 feat: add workspace index display when app icon enabled (#936) 2025-12-07 11:48:48 -05:00
Farokh
d6f48a82d9 Update VSCode color theme templates for improved contrast and readability (#931)
* matugen/vscode-theme: update VSCode templates for contrast and readability

* vscode-theme: rework dark theme, refine light, restore default fallback

* dank16: add variants option, make default vscode consistent, fix termial
always dark

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-07 11:47:25 -05:00
Marcus Ramberg
c0d73dae67 fix: handle ipc arguments (#930) 2025-12-07 11:01:31 -05:00
Marcus Ramberg
49eb60589d fix: also restart ghostty/kitty on nix (#934) 2025-12-07 10:28:26 -05:00
Marcus Ramberg
89993b7421 core: remove unused function after refactors (#935) 2025-12-07 10:27:44 -05:00
purian23
511cb93806 Update rebuild logic on automation to obs / ppa 2025-12-06 21:33:53 -05:00
purian23
8ce78e7134 Dependency removals from Dankinstaller Distros
- Removed grim, grimblast, slurp, hyprpicker & mate-polkit from all distros
2025-12-06 01:10:13 -05:00
Yuxiang Qiu
9ebfab2e78 brightness: rescan brightness (#922) 2025-12-06 00:24:54 -05:00
bbedward
833d245251 dankbar: fix centersection positioning 2025-12-05 23:59:06 -05:00
bbedward
00d3024143 dankbar: keep border on maximize 2025-12-05 23:50:27 -05:00
bbedward
aedeab8a6a screenshot: add window capture for Hyprland 2025-12-05 21:10:12 -05:00
Pi Home Server
4d39169eb8 Feature/control center widget fix (#912)
* Add a widget to display the power menu

* Update power button widget

* Upate based on new settings

* Rollback to DisplaysTab.qml
2025-12-05 20:29:39 -05:00
bbedward
2ddc448150 screenshot: ensure screencopy before surface creation 2025-12-05 17:39:35 -05:00
bbedward
f9a6b4ce2c colorpick/screenshot: make color-format aware 2025-12-05 17:26:38 -05:00
bbedward
22b2b69413 screenshot: add shift to perfect-square capability 2025-12-05 17:08:00 -05:00
bbedward
7f11632ea6 screenshot: fix notif content to show open file browser 2025-12-05 16:56:29 -05:00
bbedward
c0b4d5e2c2 screenshot: fix thumbnail preview 2025-12-05 16:16:13 -05:00
Lucas
2c23d0249c nix: match upstream package format (#918) 2025-12-05 16:11:18 -05:00
bbedward
c3233fbf61 power menu: shorter hold durations 2025-12-05 16:05:11 -05:00
bbedward
ecfc8e208c screenshot: clipboard by default 2025-12-05 15:59:37 -05:00
bbedward
52d5e21fc4 screenshot: fix some region mappings 2025-12-05 15:25:27 -05:00
bbedward
6d0c56554f core: add screenshot utility 2025-12-05 14:59:34 -05:00
bbedward
844e91dc9e controlcenter: default vpn button to on 2025-12-05 14:21:19 -05:00
bbedward
1f00b5f577 fix some stale screen ref issues in OSD and popout 2025-12-05 13:31:57 -05:00
bbedward
2c48458384 brightness: more aggressive ddc rescans on device changes 2025-12-05 13:18:10 -05:00
bbedward
ddda87c5a7 less agress dms-open MimeType declarations 2025-12-05 12:36:04 -05:00
bbedward
6b1bbca620 keybinds: fix alt+shift, kdl parsing, allow arguments 2025-12-05 12:31:15 -05:00
bbedward
b5378e5d3c hypr: add exclusive focus override 2025-12-05 10:37:24 -05:00
bbedward
c69a55df29 flickable: update momentum scrolling logic 2025-12-05 10:14:16 -05:00
bbedward
5faa1a993a launcher: reemove background from list and add a bottom fade 2025-12-05 10:04:19 -05:00
bbedward
e56481f6d7 launcher: add 1px gap between grid delegates 2025-12-05 09:33:04 -05:00
bbedward
f9610d457c dankbar: fix border thickness 2025-12-05 09:29:45 -05:00
bbedward
ae066f42a4 brightness: delay screen change rescan of devices 2025-12-04 23:10:25 -05:00
bbedward
c60dd42fa7 dankinstall: set default niri config with includes 2025-12-04 22:45:46 -05:00
Yuxiang Qiu
7aac5ac5a1 dankbar: fix privacy indicator background color (#909) 2025-12-04 21:32:48 -05:00
bbedward
ad0f3fa33b dankbar: convert center section to use WidgetHost 2025-12-04 19:37:21 -05:00
bbedward
63d121b796 proc: ability to run command with noTimeout 2025-12-04 16:09:38 -05:00
bbedward
4291cfe82f settings: fix launcher tab sizing 2025-12-04 16:01:07 -05:00
bbedward
f312868154 lock: respect confirmation mode power actions 2025-12-04 14:58:36 -05:00
bbedward
5b42d34ac8 expose iconSize helpers to plugins 2025-12-04 14:40:38 -05:00
bbedward
397a8c275d settings: add IPCs to open specific settings tabs 2025-12-04 14:31:35 -05:00
purian23
2aabee453b Remove hyprpicker requirement for DMS Copr 2025-12-04 14:25:40 -05:00
bbedward
185333a615 brightness: default IPCs to pinned devices per-display
fixes #875
2025-12-04 13:55:14 -05:00
bbedward
7d177eb1d4 greeter: fix mango config override
fixes #904
2025-12-04 13:46:11 -05:00
Givani Boekestijn
705a84051d feat(dankdash): add vim keybindings (hjkl) to wallpaper picker navigation (#903) 2025-12-04 13:22:30 -05:00
bbedward
f6821f80e1 dankslideout: convert to Rectangle 2025-12-04 12:54:15 -05:00
bbedward
e7a6f5228d widgets: fix binding loop in button 2025-12-04 12:50:06 -05:00
bbedward
8161fd6acb i18n: add hebrew *partial*
- Most widgets and components lack proper RTL layout support
- Merging hebrew anyway, as these can be updated incrementally later
2025-12-04 11:39:58 -05:00
bbedward
2137920e81 dankslideout: put opacity on parent layer 2025-12-04 10:06:45 -05:00
bbedward
879102599c matugen: package vscode theme as vsix 2025-12-04 09:39:29 -05:00
bbedward
44190f07fe colorpicker: hide magnifier on startup 2025-12-04 09:14:12 -05:00
bbedward
a41487eb8f colorpicker: hide magnfier on monitor leave 2025-12-04 09:12:21 -05:00
bbedward
e1acaaa27c dankbar: add option to disable maximize detection
fixes #895
2025-12-04 08:56:04 -05:00
Marcus Ramberg
08a97aeff8 power: support automatic profile switching on battery change (#897) 2025-12-04 08:37:07 -05:00
bbedward
5b7302b46d color picker: use shortcuts inhibitor when active 2025-12-04 00:08:43 -05:00
purian23
34c0bba130 Add Debian / Ubuntu / OpenSuse support to DankInstaller 2025-12-03 23:41:17 -05:00
bbedward
5a53447272 color picker: switch to dms picker 2025-12-03 23:18:46 -05:00
bbedward
b6847289ff keybinds tab: change colors 2025-12-03 23:14:20 -05:00
bbedward
d22c43e08b app picker: fix background close 2025-12-03 23:00:22 -05:00
bbedward
d9deaa8d74 cli: add interactive color picker 2025-12-03 22:29:57 -05:00
bbedward
6c7776a9a6 audio: add IPC & OSD for changing output audio device
fixes #754
2025-12-03 20:47:57 -05:00
bbedward
62bd6e41ef settings: break out dank bar widgets 2025-12-03 18:17:06 -05:00
bbedward
293c7b42c6 pass screen to modals 2025-12-03 17:27:07 -05:00
bbedward
788da62777 settings: mecha re-organization 2025-12-03 17:25:40 -05:00
bbedward
2c7f24a913 lock: add option to show on 1 display
fixes #607
2025-12-03 12:15:22 -05:00
bbedward
f236706d6a hyprland: fix workspace overview truncation, update scaling
fixes #871
2025-12-03 12:02:41 -05:00
purian23
b097700591 Add dbus notifications inline to systemd 2025-12-03 11:53:31 -05:00
purian23
50b112c9d6 Revert "Add DMS dbus notification service file"
This reverts commit 33e655becd.
2025-12-03 11:48:56 -05:00
purian23
c2f478b088 Remove notification conflict 2025-12-03 11:16:04 -05:00
bbedward
dccbb137d7 launcher: integrate dsearch into drawer 2025-12-03 10:49:08 -05:00
bbedward
90f9940dbd gamma: fix night mode on startup 2025-12-03 10:37:28 -05:00
bbedward
f3f7cc9077 Revert "modals: single window optimization"
This reverts commit 468e569bc7.
2025-12-03 10:34:40 -05:00
bbedward
c331e2f39e Revert "spotlight: optimize to keep loaded"
This reverts commit 01b28e3ee8.
2025-12-03 10:34:19 -05:00
bbedward
1c7ebc4323 Revert "dankmodal: fix persistent modal handling"
This reverts commit e7cb0d397e.
2025-12-03 10:34:15 -05:00
bbedward
5f5427266f keybinds: always parse binds.kdl, show warning on position-conflicts 2025-12-03 10:32:16 -05:00
purian23
33e655becd Add DMS dbus notification service file 2025-12-03 09:49:34 -05:00
bbedward
0ea0602aec notif: fix keyboard navi in popout 2025-12-03 00:59:41 -05:00
bbedward
46effd2ca4 keybind: dont make shortcut inhbitor at compile time 2025-12-03 00:50:34 -05:00
bbedward
de055e8260 i18n: update terms 2025-12-03 00:34:31 -05:00
bbedward
c3077304af keybinds: move static arrays to js files 2025-12-03 00:21:11 -05:00
purian23
e15135911f DMS Version Formatting 2025-12-03 00:19:18 -05:00
purian23
d430cae944 fix: Duplicate build automation 2025-12-02 23:14:05 -05:00
bbedward
f92dc6f71b keyboard shortcuts: comprehensive keyboard shortcut management interface
- niri only for now
- requires quickshell-git, hidden otherwise
- Add, Edit, Delete keybinds
- Large suite of pre-defined and custom actions
- Works with niri 25.11+ include feature
2025-12-02 23:08:23 -05:00
purian23
a679be68b1 Update DMS versioning for Distro packages 2025-12-02 22:28:05 -05:00
bbedward
c5c5ce8409 i18n: add spanish 2025-12-02 21:18:45 -05:00
bbedward
e7cb0d397e dankmodal: fix persistent modal handling 2025-12-02 21:11:18 -05:00
Jon Rogers
b84308cb49 packaging: Add dms-open.desktop and danklogo.svg to all distribution packages (#870)
* packaging: add dms-open.desktop and danklogo.svg to all distributions

- Add dms-open.desktop to /usr/share/applications
- Add danklogo.svg to /usr/share/icons/hicolor/scalable/apps
- Updated packaging for:
  - Fedora (dms.spec)
  - OpenSUSE (dms.spec, dms-git.spec)
  - Debian (dms, dms-git)
  - Ubuntu (dms, dms-git)

Fixes #860

* nix: add dms-open.desktop and danklogo.svg to dankMaterialShell package

* Revert "packaging: add dms-open.desktop and danklogo.svg to all distributions"

This reverts commit 862a4fc405.

* nix: add dankMaterialShell to pkgs

---------

Co-authored-by: LuckShiba <luckshiba@protonmail.com>
2025-12-02 22:32:59 -03:00
Marcus Ramberg
0df47d2ce3 core: add dynamic completion for more commands (#889) 2025-12-02 18:35:51 -05:00
purian23
e24b548b54 fix: dms-cli & about versioning in all builds 2025-12-02 18:12:13 -05:00
Lucas
75af444cee niri: add option to disable overview launcher (#887) 2025-12-02 18:04:04 -05:00
bbedward
02dd19962f matugen: backup and add to vscode extensions json when present 2025-12-02 17:32:48 -05:00
purian23
f552b8ef7b Update Debian version format 2025-12-02 16:51:58 -05:00
Marcus Ramberg
9162e31489 core: add dynamic completion for ipc command (#885) 2025-12-02 15:51:26 -05:00
bbedward
01b28e3ee8 spotlight: optimize to keep loaded 2025-12-02 15:01:23 -05:00
bbedward
f5aa855125 network: eth device speed is not exposed 2025-12-02 14:45:28 -05:00
Guilherme Pagano
db3610fcdb feat: add support for geometric centering (#856)
Introduces a configurable centering mode.
- Adds 'geometric' option.
- Retains 'index' as the default value to preserve existing behavior.
2025-12-02 14:43:51 -05:00
bbedward
2e3f330058 theme: uncomment niri alt-tab colors 2025-12-02 14:41:09 -05:00
Marcus Ramberg
1617a7f2c1 dankbar: allow disabling title scrolling in the music display (#882) 2025-12-02 13:39:19 -05:00
bbedward
69a5566bf9 dankbar: shrink to 0 spacing and no border when maximized surface is
present
2025-12-02 11:22:50 -05:00
Marcus Ramberg
30e5d8b855 core: fix crash on tui startup on nixos after removal of component detection (#881)
```sh
❯ dms
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0xb2fbe5]

goroutine 1 [running]:
github.com/AvengeMedia/DankMaterialShell/core/internal/dms.(*Detector).GetDependencyStatus(0x0)
        github.com/AvengeMedia/DankMaterialShell/core/internal/dms/detector.go:56 +0x25
github.com/AvengeMedia/DankMaterialShell/core/internal/dms.(*Detector).GetInstalledComponents(0x421dd1?)
        github.com/AvengeMedia/DankMaterialShell/core/internal/dms/detector.go:120 +0x1f
github.com/AvengeMedia/DankMaterialShell/core/internal/dms.NewModel({_, _})
        github.com/AvengeMedia/DankMaterialShell/core/internal/dms/app.go:108 +0x67
main.runInteractiveMode(0xc0001e3000?, {0xdabb80?, 0x4?, 0xdabae0?})
        github.com/AvengeMedia/DankMaterialShell/core/cmd/dms/commands_root.go:85 +0x85
github.com/spf13/cobra.(*Command).execute(0x1549460, {0xc0000360d0, 0x0, 0x0})
        github.com/spf13/cobra@v1.10.1/command.go:1019 +0xae7
github.com/spf13/cobra.(*Command).ExecuteC(0x1549460)
        github.com/spf13/cobra@v1.10.1/command.go:1148 +0x465
github.com/spf13/cobra.(*Command).Execute(...)
        github.com/spf13/cobra@v1.10.1/command.go:1071
main.main()
        github.com/AvengeMedia/DankMaterialShell/core/cmd/dms/main.go:41 +0x6a
```
2025-12-02 09:26:06 -05:00
Marcus Ramberg
67ff7726e0 make pre-commit more portable (#880) 2025-12-02 09:25:08 -05:00
purian23
f96a2e2325 fix: OpenSuse package dir & hash versioning 2025-12-01 23:48:55 -05:00
bbedward
344c4f9385 ipc/focus: add focusOrToggle to settings and processlist 2025-12-01 23:16:06 -05:00
Álvaro
89aa146845 Readjustment of the audio display name for better fit (#874) 2025-12-01 20:27:51 -05:00
bbedward
468e569bc7 modals: single window optimization 2025-12-01 17:49:32 -05:00
purian23
139c99001a Update dms core internal paths 2025-12-01 17:28:19 -05:00
bbedward
bd99be15c2 brightness: fix ddc erasing devices, fix OSD behaviors 2025-12-01 16:32:34 -05:00
purian23
1d91d8fd94 Add desktop & icon to distro pacakges 2025-12-01 15:46:15 -05:00
purian23
f425f86101 Localize Systemd & Simplify builds 2025-12-01 15:21:04 -05:00
bbedward
83a6b7567f wallpaper: vram optimizations 2025-12-01 13:54:29 -05:00
bbedward
9184c70883 fix workflow 2025-12-01 12:25:55 -05:00
bbedward
f5ca4ccce5 core: update to golangci-lint v2 2025-12-01 12:23:52 -05:00
dms-ci[bot]
50f174be92 nix: update vendorHash for go.mod changes 2025-12-01 16:56:36 +00:00
bbedward
e5d11ce535 brightness: add udev monitor, bind OSDs to netlink events
fixes #863
2025-12-01 11:54:20 -05:00
Marcus Ramberg
94851a51aa core: replace all use of interface{} with any (#848) 2025-12-01 11:04:37 -05:00
bbedward
cfc07f4411 dock: add border option
fixes #829
2025-12-01 10:53:15 -05:00
bbedward
c6e9abda9f color picker: fix save button disappearing with eye dropper
fixes #853
2025-12-01 10:01:25 -05:00
bbedward
25951ddc55 launcher: consistent spacing of grid mode 2025-12-01 09:31:57 -05:00
mbpowers
bcd9ece077 fix: open settings (#868) 2025-12-01 09:06:10 -05:00
bbedward
68adbc38ba monitors: fix icon valign in widgets
fixes #862
2025-12-01 08:57:48 -05:00
bbedward
79a4d06cc0 remove effective screen from modal
fixes #869
2025-12-01 08:53:33 -05:00
bbedward
18bf3b7548 net: fix binding loop 2025-12-01 08:26:15 -05:00
bbedward
4e66d3532e appdrawer: fix context menu
fixes #859
2025-11-30 23:02:00 -05:00
Jon Rogers
1b6d567451 feat: Add browser picker modal for URL handling (#815)
* feat: add browser picker for opening URLs

- Introduce a QML modal allowing users to select a web browser to open a given URL.
- Add a CLI command `dms open <url>` that sends a `browser.open` request to the DMS server.
- Implement server‑side Browser manager, request handling, and subscription handling to propagate open events to clients.
- Extend router and server initialization to register the new “browser” capability and include it in advertised capabilities.
- Expose `openUrlRequested` signal in DMSService.qml and connect it to the modal for seamless UI activation.
- Add a desktop entry for the Browser Picker and update the active subscriptions list to include the browser service.

* fix(browser-picker): resolve QML errors in BrowserPickerModal and DMSShell

* fix(browser-picker): fix socket discovery in dms open command

* feat: add keyboard navigation and dynamic model to browser picker

- Replace the static browsers array with a ListModel built from AppSearchService, ensuring robust iteration and future‑proofing of the browser list.
- Introduce keyboard navigation (arrow keys and Enter) using selectedIndex and gridColumns, allowing users to select a browser without a mouse.
- Reset URL, selected index, and navigation flag when the modal closes to avoid stale state.
- Redesign the grid layout to compute cell width from columns, improve focus handling, and use AppLauncherGridDelegate for a consistent UI.
- Enhance delegate behavior to update selection on hover and reset keyboard navigation state appropriately.

* feat: add searchable list/grid view to browser picker

- Introduce view mode setting (list or grid) saved in SettingsData for persistent user preference
- Add search field with real‑time filtering to quickly locate a browser by name
- Sort browsers by usage frequency from AppUsageHistoryData, falling back to alphabetical order
- Provide UI toggle buttons to switch between list and grid layouts, updating the stored setting
- Adjust keyboard navigation logic to support both layouts and improve focus handling
- Refine modal dimensions and header layout for better visual consistency
- Record launched browser usage to keep usage rankings up‑to‑date.

* feat(browser-picker): improve UX with search, view persistence, and usage tracking

Enhance BrowserPickerModal to match AppLauncher design and functionality:

UI/UX Improvements:
- Add search bar with DankTextField for filtering browsers
- Move view mode switcher (list/grid) to header next to title
- Persist view mode preference to SettingsData.browserPickerViewMode
- Match AppLauncher dimensions (520x500)
- Add proper spacing between list items
- Improve URL display with truncation (single line, elide middle)
- Remove redundant close button

Functionality:
- Implement separate browser usage tracking in SettingsData.browserUsageHistory
- Sort browsers by most recently used (independent from app launcher stats)
- Add keyboard navigation auto-scrolling for list and grid views
- Track usage count, last used timestamp, and browser name
- Filter browsers by search query

Technical:
- Add ensureVisible() functions to DankListView and DankGridView
- Store browser usage with count, lastUsed, and name fields
- Update browser list reactively on search query changes

* feat(browser-picker): use appLauncherGridColumns setting for grid layout

Make browser picker grid view respect the same column setting as the app launcher
for consistent UI across both components.

* refactor: make browser picker extensible for any MIME type/category

Refactor browser picker into a generic, reusable application picker
system that can handle any MIME type or application category, similar
to Junction. This addresses the maintainer feedback about making the
functionality "as re-usable as possible."

Frontend (QML):
- Create generic AppPickerModal component (~450 lines)
  - Configurable filtering by application categories
  - Customizable title, view modes, and usage tracking
  - Emits applicationSelected signal for flexibility
- Refactor BrowserPickerModal as thin wrapper (473 → 46 lines)
  - Demonstrates how to create specialized pickers
  - Maintains all existing browser picker functionality

Backend (Go):
- Rename browser package to apppicker for clarity
- Enhance event model to support:
  - MIME types (for future file associations)
  - Application categories (WebBrowser, Office, Graphics, etc.)
  - Request types (url, file, custom)
- Maintain backward compatibility with browser.open method
- Add new apppicker.open method for generic usage

CLI:
- Rename commands_browser.go to commands_open.go
- Add extensibility flags:
  --mime/-m: Filter by MIME type
  --category/-c: Filter by category (repeatable)
  --type/-t: Specify request type
- Examples:
  dms open file.pdf --category Office
  dms open image.png --category Graphics

DMSService:
- Add appPickerRequested signal for generic events
- Smart routing between URL and generic app picker events
- Fully backward compatible

Benefits:
- Easy to create new pickers (~15 lines of wrapper code)
- Foundation for universal file handling system
- Consistent UX across all picker types
- Ready for MIME type associations

Future extensions:
- PDF picker, image viewer picker, text editor picker
- Default application management
- File association UI in settings
- Multi-MIME type desktop file integration

* fix(cli): remove all shorthands from open command flags for consistency

Remove shorthands from --mime, --category, and --type flags to maintain
consistency and avoid conflicts with global flags.

Flags now (all long-form only):
- --category: Application categories
- --mime: MIME type
- --type: Request type

Global flags still available:
- --config, -c: Config directory path

* style: apply gofmt formatting to apppicker files

Fix formatting issues caught by CI:
- Align struct field spacing in OpenEvent
- Align variable declaration spacing
- Fix Args field alignment in cobra.Command

* feat(apppicker): add generic file opener with auto MIME detection

Implements Junction-style generic file opening capabilities:

**Backend (Go):**
- Enhanced CLI to parse file:// URIs and extract file paths
- Auto-detect MIME types from file extensions using Go's mime package
- Auto-map MIME types to desktop categories:
  - Images → Graphics, Viewer
  - Videos → Video, AudioVideo
  - Audio → Audio, AudioVideo
  - Text → TextEditor, Office (or WebBrowser for HTML)
  - PDFs → Office, Viewer
  - Office docs → Office
  - Archives → Archiving, Utility
- Added debug logging to CLI and server handler for troubleshooting

**Frontend (QML):**
- Added generic AppPickerModal (filePickerModal) for file selection
- Connected to DMSService.appPickerRequested signal
- Implemented onApplicationSelected handler with desktop entry field code support:
  - %f/%F for file paths
  - %u/%U for file:// URIs
  - Fallback to appending path if no field codes
- Separate usage tracking: filePickerUsageHistory

**Desktop Integration:**
- Updated dms-open.desktop to handle x-scheme-handler/file
- Changed category from Network;WebBrowser to Utility (more generic)
- Added text/html to MIME types

**Usage:**
Set DMS as default for specific MIME types in ~/.config/mimeapps.list:
  text/plain=dms-open.desktop
  image/png=dms-open.desktop
  application/pdf=dms-open.desktop

Then use:
  xdg-open file.txt
  xdg-open image.png
  dms open document.pdf

The picker will show appropriate apps based on auto-detected categories.

Related to #815

* fix: resolve relative path handling by converting to absolute paths

- Convert file:// URIs to absolute filesystem paths for reliable file resolution
- Convert plain local file arguments to absolute paths to ensure consistent processing
- Update log messages to display absolute paths, improving traceability
- Retain request type detection while using absolute path extensions for MIME type inference

* feat(app-picker): add Tab key view toggle and fix targetData binding

- Add Tab key to toggle between grid and list views for better keyboard UX
- Fix bug where targetData binding broke after first modal close
  - Removed targetData reset from onDialogClosed
  - Parent components (BrowserPickerModal, filePickerModal) now manage targetData
  - Fixes issue where URL/file path disappeared on subsequent opens

* fix(app-picker): properly escape URLs and file paths for shell execution

- Add shellEscape() function to wrap arguments in single quotes
- Prevents shell interpretation of special characters (&, ?, =, spaces, etc.)
- Fixes bug where URLs with query parameters were truncated at first &
- Example: http://localhost:36275/vnc.html?autoconnect=true&reconnect=true
  now properly passes the full URL instead of cutting at first &
- Applied to both BrowserPickerModal (URLs) and filePickerModal (file paths)

* fix: check error return from InitializeAppPickerManager
2025-11-30 22:41:37 -05:00
mbpowers
7959a79575 feat: add autohide and settings ipc functions (#786)
* feat: bar visibility and autoHide IPC

also changed reveal to show

* feat: settings get/set IPC

* fix: show -> reveal, show is reserved keyword

* move IpcHandlers from SettingsData to DMSShellIPC
2025-11-30 20:50:00 -05:00
dms-ci[bot]
abf3249b67 nix: update vendorHash for go.mod changes 2025-12-01 00:27:18 +00:00
bbedward
35e0dc84e8 keybinds: add niri provider 2025-11-30 19:25:48 -05:00
mbpowers
17639e8729 feat: add sun and moon view to WeatherTab (#787)
* feat: add sun and moon view to WeatherTab

* feat: hourly forecast and scrollable date

* fix: put listviews in loaders to prevent ui blocking

* dankdash/weather: wrap all tab content in loaders, weather updates
- remove a bunch of transitions that make things feel glitchy
- use animation durations from Theme
- configurable detailed/compact hourly view

* weather: fix scroll and some display issues

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-11-30 18:47:27 -05:00
xdenotte
cbd1fd908c Fix ProcessList context menu visibility in DankPopout (#857) 2025-11-30 11:21:15 -05:00
bbedward
b2cf20f3d8 core: add pre-commit hooks for go CI checks 2025-11-30 11:04:12 -05:00
bbedward
915f1a5036 cli: remove distribution enforcement from tui 2025-11-30 10:51:38 -05:00
bbedward
a55ec6416c dankinstall: remove dead nix code, add doc link 2025-11-30 10:22:54 -05:00
yayuuu
b1834b1958 Adde Loader to only load shapes once the correct path has been generated (#851) 2025-11-30 10:11:53 -05:00
Willem Schipper
1beeb9fb55 fix: recreate plugin popout binding even if contentHeight is already set (#852) 2025-11-30 10:11:18 -05:00
bbedward
18d86354ec wallpaper: revert last changes
fixes #855
2025-11-30 10:06:01 -05:00
dms-ci[bot]
6297b0679c nix: update vendorHash for go.mod changes 2025-11-30 06:43:50 +00:00
bbedward
d62ef635a7 ci: use gh app 2025-11-30 01:42:15 -05:00
bbedward
c53836040f dankbar: add width/height deps to binding 2025-11-30 01:28:04 -05:00
bbedward
0b638bf85f ci: add update-vendor trigger 2025-11-30 01:23:23 -05:00
bbedward
7f6a71b964 ci: switch to gh pat 2025-11-30 01:20:19 -05:00
bbedward
1b4363a54a dankbar: dont early return in path functions 2025-11-30 01:08:38 -05:00
bbedward
16d168c970 core: update deps 2025-11-30 01:05:15 -05:00
bbedward
4606d7960e dankbar: remove caching redraw prevention 2025-11-30 00:56:36 -05:00
bbedward
4eee126d26 media: suppress media OSD on new players for 2s
fixes #838
2025-11-30 00:35:24 -05:00
bbedward
dde426658f core: fix golang-ci lints and add a config 2025-11-30 00:12:45 -05:00
bbedward
f6874fbcad workflow: run go CI on PRs 2025-11-29 23:35:40 -05:00
bbedward
621d4e4d92 dankbar: remove barTint Shape 2025-11-29 23:12:12 -05:00
bbedward
76062231fd dankbar: another hack to try and fix opacity 2025-11-29 23:06:49 -05:00
bbedward
261f55fea5 dankbar: simplify transparency binding 2025-11-29 22:55:14 -05:00
bbedward
202cf4bcc9 dankbar: try something else for binding 2025-11-29 22:43:55 -05:00
Willem Schipper
b7572f727f feat: allow popout to resize to its contents (#847) 2025-11-29 22:39:30 -05:00
bbedward
50ab346d58 dankbar: try to fix binding issues on creation 2025-11-29 22:36:20 -05:00
bbedward
b11b375848 settings: optimize mem usage
- keep un-loaded unless called upon
2025-11-29 18:32:45 -05:00
bbedward
e6c3ae9397 cups: add comprehensive CUPs setting page
- Add printers
- Delete printers
- Use polkit APIs as fallback on auth errors
- Fix ref system to conditionally subscribe to cups when wanted
2025-11-29 17:35:21 -05:00
bbedward
df663aceb9 net: less Theme.success 2025-11-29 11:14:15 -05:00
bbedward
db7e597f67 DankDash: fix per-monitor wallpapers 2025-11-29 11:10:10 -05:00
bbedward
1d3fe81ff7 network: big feature enrichment
- Dedicated view in settings
- VPN profile management
- Ethernet disconnection
- Turn prompts into floating windows
2025-11-29 10:00:05 -05:00
Lucas
9c887fbe63 spotlight: fix mouse action menu click (#841) 2025-11-28 23:32:35 -05:00
Lucas
4723bffcd2 spotlight: fix clipping and add context menu keyboard navigation (#840)
* spotlight: fix clipping and add context menu keyboard navigation

* prime: also detect nvidia-offload command

* spotlight: fix review nitpicks
2025-11-28 19:36:35 -05:00
purian23
9643de3ca0 Update greet sync to rec ACL 2025-11-28 18:45:55 -05:00
purian23
3bf3a54916 Enhance DMS Greeter logic 2025-11-28 18:10:54 -05:00
Marcus Ramberg
bcffc8856a nix: install completion support for dms cli (#836) 2025-11-28 19:59:37 -03:00
purian23
6b8c35c27b feat: DMS Greeter for Ubuntu 2025-11-28 16:32:48 -05:00
bbedward
dd409b4d1c osd/audio: bind audio change to pipewire, suppress OSDs on startup and
resume from suspend
2025-11-28 11:05:53 -05:00
bbedward
94a1aebe2b dgop: use dgop for uptime 2025-11-28 10:41:59 -05:00
bbedward
d3030c3ec6 color picker: fall back to niri picker when on niri
fixes #828
2025-11-28 09:47:19 -05:00
purian23
0221021078 Enhance DMS Greeter automation
- Thanks @brunodsf05 for doing some legwork to hunt this down!
2025-11-27 23:12:33 -05:00
purian23
966021bfd4 fix: DankBar binding loop & sth transparency 2025-11-27 22:13:13 -05:00
bbedward
f06e6e85d5 niri: support compact kb layout display
fixes #818
fixes #500
2025-11-27 10:53:37 -05:00
bbedward
28ad641070 displays: workaround for duplicate models 2025-11-27 10:34:18 -05:00
bbedward
384c775f1a dank16: enrich with hex, hex stripped, rgb 2025-11-27 09:46:45 -05:00
bbedward
ce40c691e9 niri: remove waitingForResults since it doesnt work and bind to search
term length
2025-11-27 01:47:33 -05:00
bbedward
5b0c38b0ed niri: fix warnings in overview 2025-11-27 01:01:35 -05:00
bbedward
734456785f matugen: log worker messages 2025-11-27 00:53:32 -05:00
bbedward
4f24312432 matugen: always set color scheme on exit 2025-11-27 00:31:56 -05:00
bbedward
d79b1ff3b4 displays: show physical resolution/mode instead of logical
fixes #819
2025-11-26 23:54:19 -05:00
bbedward
bbe1c1f1e0 filebrowser: re-add layer surface version 2025-11-26 23:51:59 -05:00
purian23
1978e67401 Update dms-cli for OBS packages 2025-11-26 23:27:33 -05:00
purian23
e129e4a2d0 Update dms-cli for nightly builds 2025-11-26 22:17:49 -05:00
Lucas
f7f1bbbdd2 nix: fix NixOS systemd service PATH (#823) 2025-11-26 18:30:06 -05:00
Saurabh
de8f2e6a68 feat/matugen3 (#771)
* added matugen 3 terminal templates and logic

fixed version check and light terminal check

refactored json generation

fixed syntax

keep tmp debug

fixed file outputs

fixed syntax issues and implicit passing

added debug stderr output

* moved calls to matugen after template is built correctly

added --json hex

disabled debug message

cleaned up code into modular functions, re-added second full matugen call

fixed args

added shift

commented vs code section

debug changes

* arg format fixes

fixed json import flag

fixed string quotation

fix arg order

* cleaned up

fix cfg naming

* removed mt2.0 templates and refactored worker

removed/replaced matugen 2 templates

fix formatter diffs + consistent styling

* fixed last json output

* fixed syntax error

* vs code templates

* matugen: inject all stock/custom theme colors as overrides
- also some general architectural changes

* dank16: remove vscode enrich option

---------

Co-authored-by: bbedward
2025-11-26 16:34:53 -05:00
Álvaro
85704e3947 Improved applications naming in AudioOutputDetail (#821) 2025-11-26 16:28:26 -05:00
bbedward
4d661ff41d dankinstall: add artix 2025-11-26 16:18:11 -05:00
bbedward
d7b39634e6 hyprland: fix focus grab 2025-11-26 12:46:19 -05:00
bbedward
039c98b9e3 power: switch to hold-style confirmation
fixes #775
2025-11-26 11:19:18 -05:00
bbedward
172c4bf0a9 confirm: add keepPopoutsOpen 2025-11-26 10:34:59 -05:00
bbedward
1f2a1c5dec niri: keep overview focus when open 2025-11-26 09:38:15 -05:00
bbedward
e5a6a00282 improve border 2025-11-26 00:35:21 -05:00
bbedward
d8153f7611 dankbar: improve config reactivity 2025-11-25 22:35:38 -05:00
bbedward
8b6ae3f39b bar: use shape > canvas 2025-11-25 18:51:47 -05:00
bbedward
24537781b7 remove UPower import from Theme 2025-11-25 17:24:52 -05:00
Álvaro
d2a29506aa Add middle-click close and collapse popout (#813)
* Add middle-click close and collapse popout

* Revert ControlCenterPopout
2025-11-25 16:21:01 -05:00
bbedward
adf51d5264 cava: tweak options 2025-11-25 16:17:52 -05:00
bbedward
0864179085 media: change icon for player volume 2025-11-25 15:02:59 -05:00
bbedward
8de77f283d niri: fix exit anims on overview launcher 2025-11-25 14:54:29 -05:00
bbedward
004a014000 windows: add minimum sizes 2025-11-25 13:58:08 -05:00
bbedward
80f6eb94aa appdrawer: fix not getting mouse events sometimes 2025-11-25 12:25:40 -05:00
bbedward
4035c9cc5f plugins: fix reactivity, tooltips, new IPCs to reload 2025-11-25 11:02:38 -05:00
bbedward
3a365f6807 settings: make plugin browser and widget browser floating 2025-11-25 10:33:32 -05:00
purian23
9920a0a59f Tweak Workflows 2025-11-25 10:09:11 -05:00
github-actions[bot]
c17bb9e171 chore: update packaging versions
🤖 Automated update by GitHub Actions
Workflow run: https://github.com/AvengeMedia/DankMaterialShell/actions/runs/19673220228
2025-11-25 14:37:10 +00:00
purian23
03073f6875 Refactor distro logic & automation 2025-11-25 09:32:24 -05:00
bbedward
609caf6e5f windows: disable QT CSD 2025-11-25 09:24:40 -05:00
bbedward
411141ff88 wallpaper: fix cycling
fixes #812
2025-11-25 09:24:00 -05:00
purian23
3e472e18bd Merge pull request #809 from LuckShiba/fix-scroll
bar: fix scroll on widgets that doesn't handle scroll
2025-11-25 01:24:45 -05:00
LuckShiba
e5b6fbd12a bar: fix scroll on widgets that doesn't handle scroll 2025-11-25 03:21:35 -03:00
bbedward
c2787f1282 wallpaper: disable cycling if any toplevel is full screen 2025-11-24 22:28:53 -05:00
bbedward
df940124b1 net: allow overriding wifi device 2025-11-24 21:27:18 -05:00
bbedward
5288d042ca media: fix player button control popup things 2025-11-24 20:51:05 -05:00
bbedward
fa98a27c90 dankbar: add generic bar widget IPC for popouts
fixes #750
2025-11-24 19:52:26 -05:00
bbedward
d341a5a60b dankbar/controlcenter: add VPN, mic, brightness, battery, and printer
options for widget
2025-11-24 16:36:49 -05:00
purian23
7f15227de1 Reduce dups & add workflow hotfix 2025-11-24 13:58:22 -05:00
purian23
bb45240665 Further optimize OBS build scripts 2025-11-24 13:10:16 -05:00
bbedward
29f84aeab5 dankbar: fix monitoring widgets with no background option
fixes #806
2025-11-24 12:26:29 -05:00
bbedward
5a52edcad8 ws: add option for occupied only 2025-11-24 12:03:34 -05:00
bbedward
b078e23aa1 settings: fix scrollable area in window 2025-11-24 11:56:10 -05:00
bbedward
7fa87125b5 audio: optimize visualizations 2025-11-24 11:37:24 -05:00
bbedward
f618df46d8 audio: optimize non-cava fallback 2025-11-24 11:08:03 -05:00
bbedward
ee03853901 idle: add fade to lock option
fixes #694
fixes #805
2025-11-24 10:59:36 -05:00
bbedward
6c4a9bcfb8 modals: restore Top layer as default
- Cut a mask in the background window
- restores virt kb compat
2025-11-24 09:38:03 -05:00
bbedward
1bec20ecef dankbar: fix individual widget settings 2025-11-24 00:48:35 -05:00
bbedward
08c9bf570d widgets: add an outline option
fixes #804
2025-11-24 00:14:19 -05:00
bbedward
5e77a10a81 dankbar: make border shape respect goth radius
part of #804
2025-11-23 23:55:07 -05:00
bbedward
3bc6461e2a sysmon: change spacing of monitor widgets 2025-11-23 23:26:00 -05:00
bbedward
d3194e15e2 dock: hide pin to dock for internal windows 2025-11-23 22:55:47 -05:00
bbedward
2db79ef202 dankbar: de-bounce bar settings 2025-11-23 22:23:18 -05:00
bbedward
b3c07edef6 notifications: fix DnD tooltip 2025-11-23 20:37:08 -05:00
bbedward
b773fdca34 cc: fix brightness tooltip 2025-11-23 20:33:52 -05:00
bbedward
2e9f9f7b7e media: restore tooltips 2025-11-23 20:31:54 -05:00
bbedward
30cbfe729d dank tooltip v2: apply to settings 2025-11-23 20:00:45 -05:00
Álvaro
b036da2446 Added per app volume control (#801)
* Added per app volume control

* format and lint fixes
2025-11-23 19:46:21 -05:00
bbedward
c8a9fb1674 media: make controls more usable since popout change 2025-11-23 19:38:10 -05:00
bbedward
43bea80cad power: disable profile osd by default, ensure dbus activation doesnt
happen
2025-11-23 18:17:35 -05:00
Lucas
23538c0323 bar: fix auto-hide hiding when tray popout is opened (#802) 2025-11-23 18:06:55 -05:00
bbedward
2ae911230d osd: try to optimize power profile osd more 2025-11-23 17:29:56 -05:00
bbedward
5ce1cb87ea power profile: put OSD in a lazyloader 2025-11-23 16:55:22 -05:00
bbedward
2a37028b6a dock: touch of inner padding to dms icon 2025-11-23 16:00:51 -05:00
bbedward
8130feb2a0 paths: show dms icon & title for dms windows 2025-11-23 15:57:03 -05:00
purian23
c49a875ec2 Workflow updates 2025-11-23 14:34:07 -05:00
bbedward
2a002304b9 migrate default font family props to Theme 2025-11-23 13:26:04 -05:00
bbedward
d9522818ae greeter: fix custom themes and font family
fixes #776
2025-11-23 13:21:16 -05:00
bbedward
800588e121 modal: remove targetScreen usage
fixes #798
2025-11-23 13:03:32 -05:00
bbedward
991c31ebdb i18n: update translations 2025-11-23 12:49:29 -05:00
bbedward
48f77e1691 processlist: convert to floating window 2025-11-23 12:16:03 -05:00
bbedward
42de6fd074 modals: apply same pattern of multi-window
- fixes excessive repaints
fixes #716
2025-11-23 12:07:45 -05:00
bbedward
62845b470c popout: fix excessive repaints
- Size content window to content size, buffer for shadow
- Add second window for click outside behavior
- User overriding the layer disables the click outside behavior

part of #716
2025-11-23 10:49:59 -05:00
bbedward
fd20986cf8 settings: make responsive, view-stack style 2025-11-23 10:01:26 -05:00
purian23
61369cde9e Update gitignore 2025-11-23 03:16:00 -05:00
purian23
644384ce8b feat: Mult-Distro support - Debian, Ubuntu, OpenSuse 2025-11-23 02:39:24 -05:00
Lucas
97c11a2482 bar: fix auto-hide not hiding after popout closes (#796) 2025-11-23 01:38:58 -05:00
bbedward
1e7e1c2d78 settings: clamp max content width 2025-11-23 01:38:16 -05:00
bbedward
1c7201fb04 settings: make settings and file browser normal windows
- add default floating rules for dankinstall
2025-11-23 01:23:06 -05:00
bbedward
61ec0c697a gamma: dont transition before destroying controls 2025-11-23 00:48:23 -05:00
bbedward
4b5fce1bfc dankbar: hide settings when bar is disabled 2025-11-23 00:45:12 -05:00
Lucas
6cc6e7c8e9 Media volume scroll on DankBar widget and media volume OSD (#795)
* osd: add media volume OSD

* media: scroll on widget changes media volume

* dash: use media volume in media tab
2025-11-23 00:42:06 -05:00
bbedward
89298fce30 bar: don't apply opacity to sth color
- legacy thing that already has it
2025-11-22 16:15:07 -05:00
bbedward
a3a27e07fa dankbar: support multiple bars and per-display bars
- Migrate settings to v2
  - Up to 4 bars
  - Per-bar settings instead of global
2025-11-22 15:28:06 -05:00
bbedward
4f32376f22 gamma: remove display sync on destruction 2025-11-22 15:26:05 -05:00
bbedward
58bf189941 launcher: set default launch prefix, if launching from systemd
- prevents apps dying when stopping the systemd unit
2025-11-22 00:23:06 -05:00
bbedward
bcfa508da5 weather: fix fahrenheit conversion 2025-11-21 22:07:44 -05:00
mbpowers
c0ae3ef58b fix: bar and dock flickering autohide (#784) 2025-11-21 21:49:31 -05:00
mbpowers
1e70d7b4c3 fix: remove useFahrenheit refresh, fetch Celcius convert locally (#785)
* fix: remove useFahrenheit refresh, fetch Celcius convert locally

* fix: typo in change unit button
2025-11-21 21:41:12 -05:00
bbedward
f8dc6ad2bc update CONTRIBUTING 2025-11-21 17:30:54 -05:00
bbedward
e22482988f weather: fix display when 0 temp
fixes #782
2025-11-21 17:06:57 -05:00
bbedward
4eb896629d net: fix VPN prompting for password 2025-11-21 12:59:12 -05:00
bbedward
b310e66275 themes: shift catpuccin palete 2025-11-21 09:30:58 -05:00
bbedward
b39da1bea7 cc: bit of extra height for some detail items 2025-11-21 09:15:59 -05:00
Pi Home Server
fa575d0574 Fix background color of the privacy widget (#779) 2025-11-21 09:05:56 -05:00
bbedward
dfe2f3771b theme: add colorful bar widget option 2025-11-21 00:07:23 -05:00
bbedward
46caeb0445 sounds: only play audio changed when trigger by us 2025-11-20 23:38:06 -05:00
bbedward
59cc9c7006 niri: ensure overview spotlight is hidden when main window is brought up 2025-11-20 21:23:56 -05:00
bbedward
12e91534eb niri: empty input region & disable spotlight content when not open 2025-11-20 16:44:46 -05:00
bbedward
d9da88ceb5 niri: embed spotlight to same window as overview layer 2025-11-20 16:28:26 -05:00
bbedward
2dbfec0307 niri: close spotlight when closing overview 2025-11-20 13:56:35 -05:00
Lucas
09cf8c9641 niri: add spotlight on overview typing functionality (#774) 2025-11-20 13:48:30 -05:00
Pi Home Server
f1bed4d6a3 Feature/privacy widget fix (#772)
* Fix active camera icon

* Fix active camera icon
2025-11-20 12:30:23 -05:00
bbedward
2ed6c33c83 missing import 2025-11-19 19:14:47 -05:00
bbedward
7ad532ed17 dankinstall: add ultramarine 2025-11-19 18:53:41 -05:00
bbedward
92fe8c5b14 hyprland: restore focus grab to tray menus 2025-11-19 17:24:14 -05:00
bbedward
8e95572589 modals: move HyprFocusGrab out of common Modal 2025-11-19 17:16:51 -05:00
bbedward
62da862a66 modal: round textureSize pixels 2025-11-19 14:36:08 -05:00
bbedward
993e34f548 dankinstall: weakdeps for niri/system 2025-11-19 09:35:22 -05:00
886 changed files with 180986 additions and 49998 deletions

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
[*.sh]
# like -i=4
indent_style = space
indent_size = 4
[*.nix]
# like -i=4
indent_style = space
indent_size = 4

View File

@@ -1,27 +0,0 @@
#!/bin/bash
# DISABLED for now
exit 0
set -euo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
cd "$REPO_ROOT"
if [[ -z "${POEDITOR_API_TOKEN:-}" ]] || [[ -z "${POEDITOR_PROJECT_ID:-}" ]]; then
exit 0
fi
if ! command -v python3 &>/dev/null; then
exit 0
fi
if ! python3 scripts/i18nsync.py check &>/dev/null; then
echo "Translations out of sync"
echo "run python3 scripts/i18nsync.py sync"
exit 1
fi
exit 0

View File

@@ -1,65 +0,0 @@
---
name: Bug Report
about: Crashes or unexpected behaviors
title: ""
labels: "bug"
assignees: ""
---
<!-- If your issue is related to ICONS
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
- [ ] Other (specify)
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the issue -->
## Expected Behavior
<!-- Describe what you expected to happen -->
## Steps to Reproduce
<!-- Please provide detailed steps to reproduce the issue -->
1.
2.
3.
## Error Messages/Logs
<!-- Please include any error messages, stack traces, or relevant logs -->
<!-- you can get a log file with the following steps:
dms kill
mkdir ~/dms_logs
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
-->
```
Paste error messages or logs here
```
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->

96
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
name: Bug Report
description: Crashes or unexpected behaviors
labels:
- bug
body:
- type: markdown
attributes:
value: |
## DankMaterialShell Bug Report
Limit your report to one issue per submission unless closely related
- type: checkboxes
id: compositor
attributes:
label: Compositor
options:
- label: Niri
- label: Hyprland
- label: MangoWC (dwl)
- label: Sway
validations:
required: true
- type: checkboxes
id: distribution
attributes:
label: Distribution
options:
- label: Arch Linux
- label: CachyOS
- label: Fedora
- label: NixOS
- label: Debian
- label: Ubuntu
- label: Gentoo
- label: OpenSUSE
- label: Other (specify below)
validations:
required: true
- type: input
id: distribution_other
attributes:
label: If Other, please specify
placeholder: e.g., PikaOS, Void Linux, etc.
validations:
required: false
- type: input
id: dms_version
attributes:
label: dms version
description: Output of dms version command
placeholder: e.g., 1.2.3
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Brief description of the issue
placeholder: What happened?
validations:
required: true
- type: textarea
id: expected_behavior
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: Describe the expected behavior
validations:
required: false
- type: textarea
id: steps_to_reproduce
attributes:
label: Steps to Reproduce & Installation Method
description: Please provide detailed steps to reproduce the issue
placeholder: |
1. ...
2. ...
3. ...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Error Messages/Logs
description: Please include any error messages, stack traces, or relevant logs
placeholder: |
Paste error messages or logs here
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots/Recordings
description: If applicable, add screenshots or screen recordings
placeholder: Attach images or videos here
validations:
required: false

View File

@@ -1,33 +0,0 @@
---
name: Request a Feature
about: New widgets, new widget behavior, etc.
title: ""
labels: "enhancement"
assignees: ""
---
## Feature Description
<!-- Brief description of the feature requested -->
## Use Case
<!-- Explain the purpose of this feature/why it'd be useful to you -->
## Compositor
Is this feature specific to one compositor?
- [ ] All compositors
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
## Proposed Solution
<!-- If you have any ideas for how to implement this, please share! -->
## Alternatives/Existing Solutions
<!-- Include any similar/pre-existing products that solve this problem -->

View File

@@ -0,0 +1,55 @@
name: Feature Request
description: Suggest a new feature or improvement for DMS
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
## DankMaterialShell Feature Request
- type: textarea
id: feature_description
attributes:
label: Feature Description
description: Brief description of the feature requested
placeholder: What feature would you like to see?
validations:
required: true
- type: textarea
id: use_case
attributes:
label: Use Case
description: Explain the purpose of this feature/why it'd be useful to you
placeholder: Why is this feature important?
validations:
required: false
- type: checkboxes
id: compositor
attributes:
label: Compositor(s)
description: Is this feature specific to one or more compositors?
options:
- label: All compositors
- label: Niri
- label: Hyprland
- label: MangoWC (dwl)
- label: Sway
- label: Other (specify below)
validations:
required: false
- type: textarea
id: proposed_solution
attributes:
label: Proposed Solution
description: If you have any ideas for how to implement this, please share!
placeholder: Suggest a solution or approach
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Alternatives/Existing Solutions
description: Include any similar/pre-existing products that solve this problem
placeholder: List alternatives or existing solutions
validations:
required: false

View File

@@ -1,40 +0,0 @@
---
name: Request Assistance or Support
about: Help with installation, usage, or general questions.
title: ""
labels: "support"
assignees: ""
---
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
- [ ] other
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the support needed -->
## Solutions Tried
<!-- Describe what you've tried so far -->
<!-- Outlining what you've tried so far helps us make improvements to the user experience and documentation to avoid recurrent issues -->
## Configuration Details
<!-- Include any configuration if relevant -->
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->

View File

@@ -0,0 +1,69 @@
name: Support Request
description: Help with installation, usage, or general questions about DankMaterialShell
labels:
- support
body:
- type: markdown
attributes:
value: |
## DankMaterialShell Support Request
- type: checkboxes
id: compositor
attributes:
label: Compositor
options:
- label: Niri
- label: Hyprland
- label: MangoWC (dwl)
- label: Sway
- label: Other (specify below)
validations:
required: false
- type: input
id: distribution
attributes:
label: Distribution
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
placeholder: Your Linux distribution
validations:
required: false
- type: input
id: dms_version
attributes:
label: dms version
description: Output of dms version command
placeholder: e.g., 1.2.3
validations:
required: false
- type: textarea
id: description
attributes:
label: Description
description: Brief description of the support needed
placeholder: What do you need help with?
validations:
required: true
- type: textarea
id: solutions_tried
attributes:
label: Solutions Tried
description: Describe what you've tried so far (commands, documentation, etc.)
placeholder: List steps or resources you've already tried
validations:
required: false
- type: textarea
id: configuration
attributes:
label: Configuration Details
description: Include any relevant configuration if relevant
placeholder: Add configuration or environment info
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots/Recordings
description: If applicable, add screenshots or screen recordings
placeholder: Attach images or videos here
validations:
required: false

383
.github/workflows/backup/run-obs.yml.bak vendored Normal file
View File

@@ -0,0 +1,383 @@
name: Update OBS Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to update (dms, dms-git, or all)"
required: false
default: "all"
force_upload:
description: "Force upload without version check"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
required: false
default: ""
push:
tags:
- "v*"
schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION (always update)"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match spec format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
if [[ -f "dms-git.spec" ]]; then
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract OBS commit, proceeding with update"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No spec file in OBS, proceeding with update"
fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
fi
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
PKG="${{ github.event.inputs.package }}"
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🚀 Force upload: all packages"
else
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🚀 Force upload: $PKG"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
update-obs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: |
github.event.inputs.force_upload == 'true' ||
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Check if last commit was automated
id: check-loop
run: |
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "✅ Last commit was not automated, proceeding"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Determine packages to update
if: steps.check-loop.outputs.skip != 'true'
id: packages
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Update dms-git spec version
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
# Update version in spec
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
# Add changelog entry
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
# Debian version format: 0.6.2+git2256.9162e314
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
CHANGELOG_DATE=$(date -R)
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
# Get current version from changelog
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
echo "Current Debian version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Only update if version changed
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
# Create new changelog entry at top
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms-git ($NEW_VERSION) nightly; urgency=medium
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
# Prepend to existing changelog
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
else
echo "✓ Debian changelog already at version $NEW_VERSION"
fi
- name: Update dms stable version
if: steps.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V"
# Update openSUSE dms spec (stable only)
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Update openSUSE spec changelog
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
# Update Debian _service files (both tar_scm and download_url formats)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
done
# Update Debian changelog for dms stable
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms ($VERSION_NO_V) stable; urgency=medium
* Update to $VERSION stable release
* Bug fixes and improvements
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to $VERSION_NO_V"
fi
- name: Install Go
if: steps.check-loop.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Install OSC
if: steps.check-loop.outputs.skip != 'true'
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
if: steps.check-loop.outputs.skip != 'true'
env:
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
fi
if [[ "$PACKAGES" == "all" ]]; then
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
else
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
fi
- name: Get changed packages
if: steps.check-loop.outputs.skip != 'true'
id: changed-packages
run: |
# Check if there are any changes to commit
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "📋 No changelog or spec changes to commit"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Get list of changed packages for commit message
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
echo "packages=$PKGS" >> $GITHUB_OUTPUT
echo "📋 Changed packages: $PKGS"
fi
- name: Commit packaging changes
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
run: |
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
git pull --rebase origin master
git push
- name: Summary
run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY

298
.github/workflows/backup/run-ppa.yml.bak vendored Normal file
View File

@@ -0,0 +1,298 @@
name: Update PPA Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: false
default: "dms-git"
force_upload:
description: "Force upload without version check"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false
default: ""
schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match changelog format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Extract commit hash from changelog
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
if [[ -f "$CHANGELOG_FILE" ]]; then
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$CHANGELOG_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract commit from changelog, proceeding with upload"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
upload-ppa:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: |
github.event.inputs.force_upload == 'true' ||
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Check if last commit was automated
id: check-loop
run: |
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "✅ Last commit was not automated, proceeding"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Set up Go
if: steps.check-loop.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache: false
- name: Install build dependencies
if: steps.check-loop.outputs.skip != 'true'
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
if: steps.check-loop.outputs.skip != 'true'
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Determine packages to upload
if: steps.check-loop.outputs.skip != 'true'
id: packages
run: |
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
PKG="${{ github.event.inputs.package }}"
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
echo "packages=all" >> $GITHUB_OUTPUT
echo "🚀 Force upload: all packages"
else
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "🚀 Force upload: $PKG"
fi
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual package selection should respect change detection
SELECTED_PKG="${{ github.event.inputs.package }}"
UPDATED_PKG="${{ needs.check-updates.outputs.packages }}"
# Check if manually selected package is in the updated list
if [[ "$UPDATED_PKG" == *"$SELECTED_PKG"* ]] || [[ "$SELECTED_PKG" == "all" ]]; then
echo "packages=$SELECTED_PKG" >> $GITHUB_OUTPUT
echo "📦 Manual selection (has updates): $SELECTED_PKG"
else
echo "packages=" >> $GITHUB_OUTPUT
echo "⚠️ Manual selection '$SELECTED_PKG' has no updates - skipping (use force_upload to override)"
fi
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Upload to PPA
if: steps.check-loop.outputs.skip != 'true'
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
if [[ -z "$PACKAGES" ]]; then
echo "No packages selected for upload. Skipping."
exit 0
fi
# Build command arguments
BUILD_ARGS=()
if [[ -n "$REBUILD_RELEASE" ]]; then
BUILD_ARGS+=("$REBUILD_RELEASE")
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi
if [[ "$PACKAGES" == "all" ]]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms to PPA..."
if [ -n "$REBUILD_RELEASE" ]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-git to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-greeter to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
else
# Map package to PPA name
case "$PACKAGES" in
dms)
PPA_NAME="dms"
;;
dms-git)
PPA_NAME="dms-git"
;;
dms-greeter)
PPA_NAME="danklinux"
;;
*)
PPA_NAME="$PACKAGES"
;;
esac
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PACKAGES to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
fi
- name: Get changed packages
if: steps.check-loop.outputs.skip != 'true'
id: changed-packages
run: |
# Check if there are any changelog changes to commit
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "📋 No changelog changes to commit"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Get list of changed packages for commit message (deduplicate)
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
echo "📋 Changed packages: $CHANGED"
echo "📋 Debug - Changed files:"
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
fi
- name: Commit changelog changes
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
run: |
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add distro/ubuntu/*/debian/changelog
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
git pull --rebase origin master
git push
- name: Summary
run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-git" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY

View File

@@ -1,315 +0,0 @@
name: DMS Copr Stable Release (Manual)
on:
workflow_dispatch:
inputs:
version:
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
required: false
default: ''
release:
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
required: false
default: '1'
jobs:
build-and-upload:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
# Get version from manual input or latest release
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
echo "Using manual version: $VERSION"
else
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
echo "Using latest release version: $VERSION"
fi
RELEASE="${{ github.event.inputs.release }}"
if [ -z "$RELEASE" ]; then
RELEASE="1"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release=$RELEASE" >> $GITHUB_OUTPUT
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
- name: Setup build environment
run: |
sudo apt-get update
sudo apt-get install -y rpm wget curl jq gzip
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
echo "✅ RPM build environment ready"
- name: Download release assets
run: |
VERSION="${{ steps.version.outputs.version }}"
cd ~/rpmbuild/SOURCES
echo "📦 Downloading DMS QML source for v${VERSION}..."
# Download DMS QML source
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1
}
echo "✅ Source downloaded"
echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture"
ls -lh
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE="${{ steps.version.outputs.release }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
# Spec for DMS stable releases - Generated by GitHub Actions
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
Name: dms
Version: %{version}
Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for the niri and hyprland compositors. Features notifications,
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%package -n dms-cli
Summary: DankMaterialShell CLI tool
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
%description -n dms-cli
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep
%setup -q -c -n dms-qml
# Download architecture-specific binaries during build
# This ensures the correct architecture is used for each build target
case "%{_arch}" in
x86_64)
ARCH_SUFFIX="amd64"
;;
aarch64)
ARCH_SUFFIX="arm64"
;;
*)
echo "Unsupported architecture: %{_arch}"
exit 1
;;
esac
# Download dms-cli for target architecture
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dms-cli for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
# Download dgop for target architecture
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build
%install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
# Shell completions
install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
%posttrans
# Clean up old installation path from previous versions (only if empty)
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
# Remove directories only if empty (preserves any user-added files)
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
# Restart DMS for active users after upgrade
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%files -n dms-cli
%{_bindir}/dms
%{_datadir}/bash-completion/completions/dms
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
echo ""
echo "=== Spec file preview ==="
head -40 ~/rpmbuild/SPECS/dms.spec
- name: Build SRPM
id: build
run: |
cd ~/rpmbuild/SPECS
echo "🔨 Building SRPM..."
rpmbuild -bs dms.spec
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
SRPM_NAME=$(basename "$SRPM")
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
echo "✅ SRPM built: $SRPM_NAME"
echo ""
echo "=== SRPM Info ==="
rpm -qpi "$SRPM"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
with:
name: dms-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI
run: |
sudo apt-get install -y python3-pip
pip3 install copr-cli
mkdir -p ~/.config
cat > ~/.config/copr << EOF
[copr-cli]
login = ${{ secrets.COPR_LOGIN }}
username = avengemedia
token = ${{ secrets.COPR_TOKEN }}
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
echo "✅ Copr CLI configured"
- name: Upload to Copr
run: |
SRPM="${{ steps.build.outputs.srpm_path }}"
VERSION="${{ steps.version.outputs.version }}"
echo "🚀 Uploading SRPM to avengemedia/dms..."
echo " SRPM: $(basename $SRPM)"
echo " Version: $VERSION"
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
echo "$BUILD_OUTPUT"
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
if [ "$BUILD_ID" != "unknown" ]; then
echo "✅ Build submitted successfully!"
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
else
echo "⚠️ Could not extract build ID, but upload may have succeeded"
fi
- name: Build summary
if: always()
run: |
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY

View File

@@ -3,13 +3,22 @@ name: Go CI
on:
push:
branches:
- '**'
- "**"
paths:
- 'core/**'
- '.github/workflows/go-ci.yml'
- "core/**"
- ".github/workflows/go-ci.yml"
pull_request:
branches: [master, main]
paths:
- "core/**"
- ".github/workflows/go-ci.yml"
concurrency:
group: go-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
lint-and-test:
runs-on: ubuntu-latest
defaults:
run:
@@ -19,24 +28,28 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
- name: Add flathub
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Add a flatpak that mutagen could support
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Format check
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
gofmt -s -l .
exit 1
fi
- name: Test
run: go test -v ./...
- name: Build dms
run: go build -v ./cmd/dms
- name: Build dms (distropkg)
run: go build -v -tags distro_binary ./cmd/dms
- name: Build dankinstall
run: go build -v ./cmd/dankinstall

23
.github/workflows/nix-pr-check.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Check nix flake
on:
pull_request:
branches: [master, main]
paths:
- "flake.*"
- "distro/nix/**"
jobs:
check-flake:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Check the flake
run: nix flake check

24
.github/workflows/prek.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Pre-commit Checks
on:
push:
pull_request:
branches: [master, main]
jobs:
pre-commit-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
- name: Add flathub
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Add a flatpak that mutagen could support
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: run pre-commit hooks
uses: j178/prek-action@v1

View File

@@ -1,16 +1,19 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g., v1.0.1)"
required: true
type: string
permissions:
contents: write
actions: write
concurrency:
group: release-${{ github.ref_name }}
group: release-${{ inputs.tag }}
cancel-in-progress: true
jobs:
@@ -24,10 +27,14 @@ jobs:
run:
working-directory: core
env:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Set up Go
@@ -54,7 +61,7 @@ jobs:
run: |
set -eux
cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ matrix.arch }}
cd ../..
gzip -9 -k dankinstall-${{ matrix.arch }}
@@ -68,7 +75,7 @@ jobs:
run: |
set -eux
cd cmd/dms
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-${{ matrix.arch }}
@@ -91,7 +98,7 @@ jobs:
run: |
set -eux
cd cmd/dms
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-distropkg-${{ matrix.arch }}
@@ -128,58 +135,61 @@ jobs:
core/completion.zsh
if-no-files-found: error
update-versions:
runs-on: ubuntu-latest
needs: build-core
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
# update-versions:
# runs-on: ubuntu-latest
# needs: build-core
# steps:
# - name: Create GitHub App token
# id: app_token
# uses: actions/create-github-app-token@v1
# with:
# app-id: ${{ secrets.APP_ID }}
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Update VERSION
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# - name: Checkout
# uses: actions/checkout@v4
# with:
# token: ${{ steps.app_token.outputs.token }}
# fetch-depth: 0
version="${GITHUB_REF#refs/tags/}"
version_no_v="${version#v}"
echo "Updating to version: $version"
# - name: Update VERSION
# env:
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
# run: |
# set -euo pipefail
# git config user.name "dms-ci[bot]"
# git config user.email "dms-ci[bot]@users.noreply.github.com"
# Update VERSION file in quickshell/
echo "${version}" > quickshell/VERSION
# version="${GITHUB_REF#refs/tags/}"
# echo "Updating to version: $version"
# echo "${version}" > quickshell/VERSION
# git add quickshell/VERSION
git add quickshell/VERSION
# if ! git diff --cached --quiet; then
# git commit -m "chore: bump version to $version"
# git pull --rebase origin master
# git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
# fi
if ! git diff --cached --quiet; then
git commit -m "chore: bump version to $version"
git push origin HEAD:master || git push origin HEAD:main
echo "Pushed version updates to master"
else
echo "No version changes needed"
fi
# Force-push the tag to point to the commit with updated VERSION
git tag -f "${version}"
git push -f origin "${version}"
# git tag -f "${version}"
# git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
release:
runs-on: ubuntu-24.04
needs: [build-core, update-versions]
needs: [build-core] #, update-versions]
env:
TAG: ${{ github.ref_name }}
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Fetch updated tag after version bump
run: |
git fetch origin --force tag ${{ github.ref_name }}
git checkout ${{ github.ref_name }}
git fetch origin --force tag ${TAG}
git checkout ${TAG}
- name: Download core artifacts
uses: actions/download-artifact@v4
@@ -271,6 +281,9 @@ jobs:
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
# Copy root assets directory to quickshell for systemd service and desktop file
cp -r assets quickshell/
# Tar the CONTENTS of quickshell/, not the directory itself
(cd quickshell && tar --exclude='.git' \
--exclude='.github' \
@@ -385,246 +398,3 @@ jobs:
prerelease: ${{ contains(env.TAG, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
copr-build:
runs-on: ubuntu-latest
needs: release
env:
TAG: ${{ github.ref_name }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building DMS stable version: $VERSION"
- name: Setup build environment
run: |
sudo apt-get update
sudo apt-get install -y rpm wget curl jq gzip
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
- name: Download release assets
run: |
VERSION="${{ steps.version.outputs.version }}"
cd ~/rpmbuild/SOURCES
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1
}
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
# Spec for DMS stable releases - Generated by GitHub Actions
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
Name: dms
Version: %{version}
Release: 1%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for the niri and hyprland compositors. Features notifications,
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%package -n dms-cli
Summary: DankMaterialShell CLI tool
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
%description -n dms-cli
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep
%setup -q -c -n dms-qml
# Download architecture-specific binaries during build
case "%{_arch}" in
x86_64)
ARCH_SUFFIX="amd64"
;;
aarch64)
ARCH_SUFFIX="arm64"
;;
*)
echo "Unsupported architecture: %{_arch}"
exit 1
;;
esac
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dms-cli for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build
%install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%files -n dms-cli
%{_bindir}/dms
%{_datadir}/bash-completion/completions/dms
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
- name: Build SRPM
id: build
run: |
cd ~/rpmbuild/SPECS
rpmbuild -bs dms.spec
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
SRPM_NAME=$(basename "$SRPM")
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
echo "SRPM built: $SRPM_NAME"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
with:
name: dms-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI
run: |
sudo apt-get install -y python3-pip
pip3 install copr-cli
mkdir -p ~/.config
cat > ~/.config/copr << EOF
[copr-cli]
login = ${{ secrets.COPR_LOGIN }}
username = avengemedia
token = ${{ secrets.COPR_TOKEN }}
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
- name: Upload to Copr
run: |
SRPM="${{ steps.build.outputs.srpm_path }}"
VERSION="${{ steps.version.outputs.version }}"
echo "Uploading SRPM to avengemedia/dms..."
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
echo "$BUILD_OUTPUT"
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
if [ "$BUILD_ID" != "unknown" ]; then
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
fi

210
.github/workflows/run-copr.yml vendored Normal file
View File

@@ -0,0 +1,210 @@
name: DMS Copr Stable Release
on:
workflow_dispatch:
inputs:
package:
description: 'Package to build (dms, dms-greeter, or both)'
required: false
default: 'dms'
type: choice
options:
- dms
- dms-greeter
- both
version:
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
required: false
default: ''
release:
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
required: false
default: '1'
jobs:
determine-packages:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.set-packages.outputs.packages }}
steps:
- name: Set package list
id: set-packages
run: |
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
if [ "$PACKAGE_INPUT" = "both" ]; then
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
else
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
fi
build-and-upload:
needs: determine-packages
runs-on: ubuntu-latest
strategy:
matrix:
package: ${{ fromJSON(needs.determine-packages.outputs.packages) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
# Get version from manual input or latest release
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
echo "Using manual version: $VERSION"
else
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
echo "Using latest release version: $VERSION"
fi
RELEASE="${{ github.event.inputs.release }}"
if [ -z "$RELEASE" ]; then
RELEASE="1"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release=$RELEASE" >> $GITHUB_OUTPUT
echo "✅ Building ${{ matrix.package }} version: $VERSION-$RELEASE"
- name: Setup build environment
run: |
sudo apt-get update
sudo apt-get install -y rpm wget curl jq gzip
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
echo "✅ RPM build environment ready"
- name: Download release assets
run: |
VERSION="${{ steps.version.outputs.version }}"
cd ~/rpmbuild/SOURCES
echo "📦 Downloading DMS QML source for v${VERSION}..."
# Download DMS QML source
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1
}
echo "✅ Source downloaded"
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
ls -lh
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE="${{ steps.version.outputs.release }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
PACKAGE="${{ matrix.package }}"
# Copy spec file from repository
cp distro/fedora/${PACKAGE}.spec ~/rpmbuild/SPECS/${PACKAGE}.spec
# Replace placeholders with actual values
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
echo "✅ Spec file generated for ${PACKAGE} v${VERSION}-${RELEASE}"
echo ""
echo "=== Spec file preview ==="
head -40 ~/rpmbuild/SPECS/${PACKAGE}.spec
- name: Build SRPM
id: build
run: |
cd ~/rpmbuild/SPECS
PACKAGE="${{ matrix.package }}"
echo "🔨 Building SRPM for ${PACKAGE}..."
rpmbuild -bs ${PACKAGE}.spec
SRPM=$(ls ~/rpmbuild/SRPMS/${PACKAGE}-*.src.rpm | tail -n 1)
SRPM_NAME=$(basename "$SRPM")
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
echo "✅ SRPM built: $SRPM_NAME"
echo ""
echo "=== SRPM Info ==="
rpm -qpi "$SRPM"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI
run: |
sudo apt-get install -y python3-pip
pip3 install copr-cli
mkdir -p ~/.config
cat > ~/.config/copr << EOF
[copr-cli]
login = ${{ secrets.COPR_LOGIN }}
username = avengemedia
token = ${{ secrets.COPR_TOKEN }}
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
echo "✅ Copr CLI configured"
- name: Determine Copr project
id: copr_project
run: |
PACKAGE="${{ matrix.package }}"
if [ "$PACKAGE" = "dms" ]; then
COPR_PROJECT="avengemedia/dms"
elif [ "$PACKAGE" = "dms-greeter" ]; then
COPR_PROJECT="avengemedia/danklinux"
else
echo "❌ Unknown package: $PACKAGE"
exit 1
fi
echo "copr_project=$COPR_PROJECT" >> $GITHUB_OUTPUT
echo "✅ Copr project: $COPR_PROJECT"
- name: Upload to Copr
run: |
SRPM="${{ steps.build.outputs.srpm_path }}"
VERSION="${{ steps.version.outputs.version }}"
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
PACKAGE="${{ matrix.package }}"
echo "🚀 Uploading ${PACKAGE} SRPM to ${COPR_PROJECT}..."
echo " SRPM: $(basename $SRPM)"
echo " Version: $VERSION"
BUILD_OUTPUT=$(copr-cli build "$COPR_PROJECT" "$SRPM" --nowait 2>&1)
echo "$BUILD_OUTPUT"
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
if [ "$BUILD_ID" != "unknown" ]; then
echo "✅ Build submitted successfully!"
echo "🔗 https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/build/$BUILD_ID/"
else
echo "⚠️ Could not extract build ID, but upload may have succeeded"
fi
- name: Build summary
if: always()
run: |
PACKAGE="${{ matrix.package }}"
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
echo "### 🎉 ${PACKAGE} Stable Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Package:** ${PACKAGE}" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY

432
.github/workflows/run-obs.yml vendored Normal file
View File

@@ -0,0 +1,432 @@
name: Update OBS Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to update"
required: true
type: choice
options:
- dms
- dms-git
- all
default: "dms"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
required: false
default: ""
schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for updates
id: check
env:
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
run: |
# Helper function to check dms-git commit
check_dms_git() {
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-git: New commit $CURRENT_COMMIT (OBS has ${OBS_COMMIT:-none})"
return 0 # Update needed
fi
}
# Helper function to check dms stable tag
# Sets LATEST_TAG variable in parent scope if update needed
check_dms_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag selected or pushed - always update stable package
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION (always update)"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - check dms-git only
echo "packages=dms-git" >> $GITHUB_OUTPUT
if check_dms_git; then
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow trigger
PKG="${{ github.event.inputs.package }}"
if [[ -n "$REBUILD" ]]; then
# Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
if check_dms_git; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms" ]]; then
if check_dms_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: $PKG"
fi
else
# Fallback - proceed
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
update-obs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
if: needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine packages to update
id: packages
run: |
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag selected or pushed - use the tag from GITHUB_REF
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION"
# Check if check-updates already determined a version (from auto-detection)
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
# Use version from check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - dms-git only
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch
# Determine version for dms stable
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
# Use github.ref if tag selected, otherwise auto-detect latest
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION"
else
# Auto-detect latest release for dms
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Auto-detected latest release: $LATEST_TAG"
else
echo "ERROR: Could not auto-detect latest release"
exit 1
fi
fi
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
# Use github.ref if tag selected, otherwise auto-detect latest
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION"
else
# Auto-detect latest release for "all"
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Auto-detected latest release: $LATEST_TAG"
else
echo "ERROR: Could not auto-detect latest release"
exit 1
fi
fi
fi
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
else
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
fi
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
fi
fi
- name: Update dms-git spec version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
# Single changelog entry (git snapshots don't need history)
DATE_STR=$(date "+%a %b %d %Y")
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms-git.spec)
{
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1"
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
} > distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
# Single changelog entry (git snapshots don't need history)
CHANGELOG_DATE=$(date -R)
{
echo "dms-git (${NEW_VERSION}db1) nightly; urgency=medium"
echo ""
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog"
- name: Update dms stable version
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "==> Updating packaging files to version: $VERSION_NO_V"
# Update spec file
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Verify the update
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
# Single changelog entry (full history on OBS website)
DATE_STR=$(date "+%a %b %d %Y")
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
{
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
echo "- Update to stable $VERSION release"
} > distro/opensuse/dms.spec
# Update Debian _service files (both tar_scm and download_url formats)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
done
# Update Debian changelog for dms stable (single entry, history on OBS website)
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
fi
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
TAG_VERSION: ${{ steps.packages.outputs.version }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!"
exit 0
fi
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
fi
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PKG to OBS..."
if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: db$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ "$PKG" == "dms-git" ]]; then
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
else
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
fi
done
- name: Summary
if: always()
run: |
echo "### OBS Package Upload Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ -z "$PACKAGES" ]]; then
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
else
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do
case "$PKG" in
dms)
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
;;
dms-git)
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "**Rebuild Number:** db${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $GITHUB_STEP_SUMMARY
fi

280
.github/workflows/run-ppa.yml vendored Normal file
View File

@@ -0,0 +1,280 @@
name: Update PPA Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: false
default: "dms-git"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false
default: ""
schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for updates
id: check
run: |
# Helper function to check dms-git commit
check_dms_git() {
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
return 0 # Update needed
fi
}
# Helper function to check stable package tag
check_stable_package() {
local PKG="$1"
local PPA_NAME="$2"
# Use git ls-remote to find the latest tag, sorted by version (descending)
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - check dms-git only
echo "packages=dms-git" >> $GITHUB_OUTPUT
if check_dms_git; then
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow trigger
PKG="${{ github.event.inputs.package }}"
if [[ -n "$REBUILD" ]]; then
# Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
if check_dms_git; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms" ]]; then
if check_stable_package "dms" "dms"; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_stable_package "dms-greeter" "danklinux"; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: $PKG"
fi
else
# Fallback
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
upload-ppa:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
if: needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache: false
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Determine packages to upload
id: packages
run: |
# Use packages determined by check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
fi
- name: Upload to PPA
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!"
exit 0
fi
# Export REBUILD_RELEASE so ppa-build.sh can use it
if [[ -n "$REBUILD_RELEASE" ]]; then
export REBUILD_RELEASE
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
# Map package to PPA name
case "$PKG" in
dms)
PPA_NAME="dms"
;;
dms-git)
PPA_NAME="dms-git"
;;
dms-greeter)
PPA_NAME="danklinux"
;;
*)
echo "⚠️ Unknown package: $PKG, skipping"
continue
;;
esac
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PKG to PPA $PPA_NAME..."
if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
done
- name: Summary
if: always()
run: |
echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ -z "$PACKAGES" ]]; then
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
else
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do
case "$PKG" in
dms)
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
;;
dms-git)
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
fi

31
.github/workflows/stable.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Update stable branch
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
update-stable:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Push to stable branch
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force

View File

@@ -1,6 +1,7 @@
name: Update Vendor Hash
on:
workflow_dispatch:
push:
paths:
- "core/go.mod"
@@ -8,14 +9,25 @@ on:
branches:
- master
permissions:
contents: write
jobs:
update-vendor-hash:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
@@ -23,68 +35,32 @@ jobs:
- name: Update vendorHash in flake.nix
run: |
set -euo pipefail
# Try to build and capture the expected hash from error message
echo "Attempting nix build to get new vendorHash..."
if output=$(nix build .#dmsCli 2>&1); then
if output=$(nix build .#dms-shell 2>&1); then
echo "Build succeeded, no hash update needed"
exit 0
fi
# Extract the expected hash from the error message
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
if [ -z "$new_hash" ]; then
echo "Could not extract new vendorHash from build output"
echo "Build output:"
echo "$output"
exit 1
fi
echo "New vendorHash: $new_hash"
# Get current hash from flake.nix
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
echo "Current vendorHash: $current_hash"
if [ "$current_hash" = "$new_hash" ]; then
echo "vendorHash is already up to date"
exit 0
fi
# Update the hash in flake.nix
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
# Verify the build works with the new hash
echo "Verifying build with new vendorHash..."
nix build .#dmsCli
nix build .#dms-shell
echo "vendorHash updated successfully!"
- name: Commit and push vendorHash update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if ! git diff --quiet flake.nix; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes"
for attempt in 1 2 3; do
if git push; then
echo "Successfully pushed vendorHash update"
exit 0
fi
echo "Push attempt $attempt failed, pulling and retrying..."
git pull --rebase
sleep $((attempt*2))
done
echo "Failed to push after retries" >&2
exit 1
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
else
echo "No changes to flake.nix"
fi

42
.gitignore vendored
View File

@@ -96,43 +96,17 @@ go.work
go.work.sum
# env file
.env
# Editor/IDE
# .idea/
# .vscode/
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
.env*
# Editor/IDE
# .idea/
# .vscode/
vim/
bin/
# direnv
.envrc
.direnv/
quickshell/dms-plugins
__pycache__

12
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,12 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: end-of-file-fixer
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]

23
CHANGELOG.MD Normal file
View File

@@ -0,0 +1,23 @@
This file is more of a quick reference so I know what to account for before next releases.
# 1.2.0
- Added clipboard and clipboard history integration
- Added swipe to dismiss notification popups and from center
- Added paste from clipboard history view - requires wtype
- Optimize surface damage of OSD & Toast
- Add monitor configurator (niri, Hyprland, MangoWC)
- **BREAKING** ghostty theme changed to ~/.config/ghostty/themes/danktheme
- requires intervention and doc update
- Added desktop widget plugins
- dev guidance available
- builtin clock & dgop widgets
- new IPC targets
- Initial RTL support/i18n
- Theme registry
- Notification persistence & history
- **BREAKING** vscode theme needs re-installed
- dms doctor cmd
- niri/hypr/mango gaps/window/border overrides
- settings search
- notification display ops on lock screen

View File

@@ -2,28 +2,80 @@
Contributions are welcome and encouraged.
## Formatting
To contribute fork this repository, make your changes, and open a pull request.
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported.
## Setup
We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
```json
"customLocalFormatters.formatters": [
{
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
"languages": ["qml"]
}
],
"[qml]": {
"editor.defaultFormatter": "jkillian.custom-local-formatters",
"editor.formatOnSave": true
},
```bash
prek install
```
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
### Nix Development Shell
If you have Nix installed with flakes enabled, you can use the provided development shell which includes all necessary dependencies:
```bash
nix develop
```
This will provide:
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
- Quickshell and required QML packages
- Properly configured QML2_IMPORT_PATH
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
## VSCode Setup
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
### QML (`quickshell` directory)
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
```json
{
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
}
```
3. Create empty `.qmlls.ini` file in `quickshell/` directory
```bash
cd quickshell
touch .qmlls.ini
```
4. Restart dms to generate the `.qmlls.ini` file
5. Make your changes, test, and open a pull request.
### I18n/Localization
When adding user-facing strings, ensure they are wrapped in `I18n.tr()` with context, for example.
```qml
import qs.Common
Text {
text: I18n.tr("Hello World", "<This is context for the translators, example> Hello world greeting that appears on the lock screen")
}
```
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
2. Ensure code is formatted with `make fmt`
3. Add appropriate test coverage and ensure tests pass with `make test`
4. Run `go mod tidy`
5. Open pull request
## Pull request

156
Makefile Normal file
View File

@@ -0,0 +1,156 @@
# Root Makefile for DankMaterialShell (DMS)
# Orchestrates building, installation, and systemd management
# Build configuration
BINARY_NAME=dms
CORE_DIR=core
BUILD_DIR=$(CORE_DIR)/bin
PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin
DATA_DIR=$(PREFIX)/share
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user
SHELL_DIR=quickshell
SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
all: build
build:
@echo "Building $(BINARY_NAME)..."
@$(MAKE) -C $(CORE_DIR) build
@echo "Build complete"
clean:
@echo "Cleaning build artifacts..."
@$(MAKE) -C $(CORE_DIR) clean
@echo "Clean complete"
# Installation targets
install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Binary installed"
install-shell:
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
@mkdir -p $(SHELL_INSTALL_DIR)
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
@echo "Shell files installed"
install-completions:
@echo "Installing shell completions..."
@mkdir -p $(DATA_DIR)/bash-completion/completions
@mkdir -p $(DATA_DIR)/zsh/site-functions
@mkdir -p $(DATA_DIR)/fish/vendor_completions.d
@$(BUILD_DIR)/$(BINARY_NAME) completion bash > $(DATA_DIR)/bash-completion/completions/dms 2>/dev/null || true
@$(BUILD_DIR)/$(BINARY_NAME) completion zsh > $(DATA_DIR)/zsh/site-functions/_dms 2>/dev/null || true
@$(BUILD_DIR)/$(BINARY_NAME) completion fish > $(DATA_DIR)/fish/vendor_completions.d/dms.fish 2>/dev/null || true
@echo "Shell completions installed"
install-systemd:
@echo "Installing systemd user service..."
@mkdir -p $(SYSTEMD_USER_DIR)
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
install-icon:
@echo "Installing icon..."
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
@echo "Icon installed"
install-desktop:
@echo "Installing desktop entry..."
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry installed"
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
@echo ""
@echo "Installation complete!"
@echo ""
@echo "=== Cheers, the DMS Team! ==="
# Uninstallation targets
uninstall-bin:
@echo "Removing $(BINARY_NAME) from $(INSTALL_DIR)..."
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Binary removed"
uninstall-shell:
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
@rm -rf $(SHELL_INSTALL_DIR)
@echo "Shell files removed"
uninstall-completions:
@echo "Removing shell completions..."
@rm -f $(DATA_DIR)/bash-completion/completions/dms
@rm -f $(DATA_DIR)/zsh/site-functions/_dms
@rm -f $(DATA_DIR)/fish/vendor_completions.d/dms.fish
@echo "Shell completions removed"
uninstall-systemd:
@echo "Removing systemd user service..."
@rm -f $(SYSTEMD_USER_DIR)/dms.service
@echo "Systemd service removed"
@echo "Note: Stop/disable service manually if running: systemctl --user stop dms"
uninstall-icon:
@echo "Removing icon..."
@rm -f $(ICON_DIR)/danklogo.svg
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
@echo "Icon removed"
uninstall-desktop:
@echo "Removing desktop entry..."
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry removed"
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
@echo ""
@echo "Uninstallation complete!"
# Target assist
help:
@echo "Available targets:"
@echo ""
@echo "Build:"
@echo " all (default) - Build the DMS binary"
@echo " build - Same as 'all'"
@echo " clean - Clean build artifacts"
@echo ""
@echo "Install:"
@echo " install - Build and install everything (requires sudo)"
@echo " install-bin - Install only the binary"
@echo " install-shell - Install only shell files"
@echo " install-completions - Install only shell completions"
@echo " install-systemd - Install only systemd service"
@echo " install-icon - Install only icon"
@echo " install-desktop - Install only desktop entry"
@echo ""
@echo "Uninstall:"
@echo " uninstall - Remove everything (requires sudo)"
@echo " uninstall-bin - Remove only the binary"
@echo " uninstall-shell - Remove only shell files"
@echo " uninstall-completions - Remove only shell completions"
@echo " uninstall-systemd - Remove only systemd service"
@echo " uninstall-icon - Remove only icon"
@echo " uninstall-desktop - Remove only desktop entry"
@echo ""
@echo "Usage:"
@echo " sudo make install - Build and install DMS"
@echo " sudo make uninstall - Remove DMS"
@echo " systemctl --user enable --now dms - Enable and start service"

View File

@@ -5,21 +5,21 @@
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
</a>
### A modern desktop shell for Wayland
### A modern desktop shell for Wayland
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
[![Documentation](https://img.shields.io/badge/docs-danklinux.com-9ccbfb?style=for-the-badge&labelColor=101418)](https://danklinux.com/docs)
[![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
[![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases)
[![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin)
[![AUR version (git)](https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git))](https://aur.archlinux.org/packages/dms-shell-git)
[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Favengemediallc)](https://ko-fi.com/avengemediallc)
[![AUR version (git)](<https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git)>)](https://aur.archlinux.org/packages/dms-shell-git)
[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux)
</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/), 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), 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
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## 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), and [labwc](https://labwc.github.io/) 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/), and [Scroll](https://github.com/dawsers/scroll) 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)
@@ -127,7 +127,7 @@ dms plugins search # Browse plugin registry
## Documentation
- **Website:** [danklinux.com](https://danklinux.com)
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs)
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs/)
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
@@ -143,6 +143,7 @@ See component-specific documentation:
### Building from Source
**Core + Dankinstall:**
```bash
cd core
make # Build dms CLI
@@ -150,17 +151,19 @@ make dankinstall # Build installer
```
**Shell:**
```bash
quickshell -p quickshell/
```
**NixOS:**
```nix
{
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
# Use in home-manager or NixOS configuration
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
imports = [ inputs.dms.homeModules.dank-material-shell ];
}
```

View File

@@ -1 +0,0 @@
indentation = "FourSpaces"

10
assets/dms-open.desktop Normal file
View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=DMS
Comment=Select an application to open links and files
Exec=dms open %u
Icon=danklogo
Terminal=false
NoDisplay=true
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/dms;text/html;application/xhtml+xml;
Categories=Utility;

View File

@@ -5,12 +5,13 @@ After=graphical-session.target
Requisite=graphical-session.target
[Service]
Type=simple
Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms
Restart=always
RestartSec=2
Restart=on-failure
RestartSec=1.23
TimeoutStopSec=10
[Install]
WantedBy=graphical-session.target
WantedBy=graphical-session.target

112
core/.golangci.yml Normal file
View File

@@ -0,0 +1,112 @@
version: "2"
linters:
enable:
- revive
settings:
revive:
rules:
- name: use-any
severity: error
errcheck:
check-type-assertions: false
check-blank: false
exclude-functions:
# Cleanup/destroy operations
- (io.Closer).Close
- (*os.File).Close
- (net.Conn).Close
- (*net.Conn).Close
# Signal handling
- (*os.Process).Signal
- (*os.Process).Kill
- syscall.Kill
# Seek on memfd (reset position before passing fd)
- syscall.Seek
# DBus cleanup
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
# Encoding to network connections (if conn is bad, nothing we can do)
- (*encoding/json.Encoder).Encode
- (net.Conn).Write
# Command execution where failure is expected/ignored
- (*os/exec.Cmd).Run
- (*os/exec.Cmd).Start
# Flush operations
- (*bufio.Writer).Flush
# Scanning user input
- fmt.Scanln
- fmt.Scanf
# Parse operations where default value is acceptable
- fmt.Sscanf
# Flag operations
- (*github.com/spf13/pflag.FlagSet).MarkHidden
# Binary encoding to buffer (can't fail for basic types)
- binary.Write
# File operations in cleanup paths
- os.Rename
- os.Remove
- os.RemoveAll
- (*os.File).WriteString
# Stdout/stderr writes (can't meaningfully handle failure)
- fmt.Fprintln
- fmt.Fprintf
- fmt.Fprint
# Writing to pipes (if pipe is bad, nothing we can do)
- (*io.PipeWriter).Write
- (*os.File).Write
exclusions:
rules:
# Exclude generated mocks from all linters
- path: internal/mocks/
linters:
- errcheck
- govet
- unused
- ineffassign
- staticcheck
- gosimple
- revive
- path: _test\.go
linters:
- errcheck
- govet
- unused
- ineffassign
- staticcheck
- gosimple
# Exclude cleanup/teardown method calls from errcheck
- linters:
- errcheck
text: "Error return value of `.+\\.(Destroy|Release|Stop|Close|Roundtrip|Store)` is not checked"
# Exclude internal state update methods that are best-effort
- linters:
- errcheck
text: "Error return value of `[mb]\\.\\w*(update|initialize|recreate|acquire|enumerate|list|List|Ensure|refresh|Lock)\\w*` is not checked"
# Exclude SetMode on wayland power controls (best-effort)
- linters:
- errcheck
text: "Error return value of `.+\\.SetMode` is not checked"
# Exclude AddMatchSignal which is best-effort monitoring setup
- linters:
- errcheck
text: "Error return value of `.+\\.AddMatchSignal` is not checked"
# Exclude wayland pkg from errcheck and ineffassign (generated code patterns)
- linters:
- errcheck
- ineffassign
path: pkg/go-wayland/
# Exclude proto pkg from ineffassign (generated protocol code)
- linters:
- ineffassign
path: internal/proto/
# binary.Write/Read to bytes.Buffer can't fail
- linters:
- errcheck
text: "Error return value of `binary\\.(Write|Read)` is not checked"
# bytes.Reader.Read can't fail (reads from memory)
- linters:
- errcheck
text: "Error return value of `buf\\.Read` is not checked"

View File

@@ -28,21 +28,49 @@ packages:
outpkg: mocks_brightness
interfaces:
DBusConn:
github.com/AvengeMedia/danklinux/internal/server/network:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config:
dir: "internal/mocks/network"
outpkg: mocks_network
interfaces:
Backend:
github.com/AvengeMedia/danklinux/internal/server/cups:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups:
config:
dir: "internal/mocks/cups"
outpkg: mocks_cups
interfaces:
CUPSClientInterface:
PkHelper:
config:
dir: "internal/mocks/cups_pkhelper"
outpkg: mocks_cups_pkhelper
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
config:
dir: "internal/mocks/evdev"
outpkg: mocks_evdev
interfaces:
EvdevDevice:
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
config:
dir: "internal/mocks/version"
outpkg: mocks_version
interfaces:
VersionFetcher:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
config:
dir: "internal/mocks/wlcontext"
outpkg: mocks_wlcontext
interfaces:
WaylandContext:
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
config:
dir: "internal/mocks/wlclient"
outpkg: mocks_wlclient
interfaces:
WaylandDisplay:
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
config:
dir: "internal/mocks/utils"
outpkg: mocks_utils
interfaces:
AppChecker:

View File

@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.6.2
hooks:
- id: golangci-lint-fmt
require_serial: true
- id: golangci-lint-full
- id: golangci-lint-config-verify
- repo: local
hooks:
- id: go-test
name: go test
entry: go test ./...
language: system
pass_filenames: false
types: [go]

View File

@@ -10,16 +10,19 @@ GO=go
GOFLAGS=-ldflags="-s -w"
# Version and build info
VERSION=$(shell git describe --tags --always 2>/dev/null || echo "dev")
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BASE_VERSION=$(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")
COMMIT_COUNT=$(shell git rev-list --count HEAD 2>/dev/null || echo "0")
COMMIT_HASH=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "unknown")
VERSION?=$(BASE_VERSION)+git$(COMMIT_COUNT).$(COMMIT_HASH)
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
COMMIT?=$(COMMIT_HASH)
BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.commit=$(COMMIT)'
# Architecture to build for dist target (amd64, arm64, or all)
ARCH ?= all
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps help
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
# Default target
all: build
@@ -132,6 +135,9 @@ version: check-go
@echo "Build Time: $(BUILD_TIME)"
@echo "Commit: $(COMMIT)"
print-version:
@echo "$(VERSION)"
help:
@echo "Available targets:"
@echo " all - Build the main binary (dms) (default)"

View File

@@ -14,44 +14,92 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
## System Integration
**Wayland Protocols**
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
- `ext-workspace-v1` - Workspace protocol support
- `wlr-output-management-unstable-v1` - Display configuration
### Wayland Protocols (Client)
**DBus Interfaces**
- NetworkManager/iwd - Network management
- logind - Session control and inhibit locks
- accountsservice - User account information
- CUPS - Printer management
- Custom IPC via unix socket (JSON API)
All Wayland protocols are consumed as a client - connecting to the compositor.
**Hardware Control**
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
- Backlight control - Internal display brightness via `login1` or sysfs
- LED control - Keyboard/device LED management
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
| Protocol | Purpose |
| ----------------------------------------- | ----------------------------------------------------------- |
| `wlr-gamma-control-unstable-v1` | Night mode color temperature control |
| `wlr-screencopy-unstable-v1` | Screen capture for color picker/screenshot |
| `wlr-layer-shell-unstable-v1` | Overlay surfaces for color picker UI/screenshot |
| `wlr-output-management-unstable-v1` | Display configuration |
| `wlr-output-power-management-unstable-v1` | DPMS on/off CLI |
| `wp-viewporter` | Fractional scaling support (color picker/screenshot UIs) |
| `keyboard-shortcuts-inhibit-unstable-v1` | Inhibit compositor shortcuts during color picker/screenshot |
| `ext-data-control-v1` | Clipboard history and persistence |
| `ext-workspace-v1` | Workspace integration |
| `dwl-ipc-unstable-v2` | dwl/MangoWC IPC for tags, outputs, etc. |
### DBus Interfaces
**Client (consuming external services):**
| Interface | Purpose |
| -------------------------------- | --------------------------------------------- |
| `org.bluez` | Bluetooth management with pairing agent |
| `org.freedesktop.NetworkManager` | Network management |
| `net.connman.iwd` | iwd Wi-Fi backend |
| `org.freedesktop.network1` | systemd-networkd integration |
| `org.freedesktop.login1` | Session control, sleep inhibitors, brightness |
| `org.freedesktop.Accounts` | User account information |
| `org.freedesktop.portal.Desktop` | Desktop appearance settings (color scheme) |
| CUPS via IPP + D-Bus | Printer management with job notifications |
**Server (implementing interfaces):**
| Interface | Purpose |
| ----------------------------- | -------------------------------------- |
| `org.freedesktop.ScreenSaver` | Screensaver inhibit for video playback |
Custom IPC via unix socket (JSON API) for shell communication.
### Hardware Control
| Subsystem | Method | Purpose |
| --------- | ------------------- | ---------------------------------- |
| DDC/CI | I2C direct | External monitor brightness |
| Backlight | logind or sysfs | Internal display brightness |
| evdev | `/dev/input/event*` | Keyboard state (caps lock LED) |
| udev | netlink monitor | Backlight device updates (for OSD) |
### Plugin System
**Plugin System**
- Plugin registry integration
- Plugin lifecycle management
- Settings persistence
## CLI Commands
- `dms run [-d]` - Start shell (optionally as daemon)
- `dms restart` / `dms kill` - Manage running processes
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
- `dms plugins [install|browse|search]` - Plugin management
- `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages)
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
### Color Picker
Native Wayland color picker with magnifier, no external dependencies. Supports HiDPI and fractional scaling.
```bash
dms color pick # Pick color, output hex
dms color pick --rgb # Output as RGB (255 128 64)
dms color pick --hsv # Output as HSV (24 75% 100%)
dms color pick --json # Output all formats as JSON
dms color pick -a # Auto-copy to clipboard
```
The on-screen preview displays the selected format. JSON output includes hex, RGB, HSL, HSV, and CMYK values.
## Building
Requires Go 1.24+
**Development build:**
```bash
make # Build dms CLI
make dankinstall # Build installer
@@ -59,6 +107,7 @@ make test # Run tests
```
**Distribution build:**
```bash
make dist # Build without update/greeter features
```
@@ -66,13 +115,23 @@ make dist # Build without update/greeter features
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
**Installation:**
```bash
sudo make install # Install to /usr/local/bin/dms
```
## Development
**Setup pre-commit hooks:**
```bash
git config core.hooksPath .githooks
```
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
**Regenerating Wayland Protocol Bindings:**
```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
@@ -80,6 +139,7 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
```
**Module Structure:**
- `cmd/` - Binary entrypoints (dms, dankinstall)
- `internal/distros/` - Distribution-specific installation logic
- `internal/proto/` - Wayland protocol bindings
@@ -114,4 +174,4 @@ Most packages available in standard repos. Minimal building required.
**Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
See installer output for distribution-specific details during installation.
See installer output for distribution-specific details during installation.

View File

@@ -8,7 +8,7 @@
<rect x="0" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="20" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="0" y="37" width="24" height="8" fill="#CCBEFF"/>
<!-- A -->
<rect x="36" y="5" width="20" height="8" fill="#CCBEFF"/>
<rect x="32" y="13" width="8" height="8" fill="#CCBEFF"/>
@@ -18,7 +18,7 @@
<rect x="52" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="32" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="52" y="37" width="8" height="8" fill="#CCBEFF"/>
<!-- N -->
<rect x="64" y="5" width="12" height="8" fill="#CCBEFF"/>
<rect x="92" y="5" width="8" height="8" fill="#CCBEFF"/>
@@ -32,7 +32,7 @@
<rect x="92" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="64" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="84" y="37" width="16" height="8" fill="#CCBEFF"/>
<!-- K -->
<rect x="104" y="5" width="8" height="8" fill="#CCBEFF"/>
<rect x="124" y="5" width="8" height="8" fill="#CCBEFF"/>
@@ -43,4 +43,4 @@
<rect x="120" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="104" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="124" y="37" width="8" height="8" fill="#CCBEFF"/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -12,6 +12,11 @@ import (
var Version = "dev"
func main() {
if os.Getuid() == 0 {
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
os.Exit(1)
}
fileLogger, err := log.NewFileLogger()
if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err)

View File

@@ -28,7 +28,14 @@ var brightnessSetCmd = &cobra.Command{
Short: "Set brightness for a device",
Long: "Set brightness percentage (0-100) for a specific device",
Args: cobra.ExactArgs(2),
Run: runBrightnessSet,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
includeDDC, _ := cmd.Flags().GetBool("ddc")
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
},
Run: runBrightnessSet,
}
var brightnessGetCmd = &cobra.Command{
@@ -36,7 +43,14 @@ var brightnessGetCmd = &cobra.Command{
Short: "Get brightness for a device",
Long: "Get current brightness percentage for a specific device",
Args: cobra.ExactArgs(1),
Run: runBrightnessGet,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
includeDDC, _ := cmd.Flags().GetBool("ddc")
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
},
Run: runBrightnessGet,
}
func init() {
@@ -105,9 +119,7 @@ Global Flags:
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
}
func runBrightnessList(cmd *cobra.Command, args []string) {
includeDDC, _ := cmd.Flags().GetBool("ddc")
func getAllBrightnessDevices(includeDDC bool) []brightness.Device {
allDevices := []brightness.Device{}
sysfs, err := brightness.NewSysfsBackend()
@@ -138,6 +150,13 @@ func runBrightnessList(cmd *cobra.Command, args []string) {
}
}
return allDevices
}
func runBrightnessList(cmd *cobra.Command, args []string) {
includeDDC, _ := cmd.Flags().GetBool("ddc")
allDevices := getAllBrightnessDevices(includeDDC)
if len(allDevices) == 0 {
fmt.Println("No brightness devices found")
return
@@ -160,7 +179,7 @@ func runBrightnessList(cmd *cobra.Command, args []string) {
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
for i := 0; i < sepLen; i++ {
for range sepLen {
fmt.Print("─")
}
fmt.Println()
@@ -192,45 +211,13 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
exponential, _ := cmd.Flags().GetBool("exponential")
exponent, _ := cmd.Flags().GetFloat64("exponent")
// For backlight/leds devices, try logind backend first (requires D-Bus connection)
parts := strings.SplitN(deviceID, ":", 2)
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
subsystem := parts[0]
name := parts[1]
// Initialize backends needed for logind approach
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("NewSysfsBackend failed: %v", err)
} else {
logind, err := brightness.NewLogindBackend()
if err != nil {
log.Debugf("NewLogindBackend failed: %v", err)
} else {
defer logind.Close()
// Get device info to convert percent to value
dev, err := sysfs.GetDevice(deviceID)
if err == nil {
// Calculate hardware value using the same logic as Manager.setViaSysfsWithLogind
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
// Call logind with hardware value
if err := logind.SetBrightness(subsystem, name, uint32(value)); err == nil {
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return
} else {
log.Debugf("logind.SetBrightness failed: %v", err)
}
} else {
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
}
}
if ok := tryLogindBrightness(parts[0], parts[1], deviceID, percent, exponential, exponent); ok {
return
}
}
// Fallback to direct sysfs (requires write permissions)
sysfs, err := brightness.NewSysfsBackend()
if err == nil {
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
@@ -261,31 +248,51 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
log.Fatalf("Failed to set brightness for device: %s", deviceID)
}
func tryLogindBrightness(subsystem, name, deviceID string, percent int, exponential bool, exponent float64) bool {
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("NewSysfsBackend failed: %v", err)
return false
}
logind, err := brightness.NewLogindBackend()
if err != nil {
log.Debugf("NewLogindBackend failed: %v", err)
return false
}
defer logind.Close()
dev, err := sysfs.GetDevice(deviceID)
if err != nil {
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
return false
}
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
if err := logind.SetBrightness(subsystem, name, uint32(value)); err != nil {
log.Debugf("logind.SetBrightness failed: %v", err)
return false
}
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return true
}
func getBrightnessDevices(includeDDC bool) []string {
allDevices := getAllBrightnessDevices(includeDDC)
var deviceIDs []string
for _, device := range allDevices {
deviceIDs = append(deviceIDs, device.ID)
}
return deviceIDs
}
func runBrightnessGet(cmd *cobra.Command, args []string) {
deviceID := args[0]
includeDDC, _ := cmd.Flags().GetBool("ddc")
allDevices := []brightness.Device{}
sysfs, err := brightness.NewSysfsBackend()
if err == nil {
devices, err := sysfs.GetDevices()
if err == nil {
allDevices = append(allDevices, devices...)
}
}
if includeDDC {
ddc, err := brightness.NewDDCBackend()
if err == nil {
defer ddc.Close()
time.Sleep(100 * time.Millisecond)
devices, err := ddc.GetDevices()
if err == nil {
allDevices = append(allDevices, devices...)
}
}
}
allDevices := getAllBrightnessDevices(includeDDC)
for _, device := range allDevices {
if device.ID == deviceID {

View File

@@ -0,0 +1,797 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
bolt "go.etcd.io/bbolt"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
var clipboardCmd = &cobra.Command{
Use: "clipboard",
Aliases: []string{"cl"},
Short: "Manage clipboard",
Long: "Interact with the clipboard manager",
}
var clipCopyCmd = &cobra.Command{
Use: "copy [text]",
Short: "Copy text to clipboard",
Long: "Copy text to clipboard. If no text provided, reads from stdin. Works without server.",
Run: runClipCopy,
}
var (
clipCopyForeground bool
clipCopyPasteOnce bool
clipCopyType string
clipJSONOutput bool
)
var clipPasteCmd = &cobra.Command{
Use: "paste",
Short: "Paste text from clipboard",
Long: "Paste text from clipboard to stdout. Works without server.",
Run: runClipPaste,
}
var clipWatchCmd = &cobra.Command{
Use: "watch [command]",
Short: "Watch clipboard for changes",
Long: `Watch clipboard for changes and optionally execute a command.
Works like wl-paste --watch. Does not require server.
If a command is provided, it will be executed each time the clipboard changes,
with the clipboard content piped to its stdin.
Examples:
dms cl watch # Print clipboard changes to stdout
dms cl watch cat # Same as above
dms cl watch notify-send # Send notification on clipboard change`,
Run: runClipWatch,
}
var clipHistoryCmd = &cobra.Command{
Use: "history",
Short: "Show clipboard history",
Long: "Show clipboard history with previews (requires server)",
Run: runClipHistory,
}
var clipGetCmd = &cobra.Command{
Use: "get <id>",
Short: "Get clipboard entry by ID",
Long: "Get full clipboard entry data by ID (requires server). Use --copy to copy it to clipboard.",
Args: cobra.ExactArgs(1),
Run: runClipGet,
}
var clipGetCopy bool
var clipDeleteCmd = &cobra.Command{
Use: "delete <id>",
Short: "Delete clipboard entry",
Long: "Delete a clipboard history entry by ID (requires server)",
Args: cobra.ExactArgs(1),
Run: runClipDelete,
}
var clipClearCmd = &cobra.Command{
Use: "clear",
Short: "Clear clipboard history",
Long: "Clear all clipboard history (requires server)",
Run: runClipClear,
}
var clipWatchStore bool
var clipSearchCmd = &cobra.Command{
Use: "search [query]",
Short: "Search clipboard history",
Long: "Search clipboard history with filters (requires server)",
Run: runClipSearch,
}
var (
clipSearchLimit int
clipSearchOffset int
clipSearchMimeType string
clipSearchImages bool
clipSearchText bool
)
var clipConfigCmd = &cobra.Command{
Use: "config",
Short: "Manage clipboard config",
Long: "Get or set clipboard configuration (requires server)",
}
var clipConfigGetCmd = &cobra.Command{
Use: "get",
Short: "Get clipboard config",
Run: runClipConfigGet,
}
var clipConfigSetCmd = &cobra.Command{
Use: "set",
Short: "Set clipboard config",
Long: `Set clipboard configuration options.
Examples:
dms cl config set --max-history 200
dms cl config set --auto-clear-days 7
dms cl config set --clear-at-startup`,
Run: runClipConfigSet,
}
var (
clipConfigMaxHistory int
clipConfigAutoClearDays int
clipConfigClearAtStartup bool
clipConfigNoClearStartup bool
clipConfigDisabled bool
clipConfigEnabled bool
)
var clipExportCmd = &cobra.Command{
Use: "export [file]",
Short: "Export clipboard history to JSON",
Long: "Export clipboard history to JSON file. If no file specified, writes to stdout.",
Run: runClipExport,
}
var clipImportCmd = &cobra.Command{
Use: "import <file>",
Short: "Import clipboard history from JSON",
Long: "Import clipboard history from JSON file exported by 'dms cl export'.",
Args: cobra.ExactArgs(1),
Run: runClipImport,
}
var clipMigrateCmd = &cobra.Command{
Use: "cliphist-migrate [db-path]",
Short: "Migrate from cliphist",
Long: "Migrate clipboard history from cliphist. Uses default cliphist path if not specified.",
Run: runClipMigrate,
}
var clipMigrateDelete bool
func init() {
clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking")
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "c", false, "Copy entry to clipboard")
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
clipSearchCmd.Flags().StringVarP(&clipSearchMimeType, "mime", "m", "", "Filter by MIME type")
clipSearchCmd.Flags().BoolVar(&clipSearchImages, "images", false, "Only images")
clipSearchCmd.Flags().BoolVar(&clipSearchText, "text", false, "Only text")
clipSearchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
clipConfigSetCmd.Flags().IntVar(&clipConfigMaxHistory, "max-history", 0, "Max history entries")
clipConfigSetCmd.Flags().IntVar(&clipConfigAutoClearDays, "auto-clear-days", -1, "Auto-clear entries older than N days (0 to disable)")
clipConfigSetCmd.Flags().BoolVar(&clipConfigClearAtStartup, "clear-at-startup", false, "Clear history on startup")
clipConfigSetCmd.Flags().BoolVar(&clipConfigNoClearStartup, "no-clear-at-startup", false, "Don't clear history on startup")
clipConfigSetCmd.Flags().BoolVar(&clipConfigDisabled, "disable", false, "Disable 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)")
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
}
func runClipCopy(cmd *cobra.Command, args []string) {
var data []byte
if len(args) > 0 {
data = []byte(args[0])
} else {
var err error
data, err = io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("read stdin: %v", err)
}
}
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err)
}
}
func runClipPaste(cmd *cobra.Command, args []string) {
data, _, err := clipboard.Paste()
if err != nil {
log.Fatalf("paste: %v", err)
}
os.Stdout.Write(data)
}
func runClipWatch(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
cancel()
}()
switch {
case len(args) > 0:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
runCommand(args, data)
}); err != nil && err != context.Canceled {
log.Fatalf("Watch error: %v", err)
}
case clipWatchStore:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
if err := clipboard.Store(data, mimeType); err != nil {
log.Errorf("store: %v", err)
}
}); err != nil && err != context.Canceled {
log.Fatalf("Watch error: %v", err)
}
case clipJSONOutput:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
out := map[string]any{
"data": string(data),
"mimeType": mimeType,
"timestamp": time.Now().Format(time.RFC3339),
"size": len(data),
}
j, _ := json.Marshal(out)
fmt.Println(string(j))
}); err != nil && err != context.Canceled {
log.Fatalf("Watch error: %v", err)
}
default:
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
os.Stdout.Write(data)
os.Stdout.WriteString("\n")
}); err != nil && err != context.Canceled {
log.Fatalf("Watch error: %v", err)
}
}
}
func runCommand(args []string, stdin []byte) {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if len(stdin) == 0 {
cmd.Run()
return
}
r, w, err := os.Pipe()
if err != nil {
cmd.Run()
return
}
cmd.Stdin = r
go func() {
w.Write(stdin)
w.Close()
}()
cmd.Run()
}
func runClipHistory(cmd *cobra.Command, args []string) {
req := models.Request{
ID: 1,
Method: "clipboard.getHistory",
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to get clipboard history: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
if resp.Result == nil {
if clipJSONOutput {
fmt.Println("[]")
} else {
fmt.Println("No clipboard history")
}
return
}
historyList, ok := (*resp.Result).([]any)
if !ok {
log.Fatal("Invalid response format")
}
if clipJSONOutput {
out, _ := json.MarshalIndent(historyList, "", " ")
fmt.Println(string(out))
return
}
if len(historyList) == 0 {
fmt.Println("No clipboard history")
return
}
fmt.Println("Clipboard History:")
fmt.Println()
for _, item := range historyList {
entry, ok := item.(map[string]any)
if !ok {
continue
}
id := uint64(entry["id"].(float64))
preview := entry["preview"].(string)
timestamp := entry["timestamp"].(string)
isImage := entry["isImage"].(bool)
typeStr := "text"
if isImage {
typeStr = "image"
}
fmt.Printf("ID: %d | %s | %s\n", id, typeStr, timestamp)
fmt.Printf(" %s\n", preview)
fmt.Println()
}
}
func runClipGet(cmd *cobra.Command, args []string) {
id, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
log.Fatalf("Invalid ID: %v", err)
}
if clipGetCopy {
req := models.Request{
ID: 1,
Method: "clipboard.copyEntry",
Params: map[string]any{
"id": id,
},
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to copy clipboard entry: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
fmt.Printf("Copied entry %d to clipboard\n", id)
return
}
req := models.Request{
ID: 1,
Method: "clipboard.getEntry",
Params: map[string]any{
"id": id,
},
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to get clipboard entry: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
if resp.Result == nil {
log.Fatal("Entry not found")
}
entry, ok := (*resp.Result).(map[string]any)
if !ok {
log.Fatal("Invalid response format")
}
switch {
case clipJSONOutput:
output, _ := json.MarshalIndent(entry, "", " ")
fmt.Println(string(output))
default:
if data, ok := entry["data"].(string); ok {
fmt.Print(data)
} else {
output, _ := json.MarshalIndent(entry, "", " ")
fmt.Println(string(output))
}
}
}
func runClipDelete(cmd *cobra.Command, args []string) {
id, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
log.Fatalf("Invalid ID: %v", err)
}
req := models.Request{
ID: 1,
Method: "clipboard.deleteEntry",
Params: map[string]any{
"id": id,
},
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to delete clipboard entry: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
fmt.Printf("Deleted entry %d\n", id)
}
func runClipClear(cmd *cobra.Command, args []string) {
req := models.Request{
ID: 1,
Method: "clipboard.clearHistory",
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to clear clipboard history: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
fmt.Println("Clipboard history cleared")
}
func runClipSearch(cmd *cobra.Command, args []string) {
params := map[string]any{
"limit": clipSearchLimit,
"offset": clipSearchOffset,
}
if len(args) > 0 {
params["query"] = args[0]
}
if clipSearchMimeType != "" {
params["mimeType"] = clipSearchMimeType
}
if clipSearchImages {
params["isImage"] = true
} else if clipSearchText {
params["isImage"] = false
}
req := models.Request{
ID: 1,
Method: "clipboard.search",
Params: params,
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to search clipboard: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
if resp.Result == nil {
log.Fatal("No results")
}
result, ok := (*resp.Result).(map[string]any)
if !ok {
log.Fatal("Invalid response format")
}
if clipJSONOutput {
out, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(out))
return
}
entries, _ := result["entries"].([]any)
total := int(result["total"].(float64))
hasMore := result["hasMore"].(bool)
if len(entries) == 0 {
fmt.Println("No results found")
return
}
fmt.Printf("Results: %d of %d\n\n", len(entries), total)
for _, item := range entries {
entry, ok := item.(map[string]any)
if !ok {
continue
}
id := uint64(entry["id"].(float64))
preview := entry["preview"].(string)
timestamp := entry["timestamp"].(string)
isImage := entry["isImage"].(bool)
typeStr := "text"
if isImage {
typeStr = "image"
}
fmt.Printf("ID: %d | %s | %s\n", id, typeStr, timestamp)
fmt.Printf(" %s\n\n", preview)
}
if hasMore {
fmt.Printf("Use --offset %d to see more results\n", clipSearchOffset+clipSearchLimit)
}
}
func runClipConfigGet(cmd *cobra.Command, args []string) {
req := models.Request{
ID: 1,
Method: "clipboard.getConfig",
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to get config: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
if resp.Result == nil {
log.Fatal("No config returned")
}
cfg, ok := (*resp.Result).(map[string]any)
if !ok {
log.Fatal("Invalid response format")
}
output, _ := json.MarshalIndent(cfg, "", " ")
fmt.Println(string(output))
}
func runClipConfigSet(cmd *cobra.Command, args []string) {
params := map[string]any{}
if cmd.Flags().Changed("max-history") {
params["maxHistory"] = clipConfigMaxHistory
}
if cmd.Flags().Changed("auto-clear-days") {
params["autoClearDays"] = clipConfigAutoClearDays
}
if clipConfigClearAtStartup {
params["clearAtStartup"] = true
}
if clipConfigNoClearStartup {
params["clearAtStartup"] = false
}
if clipConfigDisabled {
params["disabled"] = true
}
if clipConfigEnabled {
params["disabled"] = false
}
if len(params) == 0 {
fmt.Println("No config options specified")
return
}
req := models.Request{
ID: 1,
Method: "clipboard.setConfig",
Params: params,
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to set config: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
fmt.Println("Config updated")
}
func runClipExport(cmd *cobra.Command, args []string) {
req := models.Request{
ID: 1,
Method: "clipboard.getHistory",
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to get clipboard history: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
if resp.Result == nil {
log.Fatal("No clipboard history")
}
out, err := json.MarshalIndent(resp.Result, "", " ")
if err != nil {
log.Fatalf("Failed to marshal: %v", err)
}
if len(args) == 0 {
fmt.Println(string(out))
return
}
if err := os.WriteFile(args[0], out, 0644); err != nil {
log.Fatalf("Failed to write file: %v", err)
}
fmt.Printf("Exported to %s\n", args[0])
}
func runClipImport(cmd *cobra.Command, args []string) {
data, err := os.ReadFile(args[0])
if err != nil {
log.Fatalf("Failed to read file: %v", err)
}
var entries []map[string]any
if err := json.Unmarshal(data, &entries); err != nil {
log.Fatalf("Failed to parse JSON: %v", err)
}
var imported int
for _, entry := range entries {
dataStr, ok := entry["data"].(string)
if !ok {
continue
}
mimeType, _ := entry["mimeType"].(string)
if mimeType == "" {
mimeType = "text/plain"
}
var entryData []byte
if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil {
entryData = decoded
} else {
entryData = []byte(dataStr)
}
if err := clipboard.Store(entryData, mimeType); err != nil {
log.Errorf("Failed to store entry: %v", err)
continue
}
imported++
}
fmt.Printf("Imported %d entries\n", imported)
}
func runClipMigrate(cmd *cobra.Command, args []string) {
dbPath := getCliphistPath()
if len(args) > 0 {
dbPath = args[0]
}
if _, err := os.Stat(dbPath); err != nil {
log.Fatalf("Cliphist db not found: %s", dbPath)
}
db, err := bolt.Open(dbPath, 0644, &bolt.Options{
ReadOnly: true,
Timeout: 1 * time.Second,
})
if err != nil {
log.Fatalf("Failed to open cliphist db: %v", err)
}
defer db.Close()
var migrated int
err = db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("b"))
if b == nil {
return fmt.Errorf("cliphist bucket not found")
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if len(v) == 0 {
continue
}
mimeType := detectMimeType(v)
if err := clipboard.Store(v, mimeType); err != nil {
log.Errorf("Failed to store entry %d: %v", btoi(k), err)
continue
}
migrated++
}
return nil
})
if err != nil {
log.Fatalf("Migration failed: %v", err)
}
fmt.Printf("Migrated %d entries from cliphist\n", migrated)
if !clipMigrateDelete {
return
}
db.Close()
if err := os.Remove(dbPath); err != nil {
log.Errorf("Failed to delete cliphist db: %v", err)
return
}
os.Remove(filepath.Dir(dbPath))
fmt.Println("Deleted cliphist db")
}
func getCliphistPath() string {
cacheDir, err := os.UserCacheDir()
if err != nil {
return filepath.Join(os.Getenv("HOME"), ".cache", "cliphist", "db")
}
return filepath.Join(cacheDir, "cliphist", "db")
}
func detectMimeType(data []byte) string {
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil {
return "image/png"
}
return "text/plain"
}
func btoi(v []byte) uint64 {
return binary.BigEndian.Uint64(v)
}

View File

@@ -0,0 +1,127 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
"github.com/spf13/cobra"
)
var (
colorOutputFmt string
colorAutocopy bool
colorNotify bool
colorLowercase bool
)
var colorCmd = &cobra.Command{
Use: "color",
Short: "Color utilities",
Long: "Color utilities including picking colors from the screen",
}
var colorPickCmd = &cobra.Command{
Use: "pick",
Short: "Pick a color from the screen",
Long: `Pick a color from anywhere on your screen using an interactive color picker.
Click on any pixel to capture its color, or press Escape to cancel.
Output format flags (mutually exclusive, default: --hex):
--hex - Hexadecimal (#RRGGBB)
--rgb - RGB values (R G B)
--hsl - HSL values (H S% L%)
--hsv - HSV values (H S% V%)
--cmyk - CMYK values (C% M% Y% K%)
--json - JSON with all formats
Examples:
dms color pick # Pick color, output as hex
dms color pick --rgb # Output as RGB
dms color pick --json # Output all formats as JSON
dms color pick --hex -l # Output hex in lowercase
dms color pick -a # Auto-copy result to clipboard`,
Run: runColorPick,
}
func init() {
colorPickCmd.Flags().Bool("hex", false, "Output as hexadecimal (#RRGGBB)")
colorPickCmd.Flags().Bool("rgb", false, "Output as RGB (R G B)")
colorPickCmd.Flags().Bool("hsl", false, "Output as HSL (H S% L%)")
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
colorCmd.AddCommand(colorPickCmd)
}
func runColorPick(cmd *cobra.Command, args []string) {
format := colorpicker.FormatHex // default
jsonOutput, _ := cmd.Flags().GetBool("json")
if rgb, _ := cmd.Flags().GetBool("rgb"); rgb {
format = colorpicker.FormatRGB
} else if hsl, _ := cmd.Flags().GetBool("hsl"); hsl {
format = colorpicker.FormatHSL
} else if hsv, _ := cmd.Flags().GetBool("hsv"); hsv {
format = colorpicker.FormatHSV
} else if cmyk, _ := cmd.Flags().GetBool("cmyk"); cmyk {
format = colorpicker.FormatCMYK
}
config := colorpicker.Config{
Format: format,
CustomFormat: colorOutputFmt,
Lowercase: colorLowercase,
Autocopy: colorAutocopy,
Notify: colorNotify,
}
picker := colorpicker.New(config)
color, err := picker.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if color == nil {
os.Exit(0)
}
var output string
if jsonOutput {
jsonStr, err := color.ToJSON()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
output = jsonStr
} else {
output = color.Format(config.Format, config.Lowercase, config.CustomFormat)
}
if colorAutocopy {
copyToClipboard(output)
}
if jsonOutput {
fmt.Println(output)
} else if color.IsDark() {
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
} else {
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
}
}
func copyToClipboard(text string) {
if err := clipboard.CopyText(text); err != nil {
fmt.Fprintln(os.Stderr, "clipboard copy failed:", err)
}
}

View File

@@ -2,6 +2,8 @@ package main
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -66,6 +68,10 @@ var ipcCmd = &cobra.Command{
Short: "Send IPC commands to running DMS shell",
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
PreRunE: findConfig,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
runShellIPCCommand(args)
},
@@ -115,6 +121,12 @@ var pluginsInstallCmd = &cobra.Command{
Short: "Install a plugin by ID",
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getAvailablePluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := installPluginCLI(args[0]); err != nil {
log.Fatalf("Error installing plugin: %v", err)
@@ -127,6 +139,12 @@ var pluginsUninstallCmd = &cobra.Command{
Short: "Uninstall a plugin by ID",
Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := uninstallPluginCLI(args[0]); err != nil {
log.Fatalf("Error uninstalling plugin: %v", err)
@@ -134,12 +152,78 @@ var pluginsUninstallCmd = &cobra.Command{
},
}
var pluginsUpdateCmd = &cobra.Command{
Use: "update <plugin-id>",
Short: "Update a plugin by ID",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err)
}
},
}
func runVersion(cmd *cobra.Command, args []string) {
printASCII()
fmt.Printf("%s\n", Version)
fmt.Printf("%s\n", formatVersion(Version))
}
// Git builds: dms (git) v0.6.2-XXXX
// Stable releases: dms v0.6.2
func formatVersion(version string) string {
// Arch/Debian/Ubuntu/OpenSUSE git format: 0.6.2+git2264.c5c5ce84
re := regexp.MustCompile(`^([\d.]+)\+git(\d+)\.`)
if matches := re.FindStringSubmatch(version); matches != nil {
return fmt.Sprintf("dms (git) v%s-%s", matches[1], matches[2])
}
// Fedora COPR git format: 0.0.git.2267.d430cae9
re = regexp.MustCompile(`^[\d.]+\.git\.(\d+)\.`)
if matches := re.FindStringSubmatch(version); matches != nil {
baseVersion := getBaseVersion()
return fmt.Sprintf("dms (git) v%s-%s", baseVersion, matches[1])
}
// Stable release format: 0.6.2
re = regexp.MustCompile(`^([\d.]+)$`)
if matches := re.FindStringSubmatch(version); matches != nil {
return fmt.Sprintf("dms v%s", matches[1])
}
return fmt.Sprintf("dms %s", version)
}
func getBaseVersion() string {
paths := []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
}
for _, path := range paths {
if content, err := os.ReadFile(path); err == nil {
ver := strings.TrimSpace(string(content))
ver = strings.TrimPrefix(ver, "v")
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
if matches := re.FindStringSubmatch(ver); matches != nil {
return matches[1]
}
}
}
}
// Fallback
return "1.0.2"
}
func startDebugServer() error {
server.CLIVersion = Version
return server.Start(true)
}
@@ -298,6 +382,38 @@ func installPluginCLI(idOrName string) error {
return nil
}
func getAvailablePluginIDs() []string {
registry, err := plugins.NewRegistry()
if err != nil {
return nil
}
pluginList, err := registry.List()
if err != nil {
return nil
}
var ids []string
for _, p := range pluginList {
ids = append(ids, p.ID)
}
return ids
}
func getInstalledPluginIDs() []string {
manager, err := plugins.NewManager()
if err != nil {
return nil
}
installed, err := manager.ListInstalled()
if err != nil {
return nil
}
return installed
}
func uninstallPluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
@@ -309,53 +425,73 @@ func uninstallPluginCLI(idOrName string) error {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, err := registry.List()
if err != nil {
return fmt.Errorf("failed to list plugins: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
// First, try to find by ID (preferred method)
var plugin *plugins.Plugin
for _, p := range pluginList {
if p.ID == idOrName {
plugin = &p
break
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
}
// Fallback to name for backward compatibility
if plugin == nil {
for _, p := range pluginList {
if p.Name == idOrName {
plugin = &p
break
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Uninstall(*plugin); err != nil {
return fmt.Errorf("failed to uninstall plugin: %w", err)
}
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
return nil
}
if plugin == nil {
return fmt.Errorf("plugin not found: %s", idOrName)
fmt.Printf("Uninstalling plugin: %s\n", idOrName)
if err := manager.UninstallByIDOrName(idOrName); err != nil {
return err
}
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Uninstall(*plugin); err != nil {
return fmt.Errorf("failed to uninstall plugin: %w", err)
}
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
fmt.Printf("Plugin uninstalled successfully: %s\n", idOrName)
return nil
}
func updatePluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Update(*plugin); err != nil {
return fmt.Errorf("failed to update plugin: %w", err)
}
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
return nil
}
fmt.Printf("Updating plugin: %s\n", idOrName)
if err := manager.UpdateByIDOrName(idOrName); err != nil {
return err
}
fmt.Printf("Plugin updated successfully: %s\n", idOrName)
return nil
}
// getCommonCommands returns the commands available in all builds
func getCommonCommands() []*cobra.Command {
return []*cobra.Command{
versionCmd,
@@ -372,5 +508,12 @@ func getCommonCommands() []*cobra.Command {
keybindsCmd,
greeterCmd,
setupCmd,
colorCmd,
screenshotCmd,
notifyActionCmd,
matugenCmd,
clipboardCmd,
doctorCmd,
configCmd,
}
}

View File

@@ -0,0 +1,318 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Configuration utilities",
}
var resolveIncludeCmd = &cobra.Command{
Use: "resolve-include <compositor> <filename>",
Short: "Check if a file is included in compositor config",
Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.",
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
case 1:
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
Run: runResolveInclude,
}
func init() {
configCmd.AddCommand(resolveIncludeCmd)
}
type IncludeResult struct {
Exists bool `json:"exists"`
Included bool `json:"included"`
}
func runResolveInclude(cmd *cobra.Command, args []string) {
compositor := strings.ToLower(args[0])
filename := args[1]
var result IncludeResult
var err error
switch compositor {
case "hyprland":
result, err = checkHyprlandInclude(filename)
case "niri":
result, err = checkNiriInclude(filename)
case "mangowc", "dwl", "mango":
result, err = checkMangoWCInclude(filename)
default:
log.Fatalf("Unknown compositor: %s", compositor)
}
if err != nil {
log.Fatalf("Error checking include: %v", err)
}
output, _ := json.Marshal(result)
fmt.Fprintln(os.Stdout, string(output))
}
func checkHyprlandInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
if err != nil {
return IncludeResult{}, err
}
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
if _, err := os.Stat(targetPath); err == nil {
result.Exists = true
}
mainConfig := filepath.Join(configDir, "hyprland.conf")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
return result, nil
}
processed := make(map[string]bool)
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
return result, nil
}
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
}
if processed[absPath] {
return false
}
processed[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return false
}
baseDir := filepath.Dir(absPath)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
}
if !strings.HasPrefix(trimmed, "source") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) < 2 {
continue
}
sourcePath := strings.TrimSpace(parts[1])
if matchesTarget(sourcePath, target) {
return true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil {
continue
}
if hyprlandFindInclude(expanded, target, processed) {
return true
}
}
return false
}
func checkNiriInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/niri")
if err != nil {
return IncludeResult{}, err
}
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
if _, err := os.Stat(targetPath); err == nil {
result.Exists = true
}
mainConfig := filepath.Join(configDir, "config.kdl")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
return result, nil
}
processed := make(map[string]bool)
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed)
return result, nil
}
func niriFindInclude(filePath, target string, processed map[string]bool) bool {
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
}
if processed[absPath] {
return false
}
processed[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return false
}
baseDir := filepath.Dir(absPath)
content := string(data)
for _, line := range strings.Split(content, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
continue
}
if !strings.HasPrefix(trimmed, "include") {
continue
}
startQuote := strings.Index(trimmed, "\"")
if startQuote == -1 {
continue
}
endQuote := strings.LastIndex(trimmed, "\"")
if endQuote <= startQuote {
continue
}
includePath := trimmed[startQuote+1 : endQuote]
if matchesTarget(includePath, target) {
return true
}
fullPath := includePath
if !filepath.IsAbs(includePath) {
fullPath = filepath.Join(baseDir, includePath)
}
if niriFindInclude(fullPath, target, processed) {
return true
}
}
return false
}
func checkMangoWCInclude(filename string) (IncludeResult, error) {
configDir, err := utils.ExpandPath("$HOME/.config/mango")
if err != nil {
return IncludeResult{}, err
}
targetPath := filepath.Join(configDir, "dms", filename)
result := IncludeResult{}
if _, err := os.Stat(targetPath); err == nil {
result.Exists = true
}
mainConfig := filepath.Join(configDir, "config.conf")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
mainConfig = filepath.Join(configDir, "mango.conf")
}
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
return result, nil
}
processed := make(map[string]bool)
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed)
return result, nil
}
func mangowcFindInclude(filePath, target string, processed map[string]bool) bool {
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
}
if processed[absPath] {
return false
}
processed[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return false
}
baseDir := filepath.Dir(absPath)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
continue
}
if !strings.HasPrefix(trimmed, "source") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) < 2 {
continue
}
sourcePath := strings.TrimSpace(parts[1])
if matchesTarget(sourcePath, target) {
return true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil {
continue
}
if mangowcFindInclude(expanded, target, processed) {
return true
}
}
return false
}
func matchesTarget(path, target string) bool {
path = strings.TrimPrefix(path, "./")
target = strings.TrimPrefix(target, "./")
return path == target || strings.HasSuffix(path, "/"+target)
}

View File

@@ -2,7 +2,6 @@ package main
import (
"fmt"
"os"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
@@ -11,52 +10,94 @@ import (
)
var dank16Cmd = &cobra.Command{
Use: "dank16 <hex_color>",
Use: "dank16 [hex_color]",
Short: "Generate Base16 color palettes",
Long: "Generate Base16 color palettes from a color with support for various output formats",
Args: cobra.ExactArgs(1),
Args: cobra.MaximumNArgs(1),
Run: runDank16,
}
func init() {
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant")
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant (sets default to light)")
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
dank16Cmd.Flags().Bool("neovim", false, "Output in Neovim plugin format")
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
dank16Cmd.Flags().String("vscode-enrich", "", "Enrich existing VSCode theme file with terminal colors")
dank16Cmd.Flags().String("background", "", "Custom background color")
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
dank16Cmd.Flags().Bool("variants", false, "Output all variants (dark/light/default) in JSON")
dank16Cmd.Flags().String("primary-dark", "", "Primary color for dark mode (use with --variants)")
dank16Cmd.Flags().String("primary-light", "", "Primary color for light mode (use with --variants)")
_ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp
})
}
func runDank16(cmd *cobra.Command, args []string) {
primaryColor := args[0]
if !strings.HasPrefix(primaryColor, "#") {
primaryColor = "#" + primaryColor
}
isLight, _ := cmd.Flags().GetBool("light")
isJson, _ := cmd.Flags().GetBool("json")
isKitty, _ := cmd.Flags().GetBool("kitty")
isFoot, _ := cmd.Flags().GetBool("foot")
isNeovim, _ := cmd.Flags().GetBool("neovim")
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
isGhostty, _ := cmd.Flags().GetBool("ghostty")
isWezterm, _ := cmd.Flags().GetBool("wezterm")
vscodeEnrich, _ := cmd.Flags().GetString("vscode-enrich")
background, _ := cmd.Flags().GetString("background")
contrastAlgo, _ := cmd.Flags().GetString("contrast")
useVariants, _ := cmd.Flags().GetBool("variants")
primaryDark, _ := cmd.Flags().GetString("primary-dark")
primaryLight, _ := cmd.Flags().GetString("primary-light")
if background != "" && !strings.HasPrefix(background, "#") {
background = "#" + background
}
if primaryDark != "" && !strings.HasPrefix(primaryDark, "#") {
primaryDark = "#" + primaryDark
}
if primaryLight != "" && !strings.HasPrefix(primaryLight, "#") {
primaryLight = "#" + primaryLight
}
contrastAlgo = strings.ToLower(contrastAlgo)
if contrastAlgo != "dps" && contrastAlgo != "wcag" {
log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo)
}
if useVariants {
if primaryDark == "" || primaryLight == "" {
if len(args) == 0 {
log.Fatalf("--variants requires either a positional color argument or both --primary-dark and --primary-light")
}
primaryColor := args[0]
if !strings.HasPrefix(primaryColor, "#") {
primaryColor = "#" + primaryColor
}
primaryDark = primaryColor
primaryLight = primaryColor
}
variantOpts := dank16.VariantOptions{
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
Background: background,
UseDPS: contrastAlgo == "dps",
IsLightMode: isLight,
}
variantColors := dank16.GenerateVariantPalette(variantOpts)
fmt.Print(dank16.GenerateVariantJSON(variantColors))
return
}
if len(args) == 0 {
log.Fatalf("A color argument is required (or use --variants with --primary-dark and --primary-light)")
}
primaryColor := args[0]
if !strings.HasPrefix(primaryColor, "#") {
primaryColor = "#" + primaryColor
}
opts := dank16.PaletteOptions{
IsLight: isLight,
Background: background,
@@ -65,18 +106,7 @@ func runDank16(cmd *cobra.Command, args []string) {
colors := dank16.GeneratePalette(primaryColor, opts)
if vscodeEnrich != "" {
data, err := os.ReadFile(vscodeEnrich)
if err != nil {
log.Fatalf("Error reading file: %v", err)
}
enriched, err := dank16.EnrichVSCodeTheme(data, colors)
if err != nil {
log.Fatalf("Error enriching theme: %v", err)
}
fmt.Println(string(enriched))
} else if isJson {
if isJson {
fmt.Print(dank16.GenerateJSON(colors))
} else if isKitty {
fmt.Print(dank16.GenerateKittyTheme(colors))
@@ -88,6 +118,8 @@ func runDank16(cmd *cobra.Command, args []string) {
fmt.Print(dank16.GenerateGhosttyTheme(colors))
} else if isWezterm {
fmt.Print(dank16.GenerateWeztermTheme(colors))
} else if isNeovim {
fmt.Print(dank16.GenerateNeovimTheme(colors))
} else {
fmt.Print(dank16.GenerateGhosttyTheme(colors))
}

View File

@@ -0,0 +1,927 @@
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)
type status string
const (
statusOK status = "ok"
statusWarn status = "warn"
statusError status = "error"
statusInfo status = "info"
)
func (s status) IconStyle(styles tui.Styles) (string, lipgloss.Style) {
switch s {
case statusOK:
return "●", styles.Success
case statusWarn:
return "●", styles.Warning
case statusError:
return "●", styles.Error
default:
return "○", styles.Subtle
}
}
type DoctorStatus struct {
Errors []checkResult
Warnings []checkResult
OK []checkResult
Info []checkResult
}
func (ds *DoctorStatus) Add(r checkResult) {
switch r.status {
case statusError:
ds.Errors = append(ds.Errors, r)
case statusWarn:
ds.Warnings = append(ds.Warnings, r)
case statusOK:
ds.OK = append(ds.OK, r)
case statusInfo:
ds.Info = append(ds.Info, r)
}
}
func (ds *DoctorStatus) HasIssues() bool {
return len(ds.Errors) > 0 || len(ds.Warnings) > 0
}
func (ds *DoctorStatus) ErrorCount() int {
return len(ds.Errors)
}
func (ds *DoctorStatus) WarningCount() int {
return len(ds.Warnings)
}
func (ds *DoctorStatus) OKCount() int {
return len(ds.OK)
}
var (
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
)
var doctorCmd = &cobra.Command{
Use: "doctor",
Short: "Diagnose DMS installation and dependencies",
Long: "Check system health, verify dependencies, and diagnose configuration issues for DMS",
Run: runDoctor,
}
var (
doctorVerbose bool
doctorJSON bool
)
func init() {
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")
}
type category int
const (
catSystem category = iota
catVersions
catInstallation
catCompositor
catQuickshellFeatures
catOptionalFeatures
catConfigFiles
catServices
catEnvironment
)
func (c category) String() string {
switch c {
case catSystem:
return "System"
case catVersions:
return "Versions"
case catInstallation:
return "Installation"
case catCompositor:
return "Compositor"
case catQuickshellFeatures:
return "Quickshell Features"
case catOptionalFeatures:
return "Optional Features"
case catConfigFiles:
return "Config Files"
case catServices:
return "Services"
case catEnvironment:
return "Environment"
default:
return "Unknown"
}
}
const (
checkNameMaxLength = 21
doctorDocsURL = "https://danklinux.com/docs/dankmaterialshell/cli-doctor"
)
type checkResult struct {
category category
name string
status status
message string
details string
url string
}
type checkResultJSON struct {
Category string `json:"category"`
Name string `json:"name"`
Status string `json:"status"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
URL string `json:"url,omitempty"`
}
type doctorOutputJSON struct {
Summary struct {
Errors int `json:"errors"`
Warnings int `json:"warnings"`
OK int `json:"ok"`
Info int `json:"info"`
} `json:"summary"`
Results []checkResultJSON `json:"results"`
}
func (r checkResult) toJSON() checkResultJSON {
return checkResultJSON{
Category: r.category.String(),
Name: r.name,
Status: string(r.status),
Message: r.message,
Details: r.details,
URL: r.url,
}
}
func runDoctor(cmd *cobra.Command, args []string) {
if !doctorJSON {
printDoctorHeader()
}
qsFeatures, qsMissingFeatures := checkQuickshellFeatures()
results := slices.Concat(
checkSystemInfo(),
checkVersions(qsMissingFeatures),
checkDMSInstallation(),
checkWindowManagers(),
qsFeatures,
checkOptionalDependencies(),
checkConfigurationFiles(),
checkSystemdServices(),
checkEnvironmentVars(),
)
if doctorJSON {
printResultsJSON(results)
} else {
printResults(results)
printSummary(results, qsMissingFeatures)
}
}
func printDoctorHeader() {
theme := tui.TerminalTheme()
styles := tui.NewStyles(theme)
fmt.Println(getThemedASCII())
fmt.Println(styles.Title.Render("System Health Check"))
fmt.Println(styles.Subtle.Render("──────────────────────────────────────"))
fmt.Println()
}
func checkSystemInfo() []checkResult {
var results []checkResult
osInfo, err := distros.GetOSInfo()
if err != nil {
status, message, details := statusWarn, fmt.Sprintf("Unknown (%v)", err), ""
if strings.Contains(err.Error(), "Unsupported distribution") {
osRelease := readOSRelease()
switch {
case osRelease["ID"] == "nixos":
status = statusOK
message = osRelease["PRETTY_NAME"]
if message == "" {
message = fmt.Sprintf("NixOS %s", osRelease["VERSION_ID"])
}
details = "Supported for runtime (install via NixOS module or Flake)"
case osRelease["PRETTY_NAME"] != "":
message = fmt.Sprintf("%s (not supported by dms setup)", osRelease["PRETTY_NAME"])
details = "DMS may work but automatic installation is not available"
}
}
results = append(results, checkResult{catSystem, "Operating System", status, message, details, doctorDocsURL + "#operating-system"})
} else {
status := statusOK
message := osInfo.PrettyName
if message == "" {
message = fmt.Sprintf("%s %s", osInfo.Distribution.ID, osInfo.VersionID)
}
if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) {
status = statusWarn
message += " (version may not be fully supported)"
}
results = append(results, checkResult{
catSystem, "Operating System", status, message,
fmt.Sprintf("ID: %s, Version: %s, Arch: %s", osInfo.Distribution.ID, osInfo.VersionID, osInfo.Architecture),
doctorDocsURL + "#operating-system",
})
}
arch := runtime.GOARCH
archStatus := statusOK
if arch != "amd64" && arch != "arm64" {
archStatus = statusError
}
results = append(results, checkResult{catSystem, "Architecture", archStatus, arch, "", doctorDocsURL + "#architecture"})
waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
xdgSessionType := os.Getenv("XDG_SESSION_TYPE")
switch {
case waylandDisplay != "" || xdgSessionType == "wayland":
results = append(results, checkResult{
catSystem, "Display Server", statusOK, "Wayland",
fmt.Sprintf("WAYLAND_DISPLAY=%s", waylandDisplay),
doctorDocsURL + "#display-server",
})
case xdgSessionType == "x11":
results = append(results, checkResult{catSystem, "Display Server", statusError, "X11 (DMS requires Wayland)", "", doctorDocsURL + "#display-server"})
default:
results = append(results, checkResult{
catSystem, "Display Server", statusWarn, "Unknown (ensure you're running Wayland)",
fmt.Sprintf("XDG_SESSION_TYPE=%s", xdgSessionType),
doctorDocsURL + "#display-server",
})
}
return results
}
func checkEnvironmentVars() []checkResult {
var results []checkResult
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
results = append(results, checkEnvVar("QS_ICON_THEME")...)
return results
}
func checkEnvVar(name string) []checkResult {
value := os.Getenv(name)
if value != "" {
return []checkResult{{catEnvironment, name, statusInfo, value, "", doctorDocsURL + "#environment-variables"}}
}
if doctorVerbose {
return []checkResult{{catEnvironment, name, statusInfo, "Not set", "", doctorDocsURL + "#environment-variables"}}
}
return nil
}
func readOSRelease() map[string]string {
result := make(map[string]string)
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return result
}
for line := range strings.SplitSeq(string(data), "\n") {
if parts := strings.SplitN(line, "=", 2); len(parts) == 2 {
result[parts[0]] = strings.Trim(parts[1], "\"")
}
}
return result
}
func checkVersions(qsMissingFeatures bool) []checkResult {
dmsCliPath, _ := os.Executable()
dmsCliDetails := ""
if doctorVerbose {
dmsCliDetails = dmsCliPath
}
results := []checkResult{
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
}
qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures)
qsDetails := ""
if doctorVerbose && qsPath != "" {
qsDetails = qsPath
}
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
dmsVersion, dmsPath := getDMSShellVersion()
if dmsVersion != "" {
results = append(results, checkResult{catVersions, "DMS Shell", statusOK, dmsVersion, dmsPath, doctorDocsURL + "#dms-shell"})
} else {
results = append(results, checkResult{catVersions, "DMS Shell", statusError, "Not installed or not detected", "Run 'dms setup' to install", doctorDocsURL + "#dms-shell"})
}
return results
}
func getDMSShellVersion() (version, path string) {
if err := findConfig(nil, nil); err == nil && configPath != "" {
versionFile := filepath.Join(configPath, "VERSION")
if data, err := os.ReadFile(versionFile); err == nil {
return strings.TrimSpace(string(data)), configPath
}
return "installed", configPath
}
if dmsPath, err := config.LocateDMSConfig(); err == nil {
versionFile := filepath.Join(dmsPath, "VERSION")
if data, err := os.ReadFile(versionFile); err == nil {
return strings.TrimSpace(string(data)), dmsPath
}
return "installed", dmsPath
}
return "", ""
}
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
if !utils.CommandExists("qs") {
return "Not installed", statusError, ""
}
qsPath, _ := exec.LookPath("qs")
output, err := exec.Command("qs", "--version").Output()
if err != nil {
return "Installed (version check failed)", statusWarn, qsPath
}
fullVersion := strings.TrimSpace(string(output))
if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 {
if version.CompareVersions(matches[1], "0.2.0") < 0 {
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath
}
if missingFeatures {
return fullVersion, statusWarn, qsPath
}
return fullVersion, statusOK, qsPath
}
return fullVersion, statusWarn, qsPath
}
func checkDMSInstallation() []checkResult {
var results []checkResult
dmsPath := ""
if err := findConfig(nil, nil); err == nil && configPath != "" {
dmsPath = configPath
} else if path, err := config.LocateDMSConfig(); err == nil {
dmsPath = path
}
if dmsPath == "" {
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
}
results = append(results, checkResult{catInstallation, "DMS Configuration", statusOK, "Found", dmsPath, doctorDocsURL + "#dms-configuration"})
shellQml := filepath.Join(dmsPath, "shell.qml")
if _, err := os.Stat(shellQml); err != nil {
results = append(results, checkResult{catInstallation, "shell.qml", statusError, "Missing", shellQml, doctorDocsURL + "#dms-configuration"})
} else {
results = append(results, checkResult{catInstallation, "shell.qml", statusOK, "Present", shellQml, doctorDocsURL + "#dms-configuration"})
}
if doctorVerbose {
installType := "Unknown"
switch {
case strings.Contains(dmsPath, "/nix/store"):
installType = "Nix store"
case strings.Contains(dmsPath, ".local/share") || strings.Contains(dmsPath, "/usr/share"):
installType = "System package"
case strings.Contains(dmsPath, ".config"):
installType = "User config"
}
results = append(results, checkResult{catInstallation, "Install Type", statusInfo, installType, dmsPath, doctorDocsURL + "#dms-configuration"})
}
return results
}
func checkWindowManagers() []checkResult {
compositors := []struct {
name, versionCmd, versionArg string
versionRegex *regexp.Regexp
commands []string
}{
{"Hyprland", "hyprctl", "version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
{"River", "river", "-version", riverVersionRegex, []string{"river"}},
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
}
var results []checkResult
foundAny := false
for _, c := range compositors {
if !slices.ContainsFunc(c.commands, utils.CommandExists) {
continue
}
foundAny = true
var compositorPath string
for _, cmd := range c.commands {
if path, err := exec.LookPath(cmd); err == nil {
compositorPath = path
break
}
}
details := ""
if doctorVerbose && compositorPath != "" {
details = compositorPath
}
results = append(results, checkResult{
catCompositor, c.name, statusOK,
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
doctorDocsURL + "#compositor-checks",
})
}
if !foundAny {
results = append(results, checkResult{
catCompositor, "Compositor", statusError,
"No supported Wayland compositor found",
"Install Hyprland, niri, Sway, River, or Wayfire",
doctorDocsURL + "#compositor-checks",
})
}
if wm := detectRunningWM(); wm != "" {
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
}
return results
}
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
output, err := exec.Command(cmd, arg).Output()
if err != nil {
return "installed"
}
outStr := string(output)
if matches := regex.FindStringSubmatch(outStr); len(matches) > 1 {
ver := matches[1]
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
return ver + " (git)"
}
return ver
}
return strings.TrimSpace(outStr)
}
func detectRunningWM() string {
switch {
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
return "Hyprland"
case os.Getenv("NIRI_SOCKET") != "":
return "niri"
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
return os.Getenv("XDG_CURRENT_DESKTOP")
}
return ""
}
func checkQuickshellFeatures() ([]checkResult, bool) {
if !utils.CommandExists("qs") {
return nil, false
}
tmpDir := os.TempDir()
testScript := filepath.Join(tmpDir, "qs-feature-test.qml")
defer os.Remove(testScript)
qmlContent := `
import QtQuick
import Quickshell
ShellRoot {
id: root
property bool polkitAvailable: false
property bool idleMonitorAvailable: false
property bool idleInhibitorAvailable: false
property bool shortcutInhibitorAvailable: false
Timer {
interval: 50
running: true
repeat: false
onTriggered: {
try {
var polkitTest = Qt.createQmlObject(
'import Quickshell.Services.Polkit; import QtQuick; Item {}',
root
)
root.polkitAvailable = true
polkitTest.destroy()
} catch (e) {}
try {
var testItem = Qt.createQmlObject(
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
'}',
root
)
root.idleMonitorAvailable = testItem.hasIdleMonitor
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
testItem.destroy()
} catch (e) {}
console.warn(root.polkitAvailable ? "FEATURE:Polkit:OK" : "FEATURE:Polkit:UNAVAILABLE")
console.warn(root.idleMonitorAvailable ? "FEATURE:IdleMonitor:OK" : "FEATURE:IdleMonitor:UNAVAILABLE")
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
}
}
}
`
if err := os.WriteFile(testScript, []byte(qmlContent), 0644); err != nil {
return nil, false
}
cmd := exec.Command("qs", "-p", testScript)
cmd.Env = append(os.Environ(), "NO_COLOR=1")
output, _ := cmd.CombinedOutput()
outputStr := string(output)
features := []struct{ name, desc string }{
{"Polkit", "Authentication prompts"},
{"IdleMonitor", "Idle detection"},
{"IdleInhibitor", "Prevent idle/sleep"},
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
}
var results []checkResult
missingFeatures := false
for _, f := range features {
available := strings.Contains(outputStr, fmt.Sprintf("FEATURE:%s:OK", f.name))
status, message := statusOK, "Available"
if !available {
status, message = statusInfo, "Not available"
missingFeatures = true
}
results = append(results, checkResult{catQuickshellFeatures, f.name, status, message, f.desc, doctorDocsURL + "#quickshell-features"})
}
return results, missingFeatures
}
func checkI2CAvailability() checkResult {
ddc, err := brightness.NewDDCBackend()
if err != nil {
return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "Not available", "External monitor brightness control", doctorDocsURL + "#optional-features"}
}
defer ddc.Close()
devices, err := ddc.GetDevices()
if err != nil || len(devices) == 0 {
return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "No monitors detected", "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 detectNetworkBackend(stackResult *network.DetectResult) string {
switch stackResult.Backend {
case network.BackendNetworkManager:
return "NetworkManager"
case network.BackendIwd:
return "iwd"
case network.BackendNetworkd:
if stackResult.HasIwd {
return "iwd + systemd-networkd"
}
return "systemd-networkd"
case network.BackendConnMan:
return "ConnMan"
default:
return ""
}
}
func getOptionalDBusStatus(busName string) (status, string) {
if utils.IsDBusServiceAvailable(busName) {
return statusOK, "Available"
} else {
return statusWarn, "Not available"
}
}
func checkOptionalDependencies() []checkResult {
var results []checkResult
optionalFeaturesURL := doctorDocsURL + "#optional-features"
accountsStatus, accountsMsg := getOptionalDBusStatus("org.freedesktop.Accounts")
results = append(results, checkResult{catOptionalFeatures, "accountsservice", accountsStatus, accountsMsg, "User accounts", optionalFeaturesURL})
ppdStatus, ppdMsg := getOptionalDBusStatus("org.freedesktop.UPower.PowerProfiles")
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", ppdStatus, ppdMsg, "Power profile management", optionalFeaturesURL})
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
results = append(results, checkI2CAvailability())
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
} else {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
}
networkResult, err := network.DetectNetworkStack()
networkStatus, networkMessage, networkDetails := statusOK, "Not available", "Network management"
if err == nil && networkResult.Backend != network.BackendNone {
networkMessage = detectNetworkBackend(networkResult)
if doctorVerbose {
networkDetails = networkResult.ChosenReason
}
} else {
networkStatus = statusInfo
}
results = append(results, checkResult{catOptionalFeatures, "Network", networkStatus, networkMessage, networkDetails, optionalFeaturesURL})
deps := []struct {
name, cmd, desc string
important bool
}{
{"matugen", "matugen", "Dynamic theming", true},
{"dgop", "dgop", "System monitoring", true},
{"cava", "cava", "Audio visualizer", true},
{"khal", "khal", "Calendar events", false},
{"danksearch", "dsearch", "File search", false},
{"fprintd", "fprintd-list", "Fingerprint auth", false},
}
for _, d := range deps {
found := utils.CommandExists(d.cmd)
switch {
case found:
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, "Installed", d.desc, optionalFeaturesURL})
case d.important:
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, optionalFeaturesURL})
default:
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, optionalFeaturesURL})
}
}
return results
}
func checkConfigurationFiles() []checkResult {
configDir, _ := os.UserConfigDir()
cacheDir, _ := os.UserCacheDir()
dmsDir := "DankMaterialShell"
configFiles := []struct{ name, path string }{
{"settings.json", filepath.Join(configDir, dmsDir, "settings.json")},
{"clsettings.json", filepath.Join(configDir, dmsDir, "clsettings.json")},
{"plugin_settings.json", filepath.Join(configDir, dmsDir, "plugin_settings.json")},
{"session.json", filepath.Join(utils.XDGStateHome(), dmsDir, "session.json")},
{"dms-colors.json", filepath.Join(cacheDir, dmsDir, "dms-colors.json")},
}
var results []checkResult
for _, cf := range configFiles {
info, err := os.Stat(cf.path)
if err != nil {
results = append(results, checkResult{catConfigFiles, cf.name, statusInfo, "Not yet created", cf.path, doctorDocsURL + "#config-files"})
continue
}
status := statusOK
message := "Present"
if info.Mode().Perm()&0200 == 0 {
status = statusWarn
message += " (read-only)"
}
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
}
return results
}
func checkSystemdServices() []checkResult {
if !utils.CommandExists("systemctl") {
return nil
}
var results []checkResult
dmsState := getServiceState("dms", true)
if !dmsState.exists {
results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service", doctorDocsURL + "#services"})
} else {
status, message := statusOK, dmsState.enabled
if dmsState.active != "" {
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
}
switch {
case dmsState.enabled == "disabled":
status, message = statusWarn, "Disabled"
case dmsState.active == "failed" || dmsState.active == "inactive":
status = statusError
}
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
}
greetdState := getServiceState("greetd", false)
switch {
case greetdState.exists:
status := statusOK
if greetdState.enabled == "disabled" {
status = statusInfo
}
results = append(results, checkResult{catServices, "greetd", status, greetdState.enabled, "", doctorDocsURL + "#services"})
case doctorVerbose:
results = append(results, checkResult{catServices, "greetd", statusInfo, "Not installed", "Optional greeter service", doctorDocsURL + "#services"})
}
return results
}
type serviceState struct {
exists bool
enabled string
active string
}
func getServiceState(name string, userService bool) serviceState {
args := []string{"is-enabled", name}
if userService {
args = []string{"--user", "is-enabled", name}
}
output, _ := exec.Command("systemctl", args...).Output()
enabled := strings.TrimSpace(string(output))
if enabled == "" || enabled == "not-found" {
return serviceState{}
}
state := serviceState{exists: true, enabled: enabled}
if userService {
output, _ = exec.Command("systemctl", "--user", "is-active", name).Output()
if active := strings.TrimSpace(string(output)); active != "" && active != "unknown" {
state.active = active
}
}
return state
}
func printResults(results []checkResult) {
theme := tui.TerminalTheme()
styles := tui.NewStyles(theme)
currentCategory := category(-1)
for _, r := range results {
if r.category != currentCategory {
if currentCategory != -1 {
fmt.Println()
}
fmt.Printf(" %s\n", styles.Bold.Render(r.category.String()))
currentCategory = r.category
}
printResultLine(r, styles)
}
}
func printResultsJSON(results []checkResult) {
var ds DoctorStatus
for _, r := range results {
ds.Add(r)
}
output := doctorOutputJSON{}
output.Summary.Errors = ds.ErrorCount()
output.Summary.Warnings = ds.WarningCount()
output.Summary.OK = ds.OKCount()
output.Summary.Info = len(ds.Info)
output.Results = make([]checkResultJSON, 0, len(results))
for _, r := range results {
output.Results = append(output.Results, r.toJSON())
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(output); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(1)
}
}
func printResultLine(r checkResult, styles tui.Styles) {
icon, style := r.status.IconStyle(styles)
name := r.name
nameLen := len(name)
if nameLen > checkNameMaxLength {
name = name[:checkNameMaxLength-1] + "…"
nameLen = checkNameMaxLength
}
dots := strings.Repeat("·", checkNameMaxLength-nameLen)
fmt.Printf(" %s %s %s %s\n", style.Render(icon), name, styles.Subtle.Render(dots), r.message)
if doctorVerbose && r.details != "" {
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
}
if (r.status == statusError || r.status == statusWarn) && r.url != "" {
fmt.Printf(" %s\n", styles.Subtle.Render("→ "+r.url))
}
}
func printSummary(results []checkResult, qsMissingFeatures bool) {
theme := tui.TerminalTheme()
styles := tui.NewStyles(theme)
var ds DoctorStatus
for _, r := range results {
ds.Add(r)
}
fmt.Println()
fmt.Printf(" %s\n", styles.Subtle.Render("──────────────────────────────────────"))
if !ds.HasIssues() {
fmt.Printf(" %s\n", styles.Success.Render("✓ All checks passed!"))
} else {
var parts []string
if ds.ErrorCount() > 0 {
parts = append(parts, styles.Error.Render(fmt.Sprintf("%d error(s)", ds.ErrorCount())))
}
if ds.WarningCount() > 0 {
parts = append(parts, styles.Warning.Render(fmt.Sprintf("%d warning(s)", ds.WarningCount())))
}
parts = append(parts, styles.Success.Render(fmt.Sprintf("%d ok", ds.OKCount())))
fmt.Printf(" %s\n", strings.Join(parts, ", "))
if qsMissingFeatures {
fmt.Println()
fmt.Printf(" %s\n", styles.Subtle.Render("→ Consider using quickshell-git for full feature support"))
}
}
fmt.Println()
}

View File

@@ -16,14 +16,26 @@ var dpmsOnCmd = &cobra.Command{
Use: "on [output]",
Short: "Turn display(s) on",
Args: cobra.MaximumNArgs(1),
Run: runDPMSOn,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp
},
Run: runDPMSOn,
}
var dpmsOffCmd = &cobra.Command{
Use: "off [output]",
Short: "Turn display(s) off",
Args: cobra.MaximumNArgs(1),
Run: runDPMSOff,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp
},
Run: runDPMSOff,
}
var dpmsListCmd = &cobra.Command{
@@ -71,6 +83,15 @@ func runDPMSOff(cmd *cobra.Command, args []string) {
}
}
func getDPMSOutputs() []string {
client, err := newDPMSClient()
if err != nil {
return nil
}
defer client.Close()
return client.ListOutputs()
}
func runDPMSList(cmd *cobra.Command, args []string) {
client, err := newDPMSClient()
if err != nil {

View File

@@ -15,6 +15,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/spf13/cobra"
)
@@ -77,8 +78,6 @@ func runUpdate() {
switch config.Family {
case distros.FamilyArch:
updateErr = updateArchLinux()
case distros.FamilyNix:
updateErr = updateNixOS()
case distros.FamilySUSE:
updateErr = updateOtherDistros()
default:
@@ -123,10 +122,10 @@ func updateArchLinux() error {
var helper string
var updateCmd *exec.Cmd
if commandExists("yay") {
if utils.CommandExists("yay") {
helper = "yay"
updateCmd = exec.Command("yay", "-S", packageName)
} else if commandExists("paru") {
} else if utils.CommandExists("paru") {
helper = "paru"
updateCmd = exec.Command("paru", "-S", packageName)
} else {
@@ -152,27 +151,6 @@ func updateArchLinux() error {
return nil
}
func updateNixOS() error {
fmt.Println("This will update DankMaterialShell using nix profile.")
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Println("\nRunning: nix profile upgrade github:AvengeMedia/DankMaterialShell")
updateCmd := exec.Command("nix", "profile", "upgrade", "github:AvengeMedia/DankMaterialShell")
updateCmd.Stdout = os.Stdout
updateCmd.Stderr = os.Stderr
err := updateCmd.Run()
if err != nil {
fmt.Printf("Error: Failed to update using nix profile: %v\n", err)
fmt.Println("Falling back to git-based update method...")
return updateOtherDistros()
}
fmt.Println("dms successfully updated")
return nil
}
func updateOtherDistros() error {
homeDir, err := os.UserHomeDir()
if err != nil {
@@ -399,7 +377,7 @@ func updateDMSBinary() error {
}
version := ""
for _, line := range strings.Split(string(output), "\n") {
for line := range strings.SplitSeq(string(output), "\n") {
if strings.Contains(line, "\"tag_name\"") {
parts := strings.Split(line, "\"")
if len(parts) >= 4 {
@@ -465,7 +443,7 @@ func updateDMSBinary() error {
decompressedPath := filepath.Join(tempDir, "dms")
if err := os.Chmod(decompressedPath, 0755); err != nil {
if err := os.Chmod(decompressedPath, 0o755); err != nil {
return fmt.Errorf("failed to make binary executable: %w", err)
}

View File

@@ -10,7 +10,10 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var greeterCmd = &cobra.Command{
@@ -208,8 +211,8 @@ func checkGroupExists(groupName string) bool {
return false
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
lines := strings.SplitSeq(string(data), "\n")
for line := range lines {
if strings.HasPrefix(line, groupName+":") {
return true
}
@@ -217,6 +220,191 @@ func checkGroupExists(groupName string) bool {
return false
}
func disableDisplayManager(dmName string) (bool, error) {
state, err := getSystemdServiceState(dmName)
if err != nil {
return false, fmt.Errorf("failed to check %s state: %w", dmName, err)
}
if !state.Exists {
return false, nil
}
fmt.Printf("\nChecking %s...\n", dmName)
fmt.Printf(" Current state: enabled=%s\n", state.EnabledState)
actionTaken := false
if state.NeedsDisable {
var disableCmd *exec.Cmd
var actionVerb string
if state.EnabledState == "static" {
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
actionVerb = "masked"
} else {
fmt.Printf(" Disabling %s...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
actionVerb = "disabled"
}
disableCmd.Stdout = os.Stdout
disableCmd.Stderr = os.Stderr
if err := disableCmd.Run(); err != nil {
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
}
enabledState, shouldDisable, verifyErr := checkSystemdServiceEnabled(dmName)
if verifyErr != nil {
fmt.Printf(" ⚠ Warning: Could not verify %s was %s: %v\n", dmName, actionVerb, verifyErr)
} else if shouldDisable {
return actionTaken, fmt.Errorf("%s is still in state '%s' after %s operation", dmName, enabledState, actionVerb)
} else {
fmt.Printf(" ✓ %s %s (now: %s)\n", cases.Title(language.English).String(actionVerb), dmName, enabledState)
}
actionTaken = true
} else {
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Printf(" ✓ %s is already masked\n", dmName)
} else {
fmt.Printf(" ✓ %s is already disabled\n", dmName)
}
}
return actionTaken, nil
}
func ensureGreetdEnabled() error {
fmt.Println("\nChecking greetd service status...")
state, err := getSystemdServiceState("greetd")
if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err)
}
if !state.Exists {
return fmt.Errorf("greetd service not found. Please install greetd first")
}
fmt.Printf(" Current state: %s\n", state.EnabledState)
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Println(" Unmasking greetd...")
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
unmaskCmd.Stdout = os.Stdout
unmaskCmd.Stderr = os.Stderr
if err := unmaskCmd.Run(); err != nil {
return fmt.Errorf("failed to unmask greetd: %w", err)
}
fmt.Println(" ✓ Unmasked greetd")
}
switch state.EnabledState {
case "disabled", "masked", "masked-runtime":
fmt.Println(" Enabling greetd service...")
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
enableCmd.Stdout = os.Stdout
enableCmd.Stderr = os.Stderr
if err := enableCmd.Run(); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
fmt.Println(" ✓ Enabled greetd service")
case "enabled", "enabled-runtime":
fmt.Println(" ✓ greetd is already enabled")
default:
fmt.Printf(" greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
}
return nil
}
func ensureGraphicalTarget() error {
getDefaultCmd := exec.Command("systemctl", "get-default")
currentTarget, err := getDefaultCmd.Output()
if err != nil {
fmt.Println("⚠ Warning: Could not detect current default systemd target")
return nil
}
currentTargetStr := strings.TrimSpace(string(currentTarget))
if currentTargetStr != "graphical.target" {
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
setDefaultCmd.Stdout = os.Stdout
setDefaultCmd.Stderr = os.Stderr
if err := setDefaultCmd.Run(); err != nil {
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
fmt.Println(" Greeter may not start on boot. Run manually:")
fmt.Println(" sudo systemctl set-default graphical.target")
return nil
}
fmt.Println("✓ Set graphical.target as default")
} else {
fmt.Println("✓ Default target already set to graphical.target")
}
return nil
}
func handleConflictingDisplayManagers() error {
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
disabledAny := false
var errors []string
for _, dm := range conflictingDMs {
actionTaken, err := disableDisplayManager(dm)
if err != nil {
errMsg := fmt.Sprintf("Failed to handle %s: %v", dm, err)
errors = append(errors, errMsg)
fmt.Printf(" ⚠⚠⚠ ERROR: %s\n", errMsg)
continue
}
if actionTaken {
disabledAny = true
}
}
if len(errors) > 0 {
fmt.Println("\n╔════════════════════════════════════════════════════════════╗")
fmt.Println("║ ⚠⚠⚠ ERRORS OCCURRED ⚠⚠⚠ ║")
fmt.Println("╚════════════════════════════════════════════════════════════╝")
fmt.Println("\nSome display managers could not be disabled:")
for _, err := range errors {
fmt.Printf(" ✗ %s\n", err)
}
fmt.Println("\nThis may prevent greetd from starting properly.")
fmt.Println("You may need to manually disable them before greetd will work.")
fmt.Println("\nManual commands to try:")
for _, dm := range conflictingDMs {
fmt.Printf(" sudo systemctl disable %s\n", dm)
fmt.Printf(" sudo systemctl mask %s\n", dm)
}
fmt.Print("\nContinue with greeter enablement anyway? (Y/n): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response == "n" || response == "no" {
return fmt.Errorf("aborted due to display manager conflicts")
}
fmt.Println("\nContinuing despite errors...")
}
if !disabledAny && len(errors) == 0 {
fmt.Println("\n✓ No conflicting display managers found")
} else if disabledAny && len(errors) == 0 {
fmt.Println("\n✓ Successfully handled all conflicting display managers")
}
return nil
}
func enableGreeter() error {
fmt.Println("=== DMS Greeter Enable ===")
fmt.Println()
@@ -232,15 +420,36 @@ func enableGreeter() error {
}
configContent := string(data)
if strings.Contains(configContent, "dms-greeter") {
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
if configAlreadyCorrect {
fmt.Println("✓ Greeter is already configured with dms-greeter")
if err := ensureGraphicalTarget(); err != nil {
return err
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Enable Complete ===")
fmt.Println("\nGreeter configuration verified and system state corrected.")
fmt.Println("To start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.")
return nil
}
fmt.Println("Detecting installed compositors...")
compositors := greeter.DetectCompositors()
if commandExists("sway") {
if utils.CommandExists("sway") {
compositors = append(compositors, "sway")
}
@@ -312,7 +521,7 @@ func enableGreeter() error {
newConfig := strings.Join(finalLines, "\n")
tmpFile := "/tmp/greetd-config.toml"
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
return fmt.Errorf("failed to write temp config: %w", err)
}
@@ -322,11 +531,23 @@ func enableGreeter() error {
}
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
if err := ensureGraphicalTarget(); err != nil {
return err
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Enable Complete ===")
fmt.Println("\nTo start the greeter, run:")
fmt.Println("\nTo start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nTo enable on boot, run:")
fmt.Println(" sudo systemctl enable --now greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.")
return nil
}
@@ -371,8 +592,8 @@ func checkGreeterStatus() error {
if data, err := os.ReadFile(configPath); err == nil {
configContent := string(data)
if strings.Contains(configContent, "dms-greeter") {
lines := strings.Split(configContent, "\n")
for _, line := range lines {
lines := strings.SplitSeq(configContent, "\n")
for line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
parts := strings.SplitN(trimmed, "=", 2)

View File

@@ -30,14 +30,46 @@ var keybindsShowCmd = &cobra.Command{
Short: "Show keybinds for a provider",
Long: "Display keybinds/cheatsheet for the specified provider",
Args: cobra.ExactArgs(1),
Run: runKeybindsShow,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
registry := keybinds.GetDefaultRegistry()
return registry.List(), cobra.ShellCompDirectiveNoFileComp
},
Run: runKeybindsShow,
}
var keybindsSetCmd = &cobra.Command{
Use: "set <provider> <key> <action>",
Short: "Set a keybind override",
Long: "Create or update a keybind override for the specified provider",
Args: cobra.ExactArgs(3),
Run: runKeybindsSet,
}
var keybindsRemoveCmd = &cobra.Command{
Use: "remove <provider> <key>",
Short: "Remove a keybind override",
Long: "Remove a keybind override from the specified provider",
Args: cobra.ExactArgs(2),
Run: runKeybindsRemove,
}
func init() {
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
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)")
keybindsCmd.AddCommand(keybindsListCmd)
keybindsCmd.AddCommand(keybindsShowCmd)
keybindsCmd.AddCommand(keybindsSetCmd)
keybindsCmd.AddCommand(keybindsRemoveCmd)
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
return providers.NewJSONFileProvider(filePath)
@@ -59,78 +91,157 @@ func initializeProviders() {
log.Warnf("Failed to register MangoWC provider: %v", err)
}
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
}
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err)
}
niriProvider := providers.NewNiriProvider("")
if err := registry.Register(niriProvider); err != nil {
log.Warnf("Failed to register Niri provider: %v", err)
}
config := keybinds.DefaultDiscoveryConfig()
if err := keybinds.AutoDiscoverProviders(registry, config); err != nil {
log.Warnf("Failed to auto-discover providers: %v", err)
}
}
func runKeybindsList(cmd *cobra.Command, args []string) {
registry := keybinds.GetDefaultRegistry()
providers := registry.List()
func runKeybindsList(cmd *cobra.Command, _ []string) {
providerList := keybinds.GetDefaultRegistry().List()
asJSON, _ := cmd.Flags().GetBool("json")
if len(providers) == 0 {
if asJSON {
output, _ := json.Marshal(providerList)
fmt.Fprintln(os.Stdout, string(output))
return
}
if len(providerList) == 0 {
fmt.Fprintln(os.Stdout, "No providers available")
return
}
fmt.Fprintln(os.Stdout, "Available providers:")
for _, name := range providers {
for _, name := range providerList {
fmt.Fprintf(os.Stdout, " - %s\n", name)
}
}
func runKeybindsShow(cmd *cobra.Command, args []string) {
providerName := args[0]
registry := keybinds.GetDefaultRegistry()
customPath, _ := cmd.Flags().GetString("path")
if customPath != "" {
var provider keybinds.Provider
switch providerName {
case "hyprland":
provider = providers.NewHyprlandProvider(customPath)
case "mangowc":
provider = providers.NewMangoWCProvider(customPath)
case "sway":
provider = providers.NewSwayProvider(customPath)
default:
log.Fatalf("Provider %s does not support custom path", providerName)
}
sheet, err := provider.GetCheatSheet()
if err != nil {
log.Fatalf("Error getting cheatsheet: %v", err)
}
output, err := json.MarshalIndent(sheet, "", " ")
if err != nil {
log.Fatalf("Error generating JSON: %v", err)
}
fmt.Fprintln(os.Stdout, string(output))
return
}
provider, err := registry.Get(providerName)
if err != nil {
log.Fatalf("Error: %v", err)
func makeProviderWithPath(name, path string) keybinds.Provider {
switch name {
case "hyprland":
return providers.NewHyprlandProvider(path)
case "mangowc":
return providers.NewMangoWCProvider(path)
case "sway":
return providers.NewSwayProvider(path)
case "scroll":
return providers.NewSwayProvider(path)
case "niri":
return providers.NewNiriProvider(path)
default:
return nil
}
}
func printCheatSheet(provider keybinds.Provider) {
sheet, err := provider.GetCheatSheet()
if err != nil {
log.Fatalf("Error getting cheatsheet: %v", err)
}
output, err := json.MarshalIndent(sheet, "", " ")
if err != nil {
log.Fatalf("Error generating JSON: %v", err)
}
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsShow(cmd *cobra.Command, args []string) {
providerName := args[0]
customPath, _ := cmd.Flags().GetString("path")
if customPath != "" {
provider := makeProviderWithPath(providerName, customPath)
if provider == nil {
log.Fatalf("Provider %s does not support custom path", providerName)
}
printCheatSheet(provider)
return
}
provider, err := keybinds.GetDefaultRegistry().Get(providerName)
if err != nil {
log.Fatalf("Error: %v", err)
}
printCheatSheet(provider)
}
func getWritableProvider(name string) keybinds.WritableProvider {
provider, err := keybinds.GetDefaultRegistry().Get(name)
if err != nil {
log.Fatalf("Error: %v", err)
}
writable, ok := provider.(keybinds.WritableProvider)
if !ok {
log.Fatalf("Provider %s does not support writing keybinds", name)
}
return writable
}
func runKeybindsSet(cmd *cobra.Command, args []string) {
providerName, key, action := args[0], args[1], args[2]
writable := getWritableProvider(providerName)
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
_ = writable.RemoveBind(replaceKey)
}
options := make(map[string]any)
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
options["allow-when-locked"] = true
}
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
options["cooldown-ms"] = v
}
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false
}
if v, _ := cmd.Flags().GetString("flags"); v != "" {
options["flags"] = v
}
desc, _ := cmd.Flags().GetString("desc")
if err := writable.SetBind(key, action, desc, options); err != nil {
log.Fatalf("Error setting keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"action": action,
"path": writable.GetOverridePath(),
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsRemove(_ *cobra.Command, args []string) {
providerName, key := args[0], args[1]
writable := getWritableProvider(providerName)
if err := writable.RemoveBind(key); err != nil {
log.Fatalf("Error removing keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"removed": true,
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
}

View File

@@ -0,0 +1,181 @@
package main
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
var matugenCmd = &cobra.Command{
Use: "matugen",
Short: "Generate Material Design themes",
Long: "Generate Material Design themes using matugen with dank16 color integration",
}
var matugenGenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate theme synchronously",
Run: runMatugenGenerate,
}
var matugenQueueCmd = &cobra.Command{
Use: "queue",
Short: "Queue theme generation (uses socket if available)",
Run: runMatugenQueue,
}
var matugenCheckCmd = &cobra.Command{
Use: "check",
Short: "Check which template apps are detected",
Run: runMatugenCheck,
}
func init() {
matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd)
matugenCmd.AddCommand(matugenCheckCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files")
cmd.Flags().String("shell-dir", "", "DMS shell installation directory")
cmd.Flags().String("config-dir", "", "User config directory")
cmd.Flags().String("kind", "image", "Source type: image or hex")
cmd.Flags().String("value", "", "Wallpaper path or hex color")
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
}
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
}
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
stateDir, _ := cmd.Flags().GetString("state-dir")
shellDir, _ := cmd.Flags().GetString("shell-dir")
configDir, _ := cmd.Flags().GetString("config-dir")
kind, _ := cmd.Flags().GetString("kind")
value, _ := cmd.Flags().GetString("value")
mode, _ := cmd.Flags().GetString("mode")
iconTheme, _ := cmd.Flags().GetString("icon-theme")
matugenType, _ := cmd.Flags().GetString("matugen-type")
runUserTemplates, _ := cmd.Flags().GetBool("run-user-templates")
stockColors, _ := cmd.Flags().GetString("stock-colors")
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
return matugen.Options{
StateDir: stateDir,
ShellDir: shellDir,
ConfigDir: configDir,
Kind: kind,
Value: value,
Mode: matugen.ColorMode(mode),
IconTheme: iconTheme,
MatugenType: matugenType,
RunUserTemplates: runUserTemplates,
StockColors: stockColors,
SyncModeWithPortal: syncModeWithPortal,
TerminalsAlwaysDark: terminalsAlwaysDark,
SkipTemplates: skipTemplates,
}
}
func runMatugenGenerate(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd)
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
}
func runMatugenQueue(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd)
wait, _ := cmd.Flags().GetBool("wait")
timeout, _ := cmd.Flags().GetDuration("timeout")
request := models.Request{
ID: 1,
Method: "matugen.queue",
Params: map[string]any{
"stateDir": opts.StateDir,
"shellDir": opts.ShellDir,
"configDir": opts.ConfigDir,
"kind": opts.Kind,
"value": opts.Value,
"mode": opts.Mode,
"iconTheme": opts.IconTheme,
"matugenType": opts.MatugenType,
"runUserTemplates": opts.RunUserTemplates,
"stockColors": opts.StockColors,
"syncModeWithPortal": opts.SyncModeWithPortal,
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"skipTemplates": opts.SkipTemplates,
"wait": wait,
},
}
if !wait {
if err := sendServerRequestFireAndForget(request); err != nil {
log.Info("Server unavailable, running synchronously")
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
return
}
fmt.Println("Theme generation queued")
return
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resultCh := make(chan error, 1)
go func() {
resp, ok := tryServerRequest(request)
if !ok {
log.Info("Server unavailable, running synchronously")
if err := matugen.Run(opts); err != nil {
resultCh <- err
return
}
resultCh <- nil
return
}
if resp.Error != "" {
resultCh <- fmt.Errorf("server error: %s", resp.Error)
return
}
resultCh <- nil
}()
select {
case err := <-resultCh:
if err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
fmt.Println("Theme generation completed")
case <-ctx.Done():
log.Fatalf("Timeout waiting for theme generation")
}
}
func runMatugenCheck(cmd *cobra.Command, args []string) {
checks := matugen.CheckTemplates(nil)
data, err := json.Marshal(checks)
if err != nil {
log.Fatalf("Failed to marshal check results: %v", err)
}
fmt.Println(string(data))
}

View File

@@ -0,0 +1,205 @@
package main
import (
"fmt"
"mime"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
var (
openMimeType string
openCategories []string
openRequestType string
)
var openCmd = &cobra.Command{
Use: "open [target]",
Short: "Open a file, URL, or resource with an application picker",
Long: `Open a target (URL, file, or other resource) using the DMS application picker.
By default, this opens URLs with the browser picker. You can customize the behavior
with flags to handle different MIME types or application categories.
Examples:
dms open https://example.com # Open URL with browser picker
dms open file.pdf --mime application/pdf # Open PDF with compatible apps
dms open document.odt --category Office # Open with office applications
dms open --mime image/png image.png # Open image with image viewers`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runOpen(args[0])
},
}
func init() {
rootCmd.AddCommand(openCmd)
openCmd.Flags().StringVar(&openMimeType, "mime", "", "MIME type for filtering applications")
openCmd.Flags().StringSliceVar(&openCategories, "category", []string{}, "Application categories to filter (e.g., WebBrowser, Office, Graphics)")
openCmd.Flags().StringVar(&openRequestType, "type", "url", "Request type (url, file, or custom)")
_ = openCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"url", "file", "custom"}, cobra.ShellCompDirectiveNoFileComp
})
}
// mimeTypeToCategories maps MIME types to desktop file categories
func mimeTypeToCategories(mimeType string) []string {
// Split MIME type to get the main type
parts := strings.Split(mimeType, "/")
if len(parts) < 1 {
return nil
}
mainType := parts[0]
switch mainType {
case "image":
return []string{"Graphics", "Viewer"}
case "video":
return []string{"Video", "AudioVideo"}
case "audio":
return []string{"Audio", "AudioVideo"}
case "text":
if strings.Contains(mimeType, "html") {
return []string{"WebBrowser"}
}
return []string{"TextEditor", "Office"}
case "application":
if strings.Contains(mimeType, "pdf") {
return []string{"Office", "Viewer"}
}
if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "spreadsheet") ||
strings.Contains(mimeType, "presentation") || strings.Contains(mimeType, "msword") ||
strings.Contains(mimeType, "ms-excel") || strings.Contains(mimeType, "ms-powerpoint") ||
strings.Contains(mimeType, "opendocument") {
return []string{"Office"}
}
if strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") ||
strings.Contains(mimeType, "gzip") || strings.Contains(mimeType, "compress") {
return []string{"Archiving", "Utility"}
}
return []string{"Office", "Viewer"}
}
return nil
}
func runOpen(target string) {
// Parse file:// URIs to extract the actual file path
actualTarget := target
detectedMimeType := openMimeType
detectedCategories := openCategories
detectedRequestType := openRequestType
log.Infof("Processing target: %s", target)
if parsedURL, err := url.Parse(target); err == nil && parsedURL.Scheme == "file" {
// Extract file path from file:// URI and convert to absolute path
actualTarget = parsedURL.Path
if absPath, err := filepath.Abs(actualTarget); err == nil {
actualTarget = absPath
}
if detectedRequestType == "url" || detectedRequestType == "" {
detectedRequestType = "file"
}
log.Infof("Detected file:// URI, extracted absolute path: %s", actualTarget)
// Auto-detect MIME type if not provided
if detectedMimeType == "" {
ext := filepath.Ext(actualTarget)
if ext != "" {
detectedMimeType = mime.TypeByExtension(ext)
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
}
}
// Auto-detect categories based on MIME type if not provided
if len(detectedCategories) == 0 && detectedMimeType != "" {
detectedCategories = mimeTypeToCategories(detectedMimeType)
log.Infof("Detected categories from MIME type: %v", detectedCategories)
}
} else if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
// Handle HTTP(S) URLs
if detectedRequestType == "" {
detectedRequestType = "url"
}
log.Infof("Detected HTTP(S) URL")
} else if strings.HasPrefix(target, "dms://") {
// Handle DMS internal URLs (theme/plugin install, etc.)
if detectedRequestType == "" {
detectedRequestType = "url"
}
log.Infof("Detected DMS internal URL")
} else if _, err := os.Stat(target); err == nil {
// Handle local file paths directly (not file:// URIs)
// Convert to absolute path
if absPath, err := filepath.Abs(target); err == nil {
actualTarget = absPath
}
if detectedRequestType == "url" || detectedRequestType == "" {
detectedRequestType = "file"
}
log.Infof("Detected local file path, converted to absolute: %s", actualTarget)
// Auto-detect MIME type if not provided
if detectedMimeType == "" {
ext := filepath.Ext(actualTarget)
if ext != "" {
detectedMimeType = mime.TypeByExtension(ext)
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
}
}
// Auto-detect categories based on MIME type if not provided
if len(detectedCategories) == 0 && detectedMimeType != "" {
detectedCategories = mimeTypeToCategories(detectedMimeType)
log.Infof("Detected categories from MIME type: %v", detectedCategories)
}
}
params := map[string]any{
"target": actualTarget,
}
if detectedMimeType != "" {
params["mimeType"] = detectedMimeType
}
if len(detectedCategories) > 0 {
params["categories"] = detectedCategories
}
if detectedRequestType != "" {
params["requestType"] = detectedRequestType
}
method := "apppicker.open"
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "dms://")) {
method = "browser.open"
params["url"] = target
}
req := models.Request{
ID: 1,
Method: method,
Params: params,
}
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
if err := sendServerRequestFireAndForget(req); err != nil {
fmt.Println("DMS is not running. Please start DMS first.")
os.Exit(1)
}
log.Infof("Request sent successfully")
}

View File

@@ -1,14 +1,12 @@
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
tea "github.com/charmbracelet/bubbletea"
@@ -52,15 +50,18 @@ func findConfig(cmd *cobra.Command, args []string) error {
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
statePath := strings.TrimSpace(string(data))
shellPath := filepath.Join(statePath, "shell.qml")
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
log.Debug("Using config from active session state file: %s", statePath)
configPath = statePath
log.Debug("Using config from: %s", configPath)
return nil // <-- Guard statement
if len(getAllDMSPIDs()) == 0 {
os.Remove(configStateFile)
} else {
statePath := strings.TrimSpace(string(data))
shellPath := filepath.Join(statePath, "shell.qml")
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
log.Debug("Using config from active session state file: %s", statePath)
configPath = statePath
log.Debug("Using config from: %s", configPath)
return nil
}
os.Remove(configStateFile)
}
}
@@ -76,14 +77,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
return nil
}
func runInteractiveMode(cmd *cobra.Command, args []string) {
detector, err := dms.NewDetector()
if err != nil && !errors.Is(err, &distros.UnsupportedDistributionError{}) {
log.Fatalf("Error initializing DMS detector: %v", err)
} else if errors.Is(err, &distros.UnsupportedDistributionError{}) {
log.Error("Interactive mode is not supported on this distribution.")
log.Info("Please run 'dms --help' for available commands.")
os.Exit(1)
}
detector, _ := dms.NewDetector()
if !detector.IsDMSInstalled() {
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")

View File

@@ -0,0 +1,412 @@
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
"github.com/spf13/cobra"
)
var (
ssOutputName string
ssIncludeCursor bool
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
)
var screenshotCmd = &cobra.Command{
Use: "screenshot",
Short: "Capture screenshots",
Long: `Capture screenshots from Wayland displays.
Modes:
region - Select a region interactively (default)
full - Capture the focused output
all - Capture all outputs combined
output - Capture a specific output by name
window - Capture the focused window (Hyprland/DWL)
last - Capture the last selected region
Output format (--format):
png - PNG format (default)
jpg/jpeg - JPEG format
ppm - PPM format
Examples:
dms screenshot # Region select, save file + clipboard
dms screenshot full # Full screen of focused output
dms screenshot all # All screens combined
dms screenshot output -o DP-1 # Specific output
dms screenshot window # Focused window (Hyprland)
dms screenshot last # Last region (pre-selected)
dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only
dms screenshot --cursor # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
}
var ssRegionCmd = &cobra.Command{
Use: "region",
Short: "Select a region interactively",
Run: runScreenshotRegion,
}
var ssFullCmd = &cobra.Command{
Use: "full",
Short: "Capture the focused output",
Run: runScreenshotFull,
}
var ssAllCmd = &cobra.Command{
Use: "all",
Short: "Capture all outputs combined",
Run: runScreenshotAll,
}
var ssOutputCmd = &cobra.Command{
Use: "output",
Short: "Capture a specific output",
Run: runScreenshotOutput,
}
var ssLastCmd = &cobra.Command{
Use: "last",
Short: "Capture the last selected region",
Long: `Capture the previously selected region without interactive selection.
If no previous region exists, falls back to interactive selection.`,
Run: runScreenshotLast,
}
var ssWindowCmd = &cobra.Command{
Use: "window",
Short: "Capture the focused window",
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
Run: runScreenshotWindow,
}
var ssListCmd = &cobra.Command{
Use: "list",
Short: "List available outputs",
Run: runScreenshotList,
}
var notifyActionCmd = &cobra.Command{
Use: "notify-action",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
screenshot.RunNotifyActionListener(args)
},
}
func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
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().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
screenshotCmd.PersistentFlags().StringVar(&ssFilename, "filename", "", "Output filename (auto-generated if empty)")
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
screenshotCmd.AddCommand(ssRegionCmd)
screenshotCmd.AddCommand(ssFullCmd)
screenshotCmd.AddCommand(ssAllCmd)
screenshotCmd.AddCommand(ssOutputCmd)
screenshotCmd.AddCommand(ssLastCmd)
screenshotCmd.AddCommand(ssWindowCmd)
screenshotCmd.AddCommand(ssListCmd)
screenshotCmd.Run = runScreenshotRegion
}
func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig()
config.Mode = mode
config.OutputName = ssOutputName
config.IncludeCursor = ssIncludeCursor
config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify
config.Stdout = ssStdout
if ssOutputDir != "" {
config.OutputDir = ssOutputDir
}
if ssFilename != "" {
config.Filename = ssFilename
}
switch strings.ToLower(ssFormat) {
case "jpg", "jpeg":
config.Format = screenshot.FormatJPEG
case "ppm":
config.Format = screenshot.FormatPPM
default:
config.Format = screenshot.FormatPNG
}
if ssQuality < 1 {
ssQuality = 1
}
if ssQuality > 100 {
ssQuality = 100
}
config.Quality = ssQuality
return config
}
func runScreenshot(config screenshot.Config) {
sc := screenshot.New(config)
result, err := sc.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if result == nil {
os.Exit(0)
}
defer result.Buffer.Close()
if result.YInverted {
result.Buffer.FlipVertical()
}
if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
os.Exit(1)
}
return
}
var filePath string
if config.SaveFile {
outputDir := config.OutputDir
if outputDir == "" {
outputDir = screenshot.GetOutputDir()
}
filename := config.Filename
if filename == "" {
filename = screenshot.GenerateFilename(config.Format)
}
filePath = filepath.Join(outputDir, filename)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
os.Exit(1)
}
fmt.Println(filePath)
}
if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
os.Exit(1)
}
if !config.SaveFile {
fmt.Println("Copied to clipboard")
}
}
if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
screenshot.SendNotification(screenshot.NotifyResult{
FilePath: filePath,
Clipboard: config.Clipboard,
ImageData: thumbData,
Width: thumbW,
Height: thumbH,
})
}
}
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
var mimeType string
var data bytes.Buffer
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
switch format {
case screenshot.FormatJPEG:
mimeType = "image/jpeg"
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
return err
}
default:
mimeType = "image/png"
if err := screenshot.EncodePNG(&data, img); err != nil {
return err
}
}
return clipboard.Copy(data.Bytes(), mimeType)
}
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
switch format {
case screenshot.FormatJPEG:
return screenshot.EncodeJPEG(os.Stdout, img, quality)
default:
return screenshot.EncodePNG(os.Stdout, img)
}
}
func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) {
srcW, srcH := buf.Width, buf.Height
scale := 1.0
if srcW > maxSize || srcH > maxSize {
if srcW > srcH {
scale = float64(maxSize) / float64(srcW)
} else {
scale = float64(maxSize) / float64(srcH)
}
}
dstW := int(float64(srcW) * scale)
dstH := int(float64(srcH) * scale)
if dstW < 1 {
dstW = 1
}
if dstH < 1 {
dstH = 1
}
data := buf.Data()
rgb := make([]byte, dstW*dstH*3)
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < dstH; y++ {
srcY := int(float64(y) / scale)
if srcY >= srcH {
srcY = srcH - 1
}
for x := 0; x < dstW; x++ {
srcX := int(float64(x) / scale)
if srcX >= srcW {
srcX = srcW - 1
}
si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3
if si+3 >= len(data) {
continue
}
if swapRB {
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
} else {
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
}
}
}
return rgb, dstW, dstH
}
func runScreenshotRegion(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeRegion)
runScreenshot(config)
}
func runScreenshotFull(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeFullScreen)
runScreenshot(config)
}
func runScreenshotAll(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeAllScreens)
runScreenshot(config)
}
func runScreenshotOutput(cmd *cobra.Command, args []string) {
if ssOutputName == "" && len(args) > 0 {
ssOutputName = args[0]
}
if ssOutputName == "" {
fmt.Fprintln(os.Stderr, "Error: output name required (use -o or provide as argument)")
os.Exit(1)
}
config := getScreenshotConfig(screenshot.ModeOutput)
runScreenshot(config)
}
func runScreenshotLast(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeLastRegion)
runScreenshot(config)
}
func runScreenshotWindow(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeWindow)
runScreenshot(config)
}
func runScreenshotList(cmd *cobra.Command, args []string) {
outputs, err := screenshot.ListOutputs()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
for _, o := range outputs {
scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
if o.FractionalScale == float64(int(o.FractionalScale)) {
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
}
transformStr := transformName(o.Transform)
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
}
}
func transformName(t int32) string {
switch t {
case 0:
return "normal"
case 1:
return "90"
case 2:
return "180"
case 3:
return "270"
case 4:
return "flipped"
case 5:
return "flipped-90"
case 6:
return "flipped-180"
case 7:
return "flipped-270"
default:
return fmt.Sprintf("%d", t)
}
}

View File

@@ -29,6 +29,7 @@ func runSetup() error {
wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.")
@@ -67,14 +68,14 @@ func runSetup() error {
var err error
if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal)
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
} else if wmSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
if len(results) > 1 {
results = results[:1]
}
} else if terminalSelected {
results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal)
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:]
}
@@ -144,6 +145,19 @@ func promptTerminal() (deps.Terminal, bool) {
}
}
func promptSystemd() bool {
fmt.Println("\nUse systemd for session management?")
fmt.Println("1) Yes (recommended for most distros)")
fmt.Println("2) No (standalone, no systemd integration)")
var response string
fmt.Print("\nChoice (1-2): ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
return response != "2"
}
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
homeDir := os.Getenv("HOME")
willBackup := false

View File

@@ -87,20 +87,14 @@ func newDPMSClient() (*dpmsClient, error) {
switch e.Interface {
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
version := e.Version
if version > 1 {
version = 1
}
version := min(e.Version, 1)
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
c.powerMgr = powerMgr
}
case "wl_output":
output := wlclient.NewOutput(c.ctx)
version := e.Version
if version > 4 {
version = 4
}
version := min(e.Version, 4)
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
outputID := fmt.Sprintf("output-%d", output.ID())
state := &outputState{

View File

@@ -23,7 +23,7 @@ func init() {
updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...)

View File

@@ -21,7 +21,7 @@ func init() {
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...)

View File

@@ -0,0 +1,114 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func sendServerRequest(req models.Request) (*models.Response[any], error) {
socketPath := getServerSocketPath()
conn, err := net.Dial("unix", socketPath)
if err != nil {
return nil, fmt.Errorf("failed to connect to server (is it running?): %w", err)
}
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Scan() // discard initial capabilities message
reqData, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
if _, err := conn.Write(reqData); err != nil {
return nil, fmt.Errorf("failed to write request: %w", err)
}
if _, err := conn.Write([]byte("\n")); err != nil {
return nil, fmt.Errorf("failed to write newline: %w", err)
}
if !scanner.Scan() {
return nil, fmt.Errorf("failed to read response")
}
var resp models.Response[any]
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &resp, nil
}
// sendServerRequestFireAndForget sends a request without waiting for a response.
// Useful for commands that trigger UI or async operations.
func sendServerRequestFireAndForget(req models.Request) error {
socketPath := getServerSocketPath()
conn, err := net.Dial("unix", socketPath)
if err != nil {
return fmt.Errorf("failed to connect to server (is it running?): %w", err)
}
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Scan() // discard initial capabilities message
reqData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
if _, err := conn.Write(reqData); err != nil {
return fmt.Errorf("failed to write request: %w", err)
}
if _, err := conn.Write([]byte("\n")); err != nil {
return fmt.Errorf("failed to write newline: %w", err)
}
return nil
}
// tryServerRequest attempts to send a request but returns false if server unavailable.
// Does not log errors - caller can decide what to do on failure.
func tryServerRequest(req models.Request) (*models.Response[any], bool) {
resp, err := sendServerRequest(req)
if err != nil {
return nil, false
}
return resp, true
}
func getServerSocketPath() string {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
runtimeDir = os.TempDir()
}
entries, err := os.ReadDir(runtimeDir)
if err != nil {
return filepath.Join(runtimeDir, "danklinux.sock")
}
for _, entry := range entries {
name := entry.Name()
if name == "danklinux.sock" {
return filepath.Join(runtimeDir, name)
}
if len(name) > 10 && name[:10] == "danklinux-" && filepath.Ext(name) == ".sock" {
return filepath.Join(runtimeDir, name)
}
}
return server.GetSocketPath()
}

View File

@@ -7,8 +7,10 @@ import (
"os/exec"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"
@@ -16,6 +18,27 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
)
type ipcTargets map[string]map[string][]string
// getProcessExitCode returns the exit code from a ProcessState.
// For normal exits, returns the exit code directly.
// For signal termination, returns 128 + signal number (Unix convention).
func getProcessExitCode(state *os.ProcessState) int {
if state == nil {
return 1
}
if code := state.ExitCode(); code != -1 {
return code
}
// Process was killed by signal - extract signal number
if status, ok := state.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
return 128 + int(status.Signal())
}
}
return 1
}
var isSessionManaged bool
func execDetachedRestart(targetPID int) {
@@ -57,13 +80,18 @@ func getRuntimeDir() string {
return os.TempDir()
}
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
}
func writePIDFile(childPID int) error {
pidFile := getPIDFilePath()
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0644)
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
}
func removePIDFile() {
@@ -97,7 +125,6 @@ func getAllDMSPIDs() []int {
continue
}
// Check if the child process is still alive
proc, err := os.FindProcess(childPID)
if err != nil {
os.Remove(pidFile)
@@ -105,18 +132,15 @@ func getAllDMSPIDs() []int {
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
// Process is dead, remove stale PID file
os.Remove(pidFile)
continue
}
pids = append(pids, childPID)
// Also get the parent PID from the filename
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
// Check if parent is still alive
if parentProc, err := os.FindProcess(parentPID); err == nil {
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
pids = append(pids, parentPID)
@@ -139,7 +163,7 @@ func runShellInteractive(session bool) {
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
@@ -152,6 +176,7 @@ func runShellInteractive(session bool) {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
@@ -161,8 +186,14 @@ func runShellInteractive(session bool) {
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
@@ -172,6 +203,16 @@ func runShellInteractive(session bool) {
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -206,15 +247,28 @@ func runShellInteractive(session bool) {
for {
select {
case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes
if sig == syscall.SIGUSR1 && !isSessionManaged {
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
return
}
// All other signals: clean shutdown
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
case <-time.After(500 * time.Millisecond):
}
log.Infof("\nReceived signal %v, shutting down...", sig)
cancel()
cmd.Process.Signal(syscall.SIGTERM)
@@ -228,7 +282,7 @@ func runShellInteractive(session bool) {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(1)
os.Exit(getProcessExitCode(cmd.ProcessState))
}
}
}
@@ -271,7 +325,6 @@ func restartShell() {
}
func killShell() {
// Get all tracked DMS PIDs from PID files
pids := getAllDMSPIDs()
if len(pids) == 0 {
@@ -282,14 +335,12 @@ func killShell() {
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
// Deduplicate and filter out current process
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
// Kill all tracked processes
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
@@ -297,7 +348,6 @@ func killShell() {
continue
}
// Check if process is still alive before killing
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
@@ -309,7 +359,6 @@ func killShell() {
}
}
// Clean up any remaining PID files
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
@@ -326,14 +375,7 @@ func killShell() {
func runShellDaemon(session bool) {
isSessionManaged = session
// Check if this is the daemon child process by looking for the hidden flag
isDaemonChild := false
for _, arg := range os.Args {
if arg == "--daemon-child" {
isDaemonChild = true
break
}
}
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
if !isDaemonChild {
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
@@ -361,7 +403,7 @@ func runShellDaemon(session bool) {
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
@@ -374,6 +416,7 @@ func runShellDaemon(session bool) {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
@@ -383,8 +426,14 @@ func runShellDaemon(session bool) {
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
@@ -394,6 +443,16 @@ func runShellDaemon(session bool) {
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
}
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
log.Fatalf("Error opening /dev/null: %v", err)
@@ -434,15 +493,28 @@ func runShellDaemon(session bool) {
for {
select {
case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes
if sig == syscall.SIGUSR1 && !isSessionManaged {
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
return
}
// All other signals: clean shutdown
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
case <-time.After(500 * time.Millisecond):
}
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
@@ -454,11 +526,96 @@ func runShellDaemon(session bool) {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(1)
os.Exit(getProcessExitCode(cmd.ProcessState))
}
}
}
var qsHasAnyDisplay = sync.OnceValue(func() bool {
out, err := exec.Command("qs", "ipc", "--help").Output()
if err != nil {
return false
}
return strings.Contains(string(out), "--any-display")
})
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
targets := make(ipcTargets)
var currentTarget string
for line := range strings.SplitSeq(output, "\n") {
if after, ok := strings.CutPrefix(line, "target "); ok {
currentTarget = strings.TrimSpace(after)
targets[currentTarget] = make(map[string][]string)
}
if strings.HasPrefix(line, " function") && currentTarget != "" {
argsList := []string{}
currentFunc := strings.TrimPrefix(line, " function ")
funcDef := strings.SplitN(currentFunc, "(", 2)
argList := strings.SplitN(funcDef[1], ")", 2)[0]
args := strings.Split(argList, ",")
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
argsList = append(argsList, funcDef[0])
for _, arg := range args {
argName := strings.SplitN(strings.TrimSpace(arg), ":", 2)[0]
argsList = append(argsList, argName)
}
targets[currentTarget][funcDef[0]] = argsList
} else {
targets[currentTarget][funcDef[0]] = make([]string, 0)
}
}
}
return targets
}
func getShellIPCCompletions(args []string, _ string) []string {
cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath, "show")
cmd := exec.Command("qs", cmdArgs...)
var targets ipcTargets
if output, err := cmd.Output(); err == nil {
targets = parseTargetsFromIPCShowOutput(string(output))
} else {
log.Debugf("Error getting IPC show output for completions: %v", err)
return nil
}
if len(args) > 0 && args[0] == "call" {
args = args[1:]
}
if len(args) == 0 {
targetNames := make([]string, 0)
targetNames = append(targetNames, "call")
for k := range targets {
targetNames = append(targetNames, k)
}
return targetNames
}
if len(args) == 1 {
if targetFuncs, ok := targets[args[0]]; ok {
funcNames := make([]string, 0)
for k := range targetFuncs {
funcNames = append(funcNames, k)
}
return funcNames
}
return nil
}
if len(args) <= len(targets[args[0]]) {
funcArgs := targets[args[0]][args[1]]
if len(funcArgs) >= len(args) {
return []string{fmt.Sprintf("[%s]", funcArgs[len(args)-1])}
}
}
return nil
}
func runShellIPCCommand(args []string) {
if len(args) == 0 {
log.Error("IPC command requires arguments")
@@ -470,7 +627,12 @@ func runShellIPCCommand(args []string) {
args = append([]string{"call"}, args...)
}
cmdArgs := append([]string{"-p", configPath, "ipc"}, args...)
cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
cmdArgs = append(cmdArgs, args...)
cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout

View File

@@ -17,8 +17,8 @@ func getThemedASCII() string {
logo := `
██████╗ █████╗ ███╗ ██╗██╗ ██╗
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
██║ ██║███████║██╔██╗ ██║█████╔╝
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
██║ ██║███████║██╔██╗ ██║█████╔╝
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
██████╔╝██║ ██║██║ ╚████║██║ ██╗
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`

View File

@@ -3,14 +3,10 @@ package main
import (
"fmt"
"os/exec"
"slices"
"strings"
)
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// findCommandPath returns the absolute path to a command in PATH
func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd)
if err != nil {
@@ -24,3 +20,62 @@ func isArchPackageInstalled(packageName string) bool {
err := cmd.Run()
return err == nil
}
type systemdServiceState struct {
Name string
EnabledState string
NeedsDisable bool
Exists bool
}
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
cmd := exec.Command("systemctl", "is-enabled", serviceName)
output, err := cmd.Output()
stateStr := strings.TrimSpace(string(output))
if err != nil {
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
isKnownState := slices.Contains(knownStates, stateStr)
if !isKnownState {
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
}
}
shouldDisable := false
switch stateStr {
case "enabled", "enabled-runtime", "static", "indirect", "alias":
shouldDisable = true
case "disabled", "masked", "masked-runtime", "not-found":
shouldDisable = false
default:
shouldDisable = true
}
return stateStr, shouldDisable, nil
}
func getSystemdServiceState(serviceName string) (*systemdServiceState, error) {
state := &systemdServiceState{
Name: serviceName,
Exists: false,
}
enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName)
if err != nil {
return nil, fmt.Errorf("failed to check enabled state: %w", err)
}
state.EnabledState = enabledState
state.NeedsDisable = needsDisable
if enabledState == "not-found" {
state.Exists = false
return state, nil
}
state.Exists = true
return state, nil
}

View File

@@ -9,24 +9,28 @@ require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2
github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.1.0
github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
github.com/spf13/cobra v1.10.1
github.com/pilebones/go-udev v0.9.1
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
golang.org/x/image v0.34.0
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.5.0 // indirect
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
github.com/cloudflare/circl v1.6.2 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
@@ -34,21 +38,21 @@ require (
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.0 // indirect
github.com/charmbracelet/x/ansi v0.11.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -62,7 +66,7 @@ require (
github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0 // indirect
golang.org/x/sys v0.39.0
golang.org/x/text v0.32.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -18,29 +18,37 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
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/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -56,16 +64,24 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
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/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 h1:EC9n6hr6yKDoVJ6g7Ko523LbbceJfR0ohbOp809Fyf4=
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0/go.mod h1:E3VhlS+AKkrq6ZNn1axE2/nDRJ87l1FJk9r5HT2vPX0=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9 h1:SOFrnF9LCssC6q6Rb0084Bzg2aBYbe8QXv9xKGXmt/w=
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9/go.mod h1:0wtvm/JfPC9RFVEAP3ks0ec5h64/qmZkTTUE3pjz7Hc=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
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/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -83,8 +99,9 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
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=
@@ -99,6 +116,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -108,12 +127,18 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -125,20 +150,36 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
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/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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env bash
set -e
@@ -9,8 +9,8 @@ NC='\033[0m' # No Color
# Check for root privileges
if [ "$(id -u)" == "0" ]; then
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
exit 1
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
exit 1
fi
# Check if running on Linux
@@ -22,17 +22,17 @@ fi
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
ARCH="amd64"
;;
aarch64)
ARCH="arm64"
;;
*)
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
exit 1
;;
x86_64)
ARCH="amd64"
;;
aarch64)
ARCH="arm64"
;;
*)
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
exit 1
;;
esac
# Get the latest release version
@@ -55,7 +55,7 @@ curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LAT
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
# Get the expected checksum
EXPECTED_CHECKSUM=$(cat expected.sha256 | awk '{print $1}')
EXPECTED_CHECKSUM=$(awk '{print $1}' expected.sha256)
# Calculate actual checksum
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
@@ -67,7 +67,7 @@ if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
printf "The downloaded file may be corrupted or tampered with\n"
cd - > /dev/null
cd - >/dev/null
rm -rf "$TEMP_DIR"
exit 1
fi
@@ -82,5 +82,5 @@ printf "%bRunning installer...%b\n" "$GREEN" "$NC"
./installer
# Cleanup
cd - > /dev/null
rm -rf "$TEMP_DIR"
cd - >/dev/null
rm -rf "$TEMP_DIR"

View File

@@ -0,0 +1,332 @@
package clipboard
import (
"fmt"
"io"
"os"
"os/exec"
"syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
func Copy(data []byte, mimeType string) error {
return CopyOpts(data, mimeType, false, false)
}
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if !foreground {
return copyFork(data, mimeType, pasteOnce)
}
return copyServe(data, mimeType, pasteOnce)
}
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground"}
if pasteOnce {
args = append(args, "--paste-once")
}
args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
if _, err := stdin.Write(data); err != nil {
stdin.Close()
return fmt.Errorf("write stdin: %w", err)
}
stdin.Close()
return nil
}
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
ctx := 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(ctx)
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(ctx)
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()
source, err := dataControlMgr.CreateDataSource()
if err != nil {
return fmt.Errorf("create data source: %w", err)
}
if err := source.Offer(mimeType); err != nil {
return fmt.Errorf("offer mime type: %w", err)
}
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
if err := source.Offer("text/plain"); err != nil {
return fmt.Errorf("offer text/plain: %w", err)
}
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
}
if err := source.Offer("UTF8_STRING"); err != nil {
return fmt.Errorf("offer UTF8_STRING: %w", err)
}
if err := source.Offer("STRING"); err != nil {
return fmt.Errorf("offer STRING: %w", err)
}
if err := source.Offer("TEXT"); err != nil {
return fmt.Errorf("offer TEXT: %w", err)
}
}
cancelled := make(chan struct{})
pasted := make(chan struct{}, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
file.Write(data)
select {
case pasted <- struct{}{}:
default:
}
})
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
close(cancelled)
})
if err := device.SetSelection(source); err != nil {
return fmt.Errorf("set selection: %w", err)
}
display.Roundtrip()
for {
select {
case <-cancelled:
return nil
case <-pasted:
if pasteOnce {
return nil
}
default:
if err := ctx.Dispatch(); err != nil {
return nil
}
}
}
}
func CopyText(text string) error {
return Copy([]byte(text), "text/plain;charset=utf-8")
}
func Paste() ([]byte, string, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, "", fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return nil, "", 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(ctx)
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(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return nil, "", fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return nil, "", 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)
})
})
var selectionOffer *ext_data_control.ExtDataControlOfferV1
gotSelection := false
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
selectionOffer = e.Id
gotSelection = true
})
display.Roundtrip()
display.Roundtrip()
if !gotSelection || selectionOffer == nil {
return nil, "", fmt.Errorf("no clipboard data")
}
mimeTypes := offerMimeTypes[selectionOffer]
selectedMime := selectPreferredMimeType(mimeTypes)
if selectedMime == "" {
return nil, "", fmt.Errorf("no supported mime type")
}
r, w, err := os.Pipe()
if err != nil {
return nil, "", fmt.Errorf("create pipe: %w", err)
}
defer r.Close()
if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil {
w.Close()
return nil, "", fmt.Errorf("receive: %w", err)
}
w.Close()
display.Roundtrip()
data, err := io.ReadAll(r)
if err != nil {
return nil, "", fmt.Errorf("read: %w", err)
}
return data, selectedMime, nil
}
func PasteText() (string, error) {
data, _, err := Paste()
if err != nil {
return "", err
}
return string(data), nil
}
func selectPreferredMimeType(mimes []string) string {
preferred := []string{
"text/plain;charset=utf-8",
"text/plain",
"UTF8_STRING",
"STRING",
"TEXT",
"image/png",
"image/jpeg",
}
for _, pref := range preferred {
for _, mime := range mimes {
if mime == pref {
return mime
}
}
}
if len(mimes) > 0 {
return mimes[0]
}
return ""
}
func IsImageMimeType(mime string) bool {
return len(mime) > 6 && mime[:6] == "image/"
}

View File

@@ -0,0 +1,248 @@
package clipboard
import (
"bytes"
"encoding/binary"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
"path/filepath"
"strings"
"time"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
"hash/fnv"
bolt "go.etcd.io/bbolt"
)
type StoreConfig struct {
MaxHistory int
MaxEntrySize int64
}
func DefaultStoreConfig() StoreConfig {
return StoreConfig{
MaxHistory: 100,
MaxEntrySize: 5 * 1024 * 1024,
}
}
type Entry struct {
ID uint64
Data []byte
MimeType string
Preview string
Size int
Timestamp time.Time
IsImage bool
Hash uint64
}
func Store(data []byte, mimeType string) error {
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
}
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
if len(data) == 0 {
return nil
}
if int64(len(data)) > cfg.MaxEntrySize {
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
}
dbPath, err := GetDBPath()
if err != nil {
return fmt.Errorf("get db path: %w", err)
}
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return fmt.Errorf("open db: %w", err)
}
defer db.Close()
entry := Entry{
Data: data,
MimeType: mimeType,
Size: len(data),
Timestamp: time.Now(),
IsImage: IsImageMimeType(mimeType),
Hash: computeHash(data),
}
switch {
case entry.IsImage:
entry.Preview = imagePreview(data, mimeType)
default:
entry.Preview = textPreview(data)
}
return db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
if err != nil {
return err
}
if err := deduplicateInTx(b, entry.Hash); err != nil {
return err
}
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 trimLengthInTx(b, cfg.MaxHistory)
})
}
func GetDBPath() (string, error) {
cacheDir, err := os.UserCacheDir()
if err != nil {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
cacheDir = filepath.Join(homeDir, ".cache")
}
newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
newPath := filepath.Join(newDir, "db")
if _, err := os.Stat(newPath); err == nil {
return newPath, nil
}
oldDir := filepath.Join(cacheDir, "dms-clipboard")
oldPath := filepath.Join(oldDir, "db")
if _, err := os.Stat(oldPath); err == nil {
if err := os.MkdirAll(newDir, 0700); err != nil {
return "", err
}
if err := os.Rename(oldPath, newPath); err != nil {
return "", err
}
os.Remove(oldDir)
return newPath, nil
}
if err := os.MkdirAll(newDir, 0700); err != nil {
return "", err
}
return newPath, nil
}
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
if extractHash(v) != hash {
continue
}
if err := b.Delete(k); err != nil {
return err
}
}
return nil
}
func trimLengthInTx(b *bolt.Bucket, maxHistory int) error {
c := b.Cursor()
var count int
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
if count < maxHistory {
count++
continue
}
if err := b.Delete(k); err != nil {
return err
}
}
return nil
}
func encodeEntry(e Entry) ([]byte, error) {
buf := new(bytes.Buffer)
binary.Write(buf, binary.BigEndian, e.ID)
binary.Write(buf, binary.BigEndian, uint32(len(e.Data)))
buf.Write(e.Data)
binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType)))
buf.WriteString(e.MimeType)
binary.Write(buf, binary.BigEndian, uint32(len(e.Preview)))
buf.WriteString(e.Preview)
binary.Write(buf, binary.BigEndian, int32(e.Size))
binary.Write(buf, binary.BigEndian, e.Timestamp.Unix())
if e.IsImage {
buf.WriteByte(1)
} else {
buf.WriteByte(0)
}
binary.Write(buf, binary.BigEndian, e.Hash)
return buf.Bytes(), nil
}
func itob(v uint64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, v)
return b
}
func computeHash(data []byte) uint64 {
h := fnv.New64a()
h.Write(data)
return h.Sum64()
}
func extractHash(data []byte) uint64 {
if len(data) < 8 {
return 0
}
return binary.BigEndian.Uint64(data[len(data)-8:])
}
func textPreview(data []byte) string {
text := string(data)
text = strings.TrimSpace(text)
text = strings.Join(strings.Fields(text), " ")
if len(text) > 100 {
return text[:100] + "…"
}
return text
}
func imagePreview(data []byte, format string) string {
config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format)
}
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
}
func sizeStr(size int) string {
units := []string{"B", "KiB", "MiB"}
var i int
fsize := float64(size)
for fsize >= 1024 && i < len(units)-1 {
fsize /= 1024
i++
}
return fmt.Sprintf("%.0f %s", fsize, units[i])
}

View File

@@ -0,0 +1,160 @@
package clipboard
import (
"context"
"fmt"
"io"
"os"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type ClipboardChange struct {
Data []byte
MimeType string
}
func Watch(ctx context.Context, callback func(data []byte, mimeType 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
}
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)
}()
})
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 && err != os.ErrDeadlineExceeded {
return fmt.Errorf("dispatch: %w", err)
}
}
}
}
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
ch := make(chan ClipboardChange, 16)
errCh := make(chan error, 1)
go func() {
defer close(ch)
err := Watch(ctx, func(data []byte, mimeType string) {
select {
case ch <- ClipboardChange{Data: data, MimeType: mimeType}:
default:
}
})
if err != nil && err != context.Canceled {
errCh <- err
}
close(errCh)
}()
time.Sleep(50 * time.Millisecond)
return ch, errCh
}

View File

@@ -0,0 +1,306 @@
package colorpicker
import (
"encoding/json"
"fmt"
"math"
"strings"
)
type Color struct {
R, G, B, A uint8
}
type OutputFormat int
const (
FormatHex OutputFormat = iota
FormatRGB
FormatHSL
FormatHSV
FormatCMYK
)
func ParseFormat(s string) OutputFormat {
switch strings.ToLower(s) {
case "rgb":
return FormatRGB
case "hsl":
return FormatHSL
case "hsv":
return FormatHSV
case "cmyk":
return FormatCMYK
default:
return FormatHex
}
}
func (c Color) ToHex(lowercase bool) string {
if lowercase {
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}
func (c Color) ToRGB() string {
return fmt.Sprintf("%d %d %d", c.R, c.G, c.B)
}
func (c Color) ToHSL() string {
h, s, l := rgbToHSL(c.R, c.G, c.B)
return fmt.Sprintf("%d %d%% %d%%", h, s, l)
}
func (c Color) ToHSV() string {
h, s, v := rgbToHSV(c.R, c.G, c.B)
return fmt.Sprintf("%d %d%% %d%%", h, s, v)
}
func (c Color) ToCMYK() string {
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
return fmt.Sprintf("%d%% %d%% %d%% %d%%", cy, m, y, k)
}
func (c Color) Format(format OutputFormat, lowercase bool, customFmt string) string {
if customFmt != "" {
return c.formatCustom(format, customFmt)
}
switch format {
case FormatRGB:
return c.ToRGB()
case FormatHSL:
return c.ToHSL()
case FormatHSV:
return c.ToHSV()
case FormatCMYK:
return c.ToCMYK()
default:
return c.ToHex(lowercase)
}
}
func (c Color) formatCustom(format OutputFormat, customFmt string) string {
switch format {
case FormatRGB:
return replaceArgs(customFmt, c.R, c.G, c.B)
case FormatHSL:
h, s, l := rgbToHSL(c.R, c.G, c.B)
return replaceArgs(customFmt, h, s, l)
case FormatHSV:
h, s, v := rgbToHSV(c.R, c.G, c.B)
return replaceArgs(customFmt, h, s, v)
case FormatCMYK:
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
return replaceArgs4(customFmt, cy, m, y, k)
default:
if strings.Contains(customFmt, "{0}") {
r := fmt.Sprintf("%02X", c.R)
g := fmt.Sprintf("%02X", c.G)
b := fmt.Sprintf("%02X", c.B)
return replaceArgsStr(customFmt, r, g, b)
}
return c.ToHex(false)
}
}
func replaceArgs[T any](format string, a, b, c T) string {
result := format
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
return result
}
func replaceArgs4[T any](format string, a, b, c, d T) string {
result := format
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
result = strings.ReplaceAll(result, "{3}", fmt.Sprintf("%v", d))
return result
}
func replaceArgsStr(format, a, b, c string) string {
result := format
result = strings.ReplaceAll(result, "{0}", a)
result = strings.ReplaceAll(result, "{1}", b)
result = strings.ReplaceAll(result, "{2}", c)
return result
}
func rgbToHSL(r, g, b uint8) (int, int, int) {
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
maxVal := math.Max(rf, math.Max(gf, bf))
minVal := math.Min(rf, math.Min(gf, bf))
l := (maxVal + minVal) / 2
if maxVal == minVal {
return 0, 0, int(math.Round(l * 100))
}
d := maxVal - minVal
var s float64
if l > 0.5 {
s = d / (2 - maxVal - minVal)
} else {
s = d / (maxVal + minVal)
}
var h float64
switch maxVal {
case rf:
h = (gf - bf) / d
if gf < bf {
h += 6
}
case gf:
h = (bf-rf)/d + 2
case bf:
h = (rf-gf)/d + 4
}
h /= 6
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(l * 100))
}
func rgbToHSV(r, g, b uint8) (int, int, int) {
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
maxVal := math.Max(rf, math.Max(gf, bf))
minVal := math.Min(rf, math.Min(gf, bf))
v := maxVal
d := maxVal - minVal
var s float64
if maxVal != 0 {
s = d / maxVal
}
if maxVal == minVal {
return 0, int(math.Round(s * 100)), int(math.Round(v * 100))
}
var h float64
switch maxVal {
case rf:
h = (gf - bf) / d
if gf < bf {
h += 6
}
case gf:
h = (bf-rf)/d + 2
case bf:
h = (rf-gf)/d + 4
}
h /= 6
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(v * 100))
}
func rgbToCMYK(r, g, b uint8) (int, int, int, int) {
if r == 0 && g == 0 && b == 0 {
return 0, 0, 0, 100
}
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
k := 1 - math.Max(rf, math.Max(gf, bf))
c := (1 - rf - k) / (1 - k)
m := (1 - gf - k) / (1 - k)
y := (1 - bf - k) / (1 - k)
return int(math.Round(c * 100)), int(math.Round(m * 100)), int(math.Round(y * 100)), int(math.Round(k * 100))
}
func (c Color) Luminance() float64 {
r := float64(c.R) / 255.0
g := float64(c.G) / 255.0
b := float64(c.B) / 255.0
if r <= 0.03928 {
r = r / 12.92
} else {
r = math.Pow((r+0.055)/1.055, 2.4)
}
if g <= 0.03928 {
g = g / 12.92
} else {
g = math.Pow((g+0.055)/1.055, 2.4)
}
if b <= 0.03928 {
b = b / 12.92
} else {
b = math.Pow((b+0.055)/1.055, 2.4)
}
return 0.2126*r + 0.7152*g + 0.0722*b
}
func (c Color) IsDark() bool {
return c.Luminance() < 0.179
}
type ColorJSON struct {
Hex string `json:"hex"`
RGB struct {
R int `json:"r"`
G int `json:"g"`
B int `json:"b"`
} `json:"rgb"`
HSL struct {
H int `json:"h"`
S int `json:"s"`
L int `json:"l"`
} `json:"hsl"`
HSV struct {
H int `json:"h"`
S int `json:"s"`
V int `json:"v"`
} `json:"hsv"`
CMYK struct {
C int `json:"c"`
M int `json:"m"`
Y int `json:"y"`
K int `json:"k"`
} `json:"cmyk"`
}
func (c Color) ToJSON() (string, error) {
h, s, l := rgbToHSL(c.R, c.G, c.B)
hv, sv, v := rgbToHSV(c.R, c.G, c.B)
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
data := ColorJSON{
Hex: c.ToHex(false),
}
data.RGB.R = int(c.R)
data.RGB.G = int(c.G)
data.RGB.B = int(c.B)
data.HSL.H = h
data.HSL.S = s
data.HSL.L = l
data.HSV.H = hv
data.HSV.S = sv
data.HSV.V = v
data.CMYK.C = cy
data.CMYK.M = m
data.CMYK.Y = y
data.CMYK.K = k
bytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return "", err
}
return string(bytes), nil
}

View File

@@ -0,0 +1,741 @@
package colorpicker
import (
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type Config struct {
Format OutputFormat
CustomFormat string
Lowercase bool
Autocopy bool
Notify bool
}
type Output struct {
wlOutput *client.Output
name string
globalName uint32
x, y int32
width int32
height int32
scale int32
fractionalScale float64
transform int32
}
type LayerSurface struct {
output *Output
state *SurfaceState
wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport
wlPool *client.ShmPool
wlBuffer *client.Buffer
bufferBusy bool
oldPool *client.ShmPool
oldBuffer *client.Buffer
scopyBuffer *client.Buffer
configured bool
hidden bool
}
type Picker struct {
config Config
display *client.Display
registry *client.Registry
ctx *client.Context
compositor *client.Compositor
shm *client.Shm
seat *client.Seat
pointer *client.Pointer
keyboard *client.Keyboard
layerShell *wlr_layer_shell.ZwlrLayerShellV1
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
viewporter *wp_viewporter.WpViewporter
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
outputs map[uint32]*Output
outputsMu sync.Mutex
surfaces []*LayerSurface
activeSurface *LayerSurface
running bool
pickedColor *Color
err error
}
func New(config Config) *Picker {
return &Picker{
config: config,
outputs: make(map[uint32]*Output),
}
}
func (p *Picker) Run() (*Color, error) {
if err := p.connect(); err != nil {
return nil, fmt.Errorf("wayland connect: %w", err)
}
defer p.cleanup()
if err := p.setupRegistry(); err != nil {
return nil, fmt.Errorf("registry setup: %w", err)
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
if p.screencopy == nil {
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
}
if p.layerShell == nil {
return nil, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
}
if p.seat == nil {
return nil, fmt.Errorf("no seat available")
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
// Extra roundtrip to ensure pointer/keyboard from seat capabilities are registered
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip after seat: %w", err)
}
if err := p.createSurfaces(); err != nil {
return nil, fmt.Errorf("create surfaces: %w", err)
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
p.running = true
for p.running {
if err := p.ctx.Dispatch(); err != nil {
p.err = err
break
}
p.checkDone()
}
if p.err != nil {
return nil, p.err
}
return p.pickedColor, nil
}
func (p *Picker) checkDone() {
for _, ls := range p.surfaces {
picked, cancelled := ls.state.IsDone()
switch {
case cancelled:
p.running = false
return
case picked:
color, ok := ls.state.PickColor()
if ok {
p.pickedColor = &color
}
p.running = false
return
}
}
}
func (p *Picker) connect() error {
display, err := client.Connect("")
if err != nil {
return err
}
p.display = display
p.ctx = display.Context()
return nil
}
func (p *Picker) roundtrip() error {
return wlhelpers.Roundtrip(p.display, p.ctx)
}
func (p *Picker) setupRegistry() error {
registry, err := p.display.GetRegistry()
if err != nil {
return err
}
p.registry = registry
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
p.handleGlobal(e)
})
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
p.outputsMu.Lock()
delete(p.outputs, e.Name)
p.outputsMu.Unlock()
})
return nil
}
func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.CompositorInterfaceName:
compositor := client.NewCompositor(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
p.compositor = compositor
}
case client.ShmInterfaceName:
shm := client.NewShm(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
p.shm = shm
}
case client.SeatInterfaceName:
seat := client.NewSeat(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
p.seat = seat
p.setupInput()
}
case client.OutputInterfaceName:
output := client.NewOutput(p.ctx)
version := min(e.Version, 4)
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
p.outputsMu.Lock()
p.outputs[e.Name] = &Output{
wlOutput: output,
globalName: e.Name,
scale: 1,
fractionalScale: 1.0,
}
p.outputsMu.Unlock()
p.setupOutputHandlers(e.Name, output)
}
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
version := min(e.Version, 4)
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
p.layerShell = layerShell
}
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
version := min(e.Version, 3)
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
p.screencopy = screencopy
}
case wp_viewporter.WpViewporterInterfaceName:
viewporter := wp_viewporter.NewWpViewporter(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, viewporter); err == nil {
p.viewporter = viewporter
}
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
p.shortcutsInhibitMgr = mgr
}
}
}
func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.x = e.X
o.y = e.Y
o.transform = int32(e.Transform)
}
p.outputsMu.Unlock()
})
output.SetModeHandler(func(e client.OutputModeEvent) {
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
return
}
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.width = e.Width
o.height = e.Height
}
p.outputsMu.Unlock()
})
output.SetScaleHandler(func(e client.OutputScaleEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.scale = e.Factor
o.fractionalScale = float64(e.Factor)
}
p.outputsMu.Unlock()
})
output.SetNameHandler(func(e client.OutputNameEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.name = e.Name
}
p.outputsMu.Unlock()
})
}
func (p *Picker) createSurfaces() error {
p.outputsMu.Lock()
outputs := make([]*Output, 0, len(p.outputs))
for _, o := range p.outputs {
outputs = append(outputs, o)
}
p.outputsMu.Unlock()
for _, output := range outputs {
ls, err := p.createLayerSurface(output)
if err != nil {
return fmt.Errorf("output %s: %w", output.name, err)
}
p.surfaces = append(p.surfaces, ls)
}
return nil
}
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
surface, err := p.compositor.CreateSurface()
if err != nil {
return nil, fmt.Errorf("create surface: %w", err)
}
layerSurf, err := p.layerShell.GetLayerSurface(
surface,
output.wlOutput,
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
"dms-colorpicker",
)
if err != nil {
return nil, fmt.Errorf("get layer surface: %w", err)
}
ls := &LayerSurface{
output: output,
state: NewSurfaceState(p.config.Format, p.config.Lowercase),
wlSurface: surface,
layerSurf: layerSurf,
hidden: true, // Start hidden, will show overlay when pointer enters
}
if p.viewporter != nil {
vp, err := p.viewporter.GetViewport(surface)
if err == nil {
ls.viewport = vp
}
}
if err := layerSurf.SetAnchor(
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
); err != nil {
log.Warn("failed to set layer anchor", "err", err)
}
if err := layerSurf.SetExclusiveZone(-1); err != nil {
log.Warn("failed to set exclusive zone", "err", err)
}
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
log.Warn("failed to set keyboard interactivity", "err", err)
}
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
if err := layerSurf.AckConfigure(e.Serial); err != nil {
log.Warn("failed to ack configure", "err", err)
}
if err := ls.state.OnLayerConfigure(int(e.Width), int(e.Height)); err != nil {
log.Warn("failed to handle layer configure", "err", err)
}
ls.configured = true
scale := p.computeSurfaceScale(ls)
ls.state.SetScale(scale)
if !ls.state.IsReady() {
p.captureForSurface(ls)
} else {
p.redrawSurface(ls)
}
// Request shortcut inhibition once surface is configured
p.ensureShortcutsInhibitor(ls)
})
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
p.running = false
})
if err := surface.Commit(); err != nil {
log.Warn("failed to commit surface", "err", err)
}
return ls, nil
}
func (p *Picker) computeSurfaceScale(ls *LayerSurface) int32 {
out := ls.output
if out == nil || out.scale <= 0 {
return 1
}
return out.scale
}
func (p *Picker) ensureShortcutsInhibitor(ls *LayerSurface) {
if p.shortcutsInhibitMgr == nil || p.seat == nil || p.shortcutsInhibitor != nil {
return
}
inhibitor, err := p.shortcutsInhibitMgr.InhibitShortcuts(ls.wlSurface, p.seat)
if err != nil {
log.Debug("failed to create shortcuts inhibitor", "err", err)
return
}
p.shortcutsInhibitor = inhibitor
inhibitor.SetActiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1ActiveEvent) {
log.Debug("shortcuts inhibitor active")
})
inhibitor.SetInactiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1InactiveEvent) {
log.Debug("shortcuts inhibitor deactivated by compositor")
})
}
func (p *Picker) captureForSurface(ls *LayerSurface) {
frame, err := p.screencopy.CaptureOutput(0, ls.output.wlOutput)
if err != nil {
return
}
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
if err := ls.state.OnScreencopyBuffer(PixelFormat(e.Format), int(e.Width), int(e.Height), int(e.Stride)); err != nil {
log.Error("failed to create screencopy buffer", "err", err)
}
})
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
screenBuf := ls.state.ScreenBuffer()
if screenBuf == nil {
return
}
pool, err := p.shm.CreatePool(screenBuf.Fd(), int32(screenBuf.Size()))
if err != nil {
return
}
wlBuffer, err := pool.CreateBuffer(0, int32(screenBuf.Width), int32(screenBuf.Height), int32(screenBuf.Stride), uint32(ls.state.screenFormat))
if err != nil {
pool.Destroy()
return
}
if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy()
}
ls.scopyBuffer = wlBuffer
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {})
if err := frame.Copy(wlBuffer); err != nil {
log.Error("failed to copy frame", "err", err)
}
pool.Destroy()
})
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
ls.state.OnScreencopyFlags(e.Flags)
})
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
ls.state.OnScreencopyReady()
screenBuf := ls.state.ScreenBuffer()
if screenBuf != nil && ls.output.transform != TransformNormal {
invTransform := InverseTransform(ls.output.transform)
transformed, err := screenBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != screenBuf {
ls.state.ReplaceScreenBuffer(transformed)
}
}
logicalW, _ := ls.state.LogicalSize()
screenBuf = ls.state.ScreenBuffer()
if logicalW > 0 && screenBuf != nil {
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
}
scale := p.computeSurfaceScale(ls)
ls.state.SetScale(scale)
frame.Destroy()
p.redrawSurface(ls)
})
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
frame.Destroy()
})
}
func (p *Picker) redrawSurface(ls *LayerSurface) {
var renderBuf *ShmBuffer
if ls.hidden {
renderBuf = ls.state.RedrawScreenOnly()
} else {
renderBuf = ls.state.Redraw()
}
if renderBuf == nil {
return
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
ls.oldBuffer = nil
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
ls.oldPool = nil
}
ls.oldPool = ls.wlPool
ls.oldBuffer = ls.wlBuffer
ls.wlPool = nil
ls.wlBuffer = nil
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if err != nil {
return
}
ls.wlPool = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffer = wlBuffer
lsRef := ls
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
lsRef.bufferBusy = false
})
ls.bufferBusy = true
logicalW, logicalH := ls.state.LogicalSize()
if logicalW == 0 || logicalH == 0 {
logicalW = int(ls.output.width)
logicalH = int(ls.output.height)
}
if ls.viewport != nil {
_ = ls.wlSurface.SetBufferScale(1)
_ = ls.viewport.SetSource(0, 0, float64(renderBuf.Width), float64(renderBuf.Height))
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
} else {
bufferScale := ls.output.scale
if bufferScale <= 0 {
bufferScale = 1
}
_ = ls.wlSurface.SetBufferScale(bufferScale)
}
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
_ = ls.wlSurface.Commit()
ls.state.SwapBuffers()
}
func (p *Picker) hideSurface(ls *LayerSurface) {
if ls == nil || ls.wlSurface == nil || ls.hidden {
return
}
ls.hidden = true
// Redraw without the crosshair overlay
p.redrawSurface(ls)
}
func (p *Picker) setupInput() {
if p.seat == nil {
return
}
p.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && p.pointer == nil {
pointer, err := p.seat.GetPointer()
if err != nil {
return
}
p.pointer = pointer
p.setupPointerHandlers()
}
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
keyboard, err := p.seat.GetKeyboard()
if err != nil {
return
}
p.keyboard = keyboard
p.setupKeyboardHandlers()
}
})
}
func (p *Picker) setupPointerHandlers() {
p.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
if err := p.pointer.SetCursor(e.Serial, nil, 0, 0); err != nil {
log.Debug("failed to hide cursor", "err", err)
}
if e.Surface == nil {
return
}
p.activeSurface = nil
surfaceID := e.Surface.ID()
for _, ls := range p.surfaces {
if ls.wlSurface.ID() == surfaceID {
p.activeSurface = ls
break
}
}
if p.activeSurface == nil {
return
}
if p.activeSurface.hidden {
p.activeSurface.hidden = false
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
})
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
if e.Surface == nil {
return
}
surfaceID := e.Surface.ID()
for _, ls := range p.surfaces {
if ls.wlSurface.ID() == surfaceID {
p.hideSurface(ls)
break
}
}
})
p.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
if p.activeSurface == nil {
return
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
})
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
if p.activeSurface == nil {
return
}
p.activeSurface.state.OnPointerButton(e.Button, e.State)
})
}
func (p *Picker) setupKeyboardHandlers() {
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
for _, ls := range p.surfaces {
ls.state.OnKey(e.Key, e.State)
}
})
}
func (p *Picker) cleanup() {
for _, ls := range p.surfaces {
if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy()
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
}
if ls.wlBuffer != nil {
ls.wlBuffer.Destroy()
}
if ls.wlPool != nil {
ls.wlPool.Destroy()
}
if ls.viewport != nil {
ls.viewport.Destroy()
}
if ls.layerSurf != nil {
ls.layerSurf.Destroy()
}
if ls.wlSurface != nil {
ls.wlSurface.Destroy()
}
if ls.state != nil {
ls.state.Destroy()
}
}
if p.shortcutsInhibitor != nil {
if err := p.shortcutsInhibitor.Destroy(); err != nil {
log.Debug("failed to destroy shortcuts inhibitor", "err", err)
}
p.shortcutsInhibitor = nil
}
if p.shortcutsInhibitMgr != nil {
if err := p.shortcutsInhibitMgr.Destroy(); err != nil {
log.Debug("failed to destroy shortcuts inhibit manager", "err", err)
}
p.shortcutsInhibitMgr = nil
}
if p.viewporter != nil {
p.viewporter.Destroy()
}
if p.screencopy != nil {
p.screencopy.Destroy()
}
if p.pointer != nil {
p.pointer.Release()
}
if p.keyboard != nil {
p.keyboard.Release()
}
if p.display != nil {
p.ctx.Close()
}
}

View File

@@ -0,0 +1,55 @@
package colorpicker
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
type ShmBuffer = shm.Buffer
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
)
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride)
}
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
}
func GetPixelColorWithFormat(buf *ShmBuffer, x, y int, format PixelFormat) Color {
if x < 0 || x >= buf.Width || y < 0 || y >= buf.Height {
return Color{}
}
data := buf.Data()
offset := y*buf.Stride + x*4
if offset+3 >= len(data) {
return Color{}
}
if format == FormatABGR8888 || format == FormatXBGR8888 {
return Color{
R: data[offset],
G: data[offset+1],
B: data[offset+2],
A: data[offset+3],
}
}
return Color{
B: data[offset],
G: data[offset+1],
R: data[offset+2],
A: data[offset+3],
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,314 @@
package colorpicker
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSurfaceState_ConcurrentPointerMotion(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 50
const iterations = 100
for i := range goroutines {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := range iterations {
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
}
}(i)
}
wg.Wait()
}
func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 30
const iterations = 100
for i := range goroutines / 2 {
wg.Add(1)
go func(id int) {
defer wg.Done()
for range iterations {
s.SetScale(int32(id%3 + 1))
}
}(i)
}
for range goroutines / 2 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
scale := s.Scale()
assert.GreaterOrEqual(t, scale, int32(1))
}
}()
}
wg.Wait()
}
func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 20
const iterations = 100
for i := range goroutines / 2 {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := range iterations {
_ = s.OnLayerConfigure(1920+id, 1080+j)
}
}(i)
}
for range goroutines / 2 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
w, h := s.LogicalSize()
_ = w
_ = h
}
}()
}
wg.Wait()
}
func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 30
const iterations = 100
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
s.OnPointerButton(0x110, 1)
}
}()
}
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
s.OnKey(1, 1)
}
}()
}
for range goroutines / 3 {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
picked, cancelled := s.IsDone()
_ = picked
_ = cancelled
}
}()
}
wg.Wait()
}
func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 20
const iterations = 100
for range goroutines {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
_ = s.IsReady()
}
}()
}
wg.Wait()
}
func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
var wg sync.WaitGroup
const goroutines = 20
const iterations = 100
for range goroutines {
wg.Add(1)
go func() {
defer wg.Done()
for range iterations {
s.SwapBuffers()
}
}()
}
wg.Wait()
}
func TestSurfaceState_ZeroScale(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
s.SetScale(0)
assert.Equal(t, int32(1), s.Scale())
}
func TestSurfaceState_NegativeScale(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
s.SetScale(-5)
assert.Equal(t, int32(1), s.Scale())
}
func TestSurfaceState_ZeroDimensionConfigure(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
err := s.OnLayerConfigure(0, 100)
assert.NoError(t, err)
err = s.OnLayerConfigure(100, 0)
assert.NoError(t, err)
err = s.OnLayerConfigure(-1, 100)
assert.NoError(t, err)
w, h := s.LogicalSize()
assert.Equal(t, 0, w)
assert.Equal(t, 0, h)
}
func TestSurfaceState_PickColorNilBuffer(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
color, ok := s.PickColor()
assert.False(t, ok)
assert.Equal(t, Color{}, color)
}
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.Redraw()
assert.Nil(t, buf)
}
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.RedrawScreenOnly()
assert.Nil(t, buf)
}
func TestSurfaceState_FrontRenderBufferNil(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.FrontRenderBuffer()
assert.Nil(t, buf)
}
func TestSurfaceState_ScreenBufferNil(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
buf := s.ScreenBuffer()
assert.Nil(t, buf)
}
func TestSurfaceState_DestroyMultipleTimes(t *testing.T) {
s := NewSurfaceState(FormatHex, false)
s.Destroy()
s.Destroy()
}
func TestClamp(t *testing.T) {
tests := []struct {
v, lo, hi, expected int
}{
{5, 0, 10, 5},
{-5, 0, 10, 0},
{15, 0, 10, 10},
{0, 0, 10, 0},
{10, 0, 10, 10},
}
for _, tt := range tests {
result := clamp(tt.v, tt.lo, tt.hi)
assert.Equal(t, tt.expected, result)
}
}
func TestClampF(t *testing.T) {
tests := []struct {
v, lo, hi, expected float64
}{
{5.0, 0.0, 10.0, 5.0},
{-5.0, 0.0, 10.0, 0.0},
{15.0, 0.0, 10.0, 10.0},
{0.0, 0.0, 10.0, 0.0},
{10.0, 0.0, 10.0, 10.0},
}
for _, tt := range tests {
result := clampF(tt.v, tt.lo, tt.hi)
assert.InDelta(t, tt.expected, result, 0.001)
}
}
func TestAbs(t *testing.T) {
tests := []struct {
v, expected int
}{
{5, 5},
{-5, 5},
{0, 0},
}
for _, tt := range tests {
result := abs(tt.v)
assert.Equal(t, tt.expected, result)
}
}
func TestBlendColors(t *testing.T) {
bg := Color{R: 0, G: 0, B: 0, A: 255}
fg := Color{R: 255, G: 255, B: 255, A: 255}
result := blendColors(bg, fg, 0.0)
assert.Equal(t, bg.R, result.R)
assert.Equal(t, bg.G, result.G)
assert.Equal(t, bg.B, result.B)
result = blendColors(bg, fg, 1.0)
assert.Equal(t, fg.R, result.R)
assert.Equal(t, fg.G, result.G)
assert.Equal(t, fg.B, result.B)
result = blendColors(bg, fg, 0.5)
assert.InDelta(t, 127, int(result.R), 1)
assert.InDelta(t, 127, int(result.G), 1)
assert.InDelta(t, 127, int(result.B), 1)
result = blendColors(bg, fg, -1.0)
assert.Equal(t, bg.R, result.R)
result = blendColors(bg, fg, 2.0)
assert.Equal(t, fg.R, result.R)
}

View File

@@ -46,11 +46,20 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context,
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
}
// DeployConfigurationsWithSystemd deploys configurations with systemd option
func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
}
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
}
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult
shouldReplaceConfig := func(configType string) bool {
@@ -64,7 +73,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
switch wm {
case deps.WindowManagerNiri:
if shouldReplaceConfig("Niri") {
result, err := cd.deployNiriConfig(terminal)
result, err := cd.deployNiriConfig(terminal, useSystemd)
results = append(results, result)
if err != nil {
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
@@ -72,7 +81,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
}
case deps.WindowManagerHyprland:
if shouldReplaceConfig("Hyprland") {
result, err := cd.deployHyprlandConfig(terminal)
result, err := cd.deployHyprlandConfig(terminal, useSystemd)
results = append(results, result)
if err != nil {
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
@@ -110,8 +119,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return results, nil
}
// deployNiriConfig handles Niri configuration deployment with backup and merging
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Niri",
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
@@ -123,6 +131,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
return result, result.Error
}
dmsDir := filepath.Join(configDir, "dms")
if err := os.MkdirAll(dmsDir, 0755); err != nil {
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
return result, result.Error
}
var existingConfig string
if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Niri configuration")
@@ -143,14 +157,6 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
}
// Detect polkit agent path
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
}
// Determine terminal command based on choice
var terminalCommand string
switch terminal {
case deps.TerminalGhostty:
@@ -160,15 +166,17 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
case deps.TerminalAlacritty:
terminalCommand = "alacritty"
default:
terminalCommand = "ghostty" // fallback to ghostty
terminalCommand = "ghostty"
}
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
}
// If there was an existing config, merge the output sections
if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig, dmsDir)
if err != nil {
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
} else {
@@ -182,11 +190,45 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
return result, result.Error
}
if err := cd.deployNiriDmsConfigs(dmsDir, terminalCommand); err != nil {
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
return result, result.Error
}
result.Deployed = true
cd.log("Successfully deployed Niri configuration")
return result, nil
}
func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) error {
configs := []struct {
name string
content string
}{
{"colors.kdl", NiriColorsConfig},
{"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"outputs.kdl", ""},
{"cursor.kdl", ""},
}
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
// Skip if file already exists to preserve user modifications
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
continue
}
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
}
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
}
return nil
}
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
var results []DeploymentResult
@@ -230,7 +272,13 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
colorResult := DeploymentResult{
ConfigType: "Ghostty Colors",
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "themes", "dankcolors"),
}
themesDir := filepath.Dir(colorResult.Path)
if err := os.MkdirAll(themesDir, 0755); err != nil {
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
@@ -375,59 +423,31 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
return results, nil
}
// detectPolkitAgent tries to find the polkit authentication agent on the system
// Prioritizes mate-polkit paths since that's what we install
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
// Prioritize mate-polkit paths first
matePaths := []string{
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
}
for _, path := range matePaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
return path, nil
}
}
// Fallback to other polkit agents if mate-polkit is not found
fallbackPaths := []string{
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
"/usr/libexec/polkit-gnome-authentication-agent-1",
}
for _, path := range fallbackPaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
return path, nil
}
}
return "", fmt.Errorf("no polkit agent found in common locations")
}
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones)
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
// Find all output sections in the existing config
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
if len(existingOutputs) == 0 {
// No output sections to merge
return newConfig, nil
}
// Remove the example output section from the new config
outputsPath := filepath.Join(dmsDir, "outputs.kdl")
if _, err := os.Stat(outputsPath); err != nil {
var outputsContent strings.Builder
for _, output := range existingOutputs {
outputsContent.WriteString(output)
outputsContent.WriteString("\n\n")
}
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0644); err != nil {
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
} else {
cd.log("Migrated output sections to dms/outputs.kdl")
}
}
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
// Find where to insert the output sections (after the input section)
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
@@ -435,7 +455,6 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
return "", fmt.Errorf("could not find insertion point for output sections")
}
// Insert after the first closing brace (end of input section)
insertPos := inputMatches[0][1]
var builder strings.Builder
@@ -453,7 +472,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
}
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Hyprland",
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
@@ -465,6 +484,12 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
return result, result.Error
}
dmsDir := filepath.Join(configDir, "dms")
if err := os.MkdirAll(dmsDir, 0755); err != nil {
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
return result, result.Error
}
var existingConfig string
if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Hyprland configuration")
@@ -485,14 +510,6 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
}
// Detect polkit agent path
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
}
// Determine terminal command based on choice
var terminalCommand string
switch terminal {
case deps.TerminalGhostty:
@@ -502,15 +519,17 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
case deps.TerminalAlacritty:
terminalCommand = "alacritty"
default:
terminalCommand = "ghostty" // fallback to ghostty
terminalCommand = "ghostty"
}
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
}
// If there was an existing config, merge the monitor sections
if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir)
if err != nil {
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
} else {
@@ -524,31 +543,68 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
return result, result.Error
}
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
return result, result.Error
}
result.Deployed = true
cd.log("Successfully deployed Hyprland configuration")
return result, nil
}
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match monitor lines (including commented ones)
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
// Also matches commented versions: # monitor = ...
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
configs := []struct {
name string
content string
}{
{"colors.conf", HyprColorsConfig},
{"layout.conf", HyprLayoutConfig},
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"outputs.conf", ""},
{"cursor.conf", ""},
}
// Find all monitor lines in the existing config
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
continue
}
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
}
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
}
return nil
}
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
if len(existingMonitors) == 0 {
// No monitor sections to merge
return newConfig, nil
}
// Remove the example monitor line from the new config
outputsPath := filepath.Join(dmsDir, "outputs.conf")
if _, err := os.Stat(outputsPath); err != nil {
var outputsContent strings.Builder
for _, monitor := range existingMonitors {
outputsContent.WriteString(monitor)
outputsContent.WriteString("\n")
}
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0644); err != nil {
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
} else {
cd.log("Migrated monitor sections to dms/outputs.conf")
}
}
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
@@ -556,8 +612,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return "", fmt.Errorf("could not find MONITOR CONFIG section")
}
// Insert after the header
insertPos := headerMatch[1] + 1 // +1 for the newline
insertPos := headerMatch[1] + 1
var builder strings.Builder
builder.WriteString(mergedConfig[:insertPos])
@@ -572,3 +627,70 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return builder.String(), nil
}
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
lines := strings.Split(config, "\n")
var result []string
startupSectionFound := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
continue
}
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true
result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
continue
}
result = append(result, line)
}
if !startupSectionFound {
for i, line := range result {
if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{
"exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
}
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
break
}
}
}
return strings.Join(result, "\n")
}
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
TERMINAL "%s"
}`, terminalCommand)
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
spawnDms := `spawn-at-startup "dms" "run"`
if !strings.Contains(config, spawnDms) {
// Insert spawn-at-startup for dms after the environment block
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
config = config[:loc[1]] + "\n" + spawnDms + config[loc[1]:]
}
}
return config
}

View File

@@ -3,7 +3,6 @@ package config
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -11,23 +10,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestDetectPolkitAgent(t *testing.T) {
cd := &ConfigDeployer{}
// This test depends on the system having a polkit agent installed
// We'll just test that the function doesn't crash and returns some path or error
path, err := cd.detectPolkitAgent()
if err != nil {
// If no polkit agent is found, that's okay for testing
assert.Contains(t, err.Error(), "no polkit agent found")
} else {
// If found, it should be a valid path
assert.NotEmpty(t, path)
assert.True(t, strings.Contains(path, "polkit"))
}
}
func TestMergeNiriOutputSections(t *testing.T) {
cd := &ConfigDeployer{}
@@ -179,7 +161,8 @@ layout {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig)
tmpDir := t.TempDir()
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig, tmpDir)
if tt.wantError {
assert.Error(t, err)
@@ -272,17 +255,6 @@ func getGhosttyPath() string {
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
}
func TestPolkitPathInjection(t *testing.T) {
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
other content`
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
}
func TestMergeHyprlandMonitorSections(t *testing.T) {
cd := &ConfigDeployer{}
@@ -391,7 +363,8 @@ input {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig)
tmpDir := t.TempDir()
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
if tt.wantError {
assert.Error(t, err)
@@ -424,7 +397,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
cd := NewConfigDeployer(logChan)
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType)
@@ -435,7 +408,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
content, err := os.ReadFile(result.Path)
require.NoError(t, err)
assert.Contains(t, string(content), "# MONITOR CONFIG")
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
assert.Contains(t, string(content), "source = ./dms/binds.conf")
assert.Contains(t, string(content), "exec-once = ")
})
@@ -454,7 +427,7 @@ general {
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
require.NoError(t, err)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType)
@@ -471,7 +444,7 @@ general {
require.NoError(t, err)
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
assert.NotContains(t, string(newContent), "monitor = eDP-2")
})
}
@@ -479,29 +452,22 @@ general {
func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriConfig, "binds {")
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
assert.Contains(t, NiriBindsConfig, "binds {")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
}
func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
assert.Contains(t, HyprlandConfig, "# ENVIRONMENT VARS")
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, HyprlandConfig, "{{TERMINAL_COMMAND}}")
assert.Contains(t, HyprlandConfig, "exec-once = dms run")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec,")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
}
func TestGhosttyConfigStructure(t *testing.T) {
assert.Contains(t, GhosttyConfig, "window-decoration = false")
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
assert.Contains(t, GhosttyConfig, "theme = dankcolors")
}
func TestGhosttyColorConfigStructure(t *testing.T) {

View File

@@ -7,29 +7,33 @@ import (
"strings"
)
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
func LocateDMSConfig() (string, error) {
var primaryPaths []string
configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(homeDir, ".config")
}
}
if configHome != "" {
configHome, err := os.UserConfigDir()
if err == nil && configHome != "" {
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
}
primaryPaths = append(primaryPaths, "/usr/share/quickshell/dms")
// System data directories
dataDirs := os.Getenv("XDG_DATA_DIRS")
if dataDirs == "" {
dataDirs = "/usr/local/share:/usr/share"
}
for dir := range strings.SplitSeq(dataDirs, ":") {
if dir != "" {
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
}
}
// System config directories (fallback)
configDirs := os.Getenv("XDG_CONFIG_DIRS")
if configDirs == "" {
configDirs = "/etc/xdg"
}
for _, dir := range strings.Split(configDirs, ":") {
for dir := range strings.SplitSeq(configDirs, ":") {
if dir != "" {
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
}

View File

@@ -48,4 +48,4 @@ keybind = shift+enter=text:\n
gtk-single-instance = true
# Dank color generation
config-file = ./config-dankcolors
theme = dankcolors

View File

@@ -0,0 +1,156 @@
# === Application Launchers ===
bind = SUPER, T, exec, {{TERMINAL_COMMAND}}
bind = SUPER, space, exec, dms ipc call spotlight toggle
bind = SUPER, V, exec, dms ipc call clipboard toggle
bind = SUPER, M, exec, dms ipc call processlist focusOrToggle
bind = SUPER, comma, exec, dms ipc call settings focusOrToggle
bind = SUPER, N, exec, dms ipc call notifications toggle
bind = SUPER SHIFT, N, exec, dms ipc call notepad toggle
bind = SUPER, Y, exec, dms ipc call dankdash wallpaper
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
bind = SUPER, X, exec, dms ipc call powermenu toggle
# === Cheat sheet
bind = SUPER SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
# === Security ===
bind = SUPER ALT, L, exec, dms ipc call lock lock
bind = SUPER SHIFT, E, exit
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
# === Audio Controls ===
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
bindl = , XF86AudioMute, exec, dms ipc call audio mute
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
bindl = , XF86AudioNext, exec, dms ipc call mpris next
# === Brightness Controls ===
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
# === Window Management ===
bind = SUPER, Q, killactive
bind = SUPER, F, fullscreen, 1
bind = SUPER SHIFT, F, fullscreen, 0
bind = SUPER SHIFT, T, togglefloating
bind = SUPER, W, togglegroup
# === Focus Navigation ===
bind = SUPER, left, movefocus, l
bind = SUPER, down, movefocus, d
bind = SUPER, up, movefocus, u
bind = SUPER, right, movefocus, r
bind = SUPER, H, movefocus, l
bind = SUPER, J, movefocus, d
bind = SUPER, K, movefocus, u
bind = SUPER, L, movefocus, r
# === Window Movement ===
bind = SUPER SHIFT, left, movewindow, l
bind = SUPER SHIFT, down, movewindow, d
bind = SUPER SHIFT, up, movewindow, u
bind = SUPER SHIFT, right, movewindow, r
bind = SUPER SHIFT, H, movewindow, l
bind = SUPER SHIFT, J, movewindow, d
bind = SUPER SHIFT, K, movewindow, u
bind = SUPER SHIFT, L, movewindow, r
# === Column Navigation ===
bind = SUPER, Home, focuswindow, first
bind = SUPER, End, focuswindow, last
# === Monitor Navigation ===
bind = SUPER CTRL, left, focusmonitor, l
bind = SUPER CTRL, right, focusmonitor, r
bind = SUPER CTRL, H, focusmonitor, l
bind = SUPER CTRL, J, focusmonitor, d
bind = SUPER CTRL, K, focusmonitor, u
bind = SUPER CTRL, L, focusmonitor, r
# === Move to Monitor ===
bind = SUPER SHIFT CTRL, left, movewindow, mon:l
bind = SUPER SHIFT CTRL, down, movewindow, mon:d
bind = SUPER SHIFT CTRL, up, movewindow, mon:u
bind = SUPER SHIFT CTRL, right, movewindow, mon:r
bind = SUPER SHIFT CTRL, H, movewindow, mon:l
bind = SUPER SHIFT CTRL, J, movewindow, mon:d
bind = SUPER SHIFT CTRL, K, movewindow, mon:u
bind = SUPER SHIFT CTRL, L, movewindow, mon:r
# === Workspace Navigation ===
bind = SUPER, Page_Down, workspace, e+1
bind = SUPER, Page_Up, workspace, e-1
bind = SUPER, U, workspace, e+1
bind = SUPER, I, workspace, e-1
bind = SUPER CTRL, down, movetoworkspace, e+1
bind = SUPER CTRL, up, movetoworkspace, e-1
bind = SUPER CTRL, U, movetoworkspace, e+1
bind = SUPER CTRL, I, movetoworkspace, e-1
# === Move Workspaces ===
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
bind = SUPER SHIFT, U, movetoworkspace, e+1
bind = SUPER SHIFT, I, movetoworkspace, e-1
# === Mouse Wheel Navigation ===
bind = SUPER, mouse_down, workspace, e+1
bind = SUPER, mouse_up, workspace, e-1
bind = SUPER CTRL, mouse_down, movetoworkspace, e+1
bind = SUPER CTRL, mouse_up, movetoworkspace, e-1
# === Numbered Workspaces ===
bind = SUPER, 1, workspace, 1
bind = SUPER, 2, workspace, 2
bind = SUPER, 3, workspace, 3
bind = SUPER, 4, workspace, 4
bind = SUPER, 5, workspace, 5
bind = SUPER, 6, workspace, 6
bind = SUPER, 7, workspace, 7
bind = SUPER, 8, workspace, 8
bind = SUPER, 9, workspace, 9
# === Move to Numbered Workspaces ===
bind = SUPER SHIFT, 1, movetoworkspace, 1
bind = SUPER SHIFT, 2, movetoworkspace, 2
bind = SUPER SHIFT, 3, movetoworkspace, 3
bind = SUPER SHIFT, 4, movetoworkspace, 4
bind = SUPER SHIFT, 5, movetoworkspace, 5
bind = SUPER SHIFT, 6, movetoworkspace, 6
bind = SUPER SHIFT, 7, movetoworkspace, 7
bind = SUPER SHIFT, 8, movetoworkspace, 8
bind = SUPER SHIFT, 9, movetoworkspace, 9
# === Column Management ===
bind = SUPER, bracketleft, layoutmsg, preselect l
bind = SUPER, bracketright, layoutmsg, preselect r
# === Sizing & Layout ===
bind = SUPER, R, layoutmsg, togglesplit
bind = SUPER CTRL, F, resizeactive, exact 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = SUPER, mouse:272, Move window, movewindow
bindmd = SUPER, mouse:273, Resize window, resizewindow
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindd = SUPER, code:20, Expand window left, resizeactive, -100 0
bindd = SUPER, code:21, Shrink window left, resizeactive, 100 0
# === Manual Sizing ===
binde = SUPER, minus, resizeactive, -10% 0
binde = SUPER, equal, resizeactive, 10% 0
binde = SUPER SHIFT, minus, resizeactive, 0 -10%
binde = SUPER SHIFT, equal, resizeactive, 0 10%
# === Screenshots ===
bind = , Print, exec, dms screenshot
bind = CTRL, Print, exec, dms screenshot full
bind = ALT, Print, exec, dms screenshot window
# === System Controls ===
bind = SUPER SHIFT, P, dpms, toggle

View File

@@ -0,0 +1,25 @@
# ! Auto-generated file. Do not edit directly.
# Remove source = ./dms/colors.conf from your config to override.
$primary = rgb(d0bcff)
$outline = rgb(948f99)
$error = rgb(f2b8b5)
general {
col.active_border = $primary
col.inactive_border = $outline
}
group {
col.border_active = $primary
col.border_inactive = $outline
col.border_locked_active = $error
col.border_locked_inactive = $outline
groupbar {
col.active = $primary
col.inactive = $outline
col.locked_active = $error
col.locked_inactive = $outline
}
}

View File

@@ -0,0 +1,11 @@
# Auto-generated by DMS - do not edit manually
general {
gaps_in = 4
gaps_out = 4
border_size = 2
}
decoration {
rounding = 12
}

View File

@@ -7,21 +7,11 @@
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
monitor = , preferred,auto,auto
# ==================
# ENVIRONMENT VARS
# ==================
env = QT_QPA_PLATFORM,wayland
env = ELECTRON_OZONE_PLATFORM_HINT,auto
env = QT_QPA_PLATFORMTHEME,gtk3
env = QT_QPA_PLATFORMTHEME_QT6,gtk3
env = TERMINAL,{{TERMINAL_COMMAND}}
# ==================
# STARTUP APPS
# ==================
exec-once = bash -c "wl-paste --watch cliphist store &"
exec-once = dms run
exec-once = {{POLKIT_AGENT_PATH}}
exec-once = dbus-update-activation-environment --systemd --all
exec-once = systemctl --user start hyprland-session.target
# ==================
# INPUT CONFIG
@@ -37,10 +27,7 @@ input {
general {
gaps_in = 5
gaps_out = 5
border_size = 0 # off in niri
col.active_border = rgba(707070ff)
col.inactive_border = rgba(d0d0d0ff)
border_size = 2
layout = dwindle
}
@@ -52,7 +39,7 @@ decoration {
rounding = 12
active_opacity = 1.0
inactive_opacity = 0.9
inactive_opacity = 1.0
shadow {
enabled = true
@@ -100,191 +87,32 @@ misc {
# ==================
# WINDOW RULES
# ==================
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
windowrulev2 = rounding 12, class:^(org\.gnome\.)
windowrulev2 = noborder, class:^(org\.gnome\.)
windowrule = rounding 12, match:class ^(org\.gnome\.)
windowrulev2 = tile, class:^(gnome-control-center)$
windowrulev2 = tile, class:^(pavucontrol)$
windowrulev2 = tile, class:^(nm-connection-editor)$
windowrule = tile on, match:class ^(gnome-control-center)$
windowrule = tile on, match:class ^(pavucontrol)$
windowrule = tile on, match:class ^(nm-connection-editor)$
windowrulev2 = float, class:^(gnome-calculator)$
windowrulev2 = float, class:^(galculator)$
windowrulev2 = float, class:^(blueman-manager)$
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
windowrulev2 = float, class:^(steam)$
windowrulev2 = float, class:^(xdg-desktop-portal)$
windowrule = float on, match:class ^(gnome-calculator)$
windowrule = float on, match:class ^(galculator)$
windowrule = float on, match:class ^(blueman-manager)$
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
windowrule = float on, match:class ^(steam)$
windowrule = float on, match:class ^(xdg-desktop-portal)$
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
windowrulev2 = noborder, class:^(Alacritty)$
windowrulev2 = noborder, class:^(zen)$
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
windowrulev2 = noborder, class:^(kitty)$
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
windowrule = float on, match:class ^(zoom)$
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
windowrulev2 = float, class:^(zoom)$
# DMS windows floating by default
# ! Hyprland doesn't size these windows correctly so disabling by default here
# windowrule = float on, match:class ^(org.quickshell)$
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
layerrule = no_anim on, match:namespace ^(quickshell)$
layerrule = noanim, ^(quickshell)$
# ==================
# KEYBINDINGS
# ==================
$mod = SUPER
# === Application Launchers ===
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist toggle
bind = $mod, comma, exec, dms ipc call settings toggle
bind = $mod, N, exec, dms ipc call notifications toggle
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
# === Cheat sheet
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
# === Security ===
bind = $mod ALT, L, exec, dms ipc call lock lock
bind = $mod SHIFT, E, exit
bind = CTRL ALT, Delete, exec, dms ipc call processlist toggle
# === Audio Controls ===
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
bindl = , XF86AudioMute, exec, dms ipc call audio mute
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
# === Brightness Controls ===
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
# === Window Management ===
bind = $mod, Q, killactive
bind = $mod, F, fullscreen, 1
bind = $mod SHIFT, F, fullscreen, 0
bind = $mod SHIFT, T, togglefloating
bind = $mod, W, togglegroup
# === Focus Navigation ===
bind = $mod, left, movefocus, l
bind = $mod, down, movefocus, d
bind = $mod, up, movefocus, u
bind = $mod, right, movefocus, r
bind = $mod, H, movefocus, l
bind = $mod, J, movefocus, d
bind = $mod, K, movefocus, u
bind = $mod, L, movefocus, r
# === Window Movement ===
bind = $mod SHIFT, left, movewindow, l
bind = $mod SHIFT, down, movewindow, d
bind = $mod SHIFT, up, movewindow, u
bind = $mod SHIFT, right, movewindow, r
bind = $mod SHIFT, H, movewindow, l
bind = $mod SHIFT, J, movewindow, d
bind = $mod SHIFT, K, movewindow, u
bind = $mod SHIFT, L, movewindow, r
# === Column Navigation ===
bind = $mod, Home, focuswindow, first
bind = $mod, End, focuswindow, last
# === Monitor Navigation ===
bind = $mod CTRL, left, focusmonitor, l
bind = $mod CTRL, right, focusmonitor, r
bind = $mod CTRL, H, focusmonitor, l
bind = $mod CTRL, J, focusmonitor, d
bind = $mod CTRL, K, focusmonitor, u
bind = $mod CTRL, L, focusmonitor, r
# === Move to Monitor ===
bind = $mod SHIFT CTRL, left, movewindow, mon:l
bind = $mod SHIFT CTRL, down, movewindow, mon:d
bind = $mod SHIFT CTRL, up, movewindow, mon:u
bind = $mod SHIFT CTRL, right, movewindow, mon:r
bind = $mod SHIFT CTRL, H, movewindow, mon:l
bind = $mod SHIFT CTRL, J, movewindow, mon:d
bind = $mod SHIFT CTRL, K, movewindow, mon:u
bind = $mod SHIFT CTRL, L, movewindow, mon:r
# === Workspace Navigation ===
bind = $mod, Page_Down, workspace, e+1
bind = $mod, Page_Up, workspace, e-1
bind = $mod, U, workspace, e+1
bind = $mod, I, workspace, e-1
bind = $mod CTRL, down, movetoworkspace, e+1
bind = $mod CTRL, up, movetoworkspace, e-1
bind = $mod CTRL, U, movetoworkspace, e+1
bind = $mod CTRL, I, movetoworkspace, e-1
# === Move Workspaces ===
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
bind = $mod SHIFT, U, movetoworkspace, e+1
bind = $mod SHIFT, I, movetoworkspace, e-1
# === Mouse Wheel Navigation ===
bind = $mod, mouse_down, workspace, e+1
bind = $mod, mouse_up, workspace, e-1
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
# === Numbered Workspaces ===
bind = $mod, 1, workspace, 1
bind = $mod, 2, workspace, 2
bind = $mod, 3, workspace, 3
bind = $mod, 4, workspace, 4
bind = $mod, 5, workspace, 5
bind = $mod, 6, workspace, 6
bind = $mod, 7, workspace, 7
bind = $mod, 8, workspace, 8
bind = $mod, 9, workspace, 9
# === Move to Numbered Workspaces ===
bind = $mod SHIFT, 1, movetoworkspace, 1
bind = $mod SHIFT, 2, movetoworkspace, 2
bind = $mod SHIFT, 3, movetoworkspace, 3
bind = $mod SHIFT, 4, movetoworkspace, 4
bind = $mod SHIFT, 5, movetoworkspace, 5
bind = $mod SHIFT, 6, movetoworkspace, 6
bind = $mod SHIFT, 7, movetoworkspace, 7
bind = $mod SHIFT, 8, movetoworkspace, 8
bind = $mod SHIFT, 9, movetoworkspace, 9
# === Column Management ===
bind = $mod, bracketleft, layoutmsg, preselect l
bind = $mod, bracketright, layoutmsg, preselect r
# === Sizing & Layout ===
bind = $mod, R, layoutmsg, togglesplit
bind = $mod CTRL, F, resizeactive, exact 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = $mod, mouse:272, Move window, movewindow
bindmd = $mod, mouse:273, Resize window, resizewindow
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
# === Manual Sizing ===
binde = $mod, minus, resizeactive, -10% 0
binde = $mod, equal, resizeactive, 10% 0
binde = $mod SHIFT, minus, resizeactive, 0 -10%
binde = $mod SHIFT, equal, resizeactive, 0 10%
# === Screenshots ===
bind = , XF86Launch1, exec, grimblast copy area
bind = CTRL, XF86Launch1, exec, grimblast copy screen
bind = ALT, XF86Launch1, exec, grimblast copy active
bind = , Print, exec, grimblast copy area
bind = CTRL, Print, exec, grimblast copy screen
bind = ALT, Print, exec, grimblast copy active
# === System Controls ===
bind = $mod SHIFT, P, dpms, off
source = ./dms/colors.conf
source = ./dms/outputs.conf
source = ./dms/layout.conf
source = ./dms/cursor.conf
source = ./dms/binds.conf

View File

@@ -0,0 +1,10 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
recent-windows {
highlight {
corner-radius 12
}
}

View File

@@ -0,0 +1,209 @@
binds {
// === System & Overview ===
Mod+D repeat=false { toggle-overview; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
XF86AudioPause allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPlay allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPrev allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "previous";
}
XF86AudioNext allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "next";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
}

View File

@@ -0,0 +1,39 @@
// ! Auto-generated file. Do not edit directly.
// Remove `include "dms/colors.kdl"` from your config to override.
layout {
background-color "transparent"
focus-ring {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
border {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
shadow {
color "#00000070"
}
tab-indicator {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
insert-hint {
color "#d0bcff80"
}
}
recent-windows {
highlight {
active-color "#4f378b"
urgent-color "#f2b8b5"
}
}

View File

@@ -0,0 +1,22 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout {
gaps 4
border {
width 2
}
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius 12
clip-to-geometry true
tiled-state true
draw-border-with-background false
}

View File

@@ -18,15 +18,64 @@ gestures {
input {
keyboard {
xkb {
// You can set rules, model, layout, variant and options.
// For more information, see xkeyboard-config(7).
// For example:
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
// If this section is empty, niri will fetch xkb settings
// from org.freedesktop.locale1. You can control these using
// localectl set-x11-keymap.
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// scroll-button-lock
// middle-emulation
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
@@ -44,7 +93,6 @@ input {
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
gaps 5
background-color "transparent"
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
@@ -87,11 +135,6 @@ layout {
inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red
}
focus-ring {
width 2
active-color "#808080" // Medium gray
inactive-color "#505050" // Dark gray
}
shadow {
softness 30
spread 5
@@ -115,16 +158,8 @@ overview {
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
spawn-at-startup "dms" "run"
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
environment {
XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
TERMINAL "{{TERMINAL_COMMAND}}"
}
hotkey-overlay {
skip-at-startup
@@ -205,214 +240,34 @@ window-rule {
match app-id="kitty"
draw-border-with-background false
}
window-rule {
match is-active=false
opacity 0.9
}
window-rule {
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
match app-id="zoom"
open-floating true
}
// Open dms windows as floating by default
window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
binds {
// === System & Overview ===
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "toggle";
}
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "toggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "toggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
match app-id=r#"org.quickshell$"#
open-floating true
}
debug {
honor-xdg-activation-with-invalid-serial
}
// Override to disable super+tab
recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
Alt+grave { next-window filter="app-id"; }
Alt+Shift+grave { previous-window filter="app-id"; }
}
}
// Include dms files
include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"
include "dms/outputs.kdl"
include "dms/cursor.kdl"

Binary file not shown.

View File

@@ -4,3 +4,12 @@ import _ "embed"
//go:embed embedded/hyprland.conf
var HyprlandConfig string
//go:embed embedded/hypr-colors.conf
var HyprColorsConfig string
//go:embed embedded/hypr-layout.conf
var HyprLayoutConfig string
//go:embed embedded/hypr-binds.conf
var HyprBindsConfig string

View File

@@ -4,3 +4,15 @@ import _ "embed"
//go:embed embedded/niri.kdl
var NiriConfig string
//go:embed embedded/niri-colors.kdl
var NiriColorsConfig string
//go:embed embedded/niri-layout.kdl
var NiriLayoutConfig string
//go:embed embedded/niri-alttab.kdl
var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl
var NiriBindsConfig string

View File

@@ -0,0 +1,6 @@
package config
import _ "embed"
//go:embed embedded/testpage.pdf
var TestPage string

View File

@@ -15,6 +15,78 @@ type HSV struct {
H, S, V float64
}
type ColorInfo struct {
Hex string `json:"hex"`
HexStripped string `json:"hex_stripped"`
R int `json:"r"`
G int `json:"g"`
B int `json:"b"`
}
type VariantColorValue struct {
Hex string `json:"hex"`
HexStripped string `json:"hex_stripped"`
}
type VariantColorInfo struct {
Dark VariantColorValue `json:"dark"`
Light VariantColorValue `json:"light"`
Default VariantColorValue `json:"default"`
}
type Palette struct {
Color0 ColorInfo `json:"color0"`
Color1 ColorInfo `json:"color1"`
Color2 ColorInfo `json:"color2"`
Color3 ColorInfo `json:"color3"`
Color4 ColorInfo `json:"color4"`
Color5 ColorInfo `json:"color5"`
Color6 ColorInfo `json:"color6"`
Color7 ColorInfo `json:"color7"`
Color8 ColorInfo `json:"color8"`
Color9 ColorInfo `json:"color9"`
Color10 ColorInfo `json:"color10"`
Color11 ColorInfo `json:"color11"`
Color12 ColorInfo `json:"color12"`
Color13 ColorInfo `json:"color13"`
Color14 ColorInfo `json:"color14"`
Color15 ColorInfo `json:"color15"`
}
type VariantPalette struct {
Color0 VariantColorInfo `json:"color0"`
Color1 VariantColorInfo `json:"color1"`
Color2 VariantColorInfo `json:"color2"`
Color3 VariantColorInfo `json:"color3"`
Color4 VariantColorInfo `json:"color4"`
Color5 VariantColorInfo `json:"color5"`
Color6 VariantColorInfo `json:"color6"`
Color7 VariantColorInfo `json:"color7"`
Color8 VariantColorInfo `json:"color8"`
Color9 VariantColorInfo `json:"color9"`
Color10 VariantColorInfo `json:"color10"`
Color11 VariantColorInfo `json:"color11"`
Color12 VariantColorInfo `json:"color12"`
Color13 VariantColorInfo `json:"color13"`
Color14 VariantColorInfo `json:"color14"`
Color15 VariantColorInfo `json:"color15"`
}
func NewColorInfo(hex string) ColorInfo {
rgb := HexToRGB(hex)
stripped := hex
if len(hex) > 0 && hex[0] == '#' {
stripped = hex[1:]
}
return ColorInfo{
Hex: hex,
HexStripped: stripped,
R: int(math.Round(rgb.R * 255)),
G: int(math.Round(rgb.G * 255)),
B: int(math.Round(rgb.B * 255)),
}
}
func HexToRGB(hex string) RGB {
if hex[0] == '#' {
hex = hex[1:]
@@ -41,13 +113,14 @@ func RGBToHSV(rgb RGB) HSV {
delta := max - min
var h float64
if delta == 0 {
switch {
case delta == 0:
h = 0
} else if max == rgb.R {
case max == rgb.R:
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
} else if max == rgb.G {
case max == rgb.G:
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
} else {
default:
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
}
@@ -272,7 +345,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
}
step := 0.5
for i := 0; i < 120; i++ {
for range 120 {
Lf = math.Max(0, math.Min(100, Lf+dir*step))
cand := labToHex(Lf, af, bf)
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
@@ -310,13 +383,13 @@ func DeriveContainer(primary string, isLight bool) string {
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
}
func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
baseColor := DeriveContainer(primaryColor, opts.IsLight)
rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb)
palette := make([]string, 0, 16)
var palette Palette
var normalTextTarget, secondaryTarget float64
if opts.UseDPS {
@@ -335,7 +408,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
} else {
bgColor = "#1a1a1a"
}
palette = append(palette, bgColor)
palette.Color0 = NewColorInfo(bgColor)
hueShift := (hsv.H - 0.6) * 0.12
satBoost := 1.15
@@ -344,39 +417,39 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
var redColor string
if opts.IsLight {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
} else {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
}
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
var greenColor string
if opts.IsLight {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} else {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
}
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
var yellowColor string
if opts.IsLight {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
} else {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
}
var blueColor string
if opts.IsLight {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} else {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
}
magH := hsv.H - 0.03
@@ -388,66 +461,116 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
hh := RGBToHSV(hr)
if opts.IsLight {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} else {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
}
cyanH := hsv.H + 0.08
if cyanH > 1.0 {
cyanH -= 1.0
}
palette = append(palette, ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
if opts.IsLight {
palette = append(palette, "#1a1a1a")
palette = append(palette, "#2e2e2e")
palette.Color7 = NewColorInfo("#1a1a1a")
palette.Color8 = NewColorInfo("#2e2e2e")
} else {
palette = append(palette, "#abb2bf")
palette = append(palette, "#5c6370")
palette.Color7 = NewColorInfo("#abb2bf")
palette.Color8 = NewColorInfo("#5c6370")
}
if opts.IsLight {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
palette = append(palette, ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} else {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
// Make it way brighter for type names in dark mode
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
brightBlue := retoneToL(primaryColor, 85.0)
palette = append(palette, brightBlue)
palette.Color12 = NewColorInfo(brightBlue)
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyanH := hsv.H + 0.02
if brightCyanH > 1.0 {
brightCyanH -= 1.0
}
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
}
if opts.IsLight {
palette = append(palette, "#1a1a1a")
palette.Color15 = NewColorInfo("#1a1a1a")
} else {
palette = append(palette, "#ffffff")
palette.Color15 = NewColorInfo("#ffffff")
}
return palette
}
type VariantOptions struct {
PrimaryDark string
PrimaryLight string
Background string
UseDPS bool
IsLightMode bool
}
func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
darkVal := VariantColorValue{Hex: dark.Hex, HexStripped: dark.HexStripped}
lightVal := VariantColorValue{Hex: light.Hex, HexStripped: light.HexStripped}
defaultVal := darkVal
if isLightMode {
defaultVal = lightVal
}
return VariantColorInfo{
Dark: darkVal,
Light: lightVal,
Default: defaultVal,
}
}
func GenerateVariantPalette(opts VariantOptions) VariantPalette {
darkOpts := PaletteOptions{IsLight: false, Background: opts.Background, UseDPS: opts.UseDPS}
lightOpts := PaletteOptions{IsLight: true, Background: opts.Background, UseDPS: opts.UseDPS}
dark := GeneratePalette(opts.PrimaryDark, darkOpts)
light := GeneratePalette(opts.PrimaryLight, lightOpts)
return VariantPalette{
Color0: mergeColorInfo(dark.Color0, light.Color0, opts.IsLightMode),
Color1: mergeColorInfo(dark.Color1, light.Color1, opts.IsLightMode),
Color2: mergeColorInfo(dark.Color2, light.Color2, opts.IsLightMode),
Color3: mergeColorInfo(dark.Color3, light.Color3, opts.IsLightMode),
Color4: mergeColorInfo(dark.Color4, light.Color4, opts.IsLightMode),
Color5: mergeColorInfo(dark.Color5, light.Color5, opts.IsLightMode),
Color6: mergeColorInfo(dark.Color6, light.Color6, opts.IsLightMode),
Color7: mergeColorInfo(dark.Color7, light.Color7, opts.IsLightMode),
Color8: mergeColorInfo(dark.Color8, light.Color8, opts.IsLightMode),
Color9: mergeColorInfo(dark.Color9, light.Color9, opts.IsLightMode),
Color10: mergeColorInfo(dark.Color10, light.Color10, opts.IsLightMode),
Color11: mergeColorInfo(dark.Color11, light.Color11, opts.IsLightMode),
Color12: mergeColorInfo(dark.Color12, light.Color12, opts.IsLightMode),
Color13: mergeColorInfo(dark.Color13, light.Color13, opts.IsLightMode),
Color14: mergeColorInfo(dark.Color14, light.Color14, opts.IsLightMode),
Color15: mergeColorInfo(dark.Color15, light.Color15, opts.IsLightMode),
}
}

View File

@@ -1,7 +1,6 @@
package dank16
import (
"encoding/json"
"math"
"testing"
)
@@ -346,106 +345,36 @@ func TestGeneratePalette(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts)
if len(result) != 16 {
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
colors := []ColorInfo{
result.Color0, result.Color1, result.Color2, result.Color3,
result.Color4, result.Color5, result.Color6, result.Color7,
result.Color8, result.Color9, result.Color10, result.Color11,
result.Color12, result.Color13, result.Color14, result.Color15,
}
for i, color := range result {
if len(color) != 7 || color[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
for i, color := range colors {
if len(color.Hex) != 7 || color.Hex[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex)
}
}
if tt.opts.Background != "" && result[0] != tt.opts.Background {
t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background)
} else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0])
} else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result[0])
if tt.opts.Background != "" && result.Color0.Hex != tt.opts.Background {
t.Errorf("Background color = %s, expected %s", result.Color0.Hex, tt.opts.Background)
} else if !tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result.Color0.Hex)
} else if tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
}
if tt.opts.IsLight && result[15] != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15])
} else if !tt.opts.IsLight && result[15] != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15])
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
}
})
}
}
func TestEnrichVSCodeTheme(t *testing.T) {
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
baseTheme := map[string]interface{}{
"name": "Test Theme",
"type": "dark",
"colors": map[string]interface{}{
"editor.background": "#000000",
},
}
themeJSON, err := json.Marshal(baseTheme)
if err != nil {
t.Fatalf("Failed to marshal base theme: %v", err)
}
result, err := EnrichVSCodeTheme(themeJSON, colors)
if err != nil {
t.Fatalf("EnrichVSCodeTheme failed: %v", err)
}
var enriched map[string]interface{}
if err := json.Unmarshal(result, &enriched); err != nil {
t.Fatalf("Failed to unmarshal result: %v", err)
}
colorsMap, ok := enriched["colors"].(map[string]interface{})
if !ok {
t.Fatal("colors is not a map")
}
terminalColors := []string{
"terminal.ansiBlack",
"terminal.ansiRed",
"terminal.ansiGreen",
"terminal.ansiYellow",
"terminal.ansiBlue",
"terminal.ansiMagenta",
"terminal.ansiCyan",
"terminal.ansiWhite",
"terminal.ansiBrightBlack",
"terminal.ansiBrightRed",
"terminal.ansiBrightGreen",
"terminal.ansiBrightYellow",
"terminal.ansiBrightBlue",
"terminal.ansiBrightMagenta",
"terminal.ansiBrightCyan",
"terminal.ansiBrightWhite",
}
for i, key := range terminalColors {
if val, ok := colorsMap[key]; !ok {
t.Errorf("Missing terminal color: %s", key)
} else if val != colors[i] {
t.Errorf("%s = %s, expected %s", key, val, colors[i])
}
}
if colorsMap["editor.background"] != "#000000" {
t.Error("Original theme colors should be preserved")
}
}
func TestEnrichVSCodeThemeInvalidJSON(t *testing.T) {
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
invalidJSON := []byte("{invalid json")
_, err := EnrichVSCodeTheme(invalidJSON, colors)
if err == nil {
t.Error("Expected error for invalid JSON, got nil")
}
}
func TestRoundTripConversion(t *testing.T) {
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
@@ -635,23 +564,26 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts)
if len(result) != 16 {
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
colors := []ColorInfo{
result.Color0, result.Color1, result.Color2, result.Color3,
result.Color4, result.Color5, result.Color6, result.Color7,
result.Color8, result.Color9, result.Color10, result.Color11,
result.Color12, result.Color13, result.Color14, result.Color15,
}
for i, color := range result {
if len(color) != 7 || color[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
for i, color := range colors {
if len(color.Hex) != 7 || color.Hex[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex)
}
}
bgColor := result[0]
bgColor := result.Color0.Hex
for i := 1; i < 8; i++ {
lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight)
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
minLc := 30.0
if lc < minLc && lc > 0 {
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
i, result[i], lc, bgColor, minLc)
i, colors[i].Hex, lc, bgColor, minLc)
}
}
})
@@ -708,17 +640,26 @@ func TestContrastAlgorithmComparison(t *testing.T) {
paletteWCAG := GeneratePalette(base, optsWCAG)
paletteDPS := GeneratePalette(base, optsDPS)
if len(paletteWCAG) != 16 || len(paletteDPS) != 16 {
t.Fatal("Both palettes should have 16 colors")
wcagColors := []ColorInfo{
paletteWCAG.Color0, paletteWCAG.Color1, paletteWCAG.Color2, paletteWCAG.Color3,
paletteWCAG.Color4, paletteWCAG.Color5, paletteWCAG.Color6, paletteWCAG.Color7,
paletteWCAG.Color8, paletteWCAG.Color9, paletteWCAG.Color10, paletteWCAG.Color11,
paletteWCAG.Color12, paletteWCAG.Color13, paletteWCAG.Color14, paletteWCAG.Color15,
}
dpsColors := []ColorInfo{
paletteDPS.Color0, paletteDPS.Color1, paletteDPS.Color2, paletteDPS.Color3,
paletteDPS.Color4, paletteDPS.Color5, paletteDPS.Color6, paletteDPS.Color7,
paletteDPS.Color8, paletteDPS.Color9, paletteDPS.Color10, paletteDPS.Color11,
paletteDPS.Color12, paletteDPS.Color13, paletteDPS.Color14, paletteDPS.Color15,
}
if paletteWCAG[0] != paletteDPS[0] {
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0])
if paletteWCAG.Color0.Hex != paletteDPS.Color0.Hex {
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG.Color0.Hex, paletteDPS.Color0.Hex)
}
differentCount := 0
for i := 0; i < 16; i++ {
if paletteWCAG[i] != paletteDPS[i] {
for i := range 16 {
if wcagColors[i].Hex != dpsColors[i].Hex {
differentCount++
}
}

View File

@@ -6,135 +6,130 @@ import (
"strings"
)
func GenerateJSON(colors []string) string {
colorMap := make(map[string]string)
for i, color := range colors {
colorMap[fmt.Sprintf("color%d", i)] = color
}
marshalled, _ := json.Marshal(colorMap)
func GenerateJSON(p Palette) string {
marshalled, _ := json.Marshal(p)
return string(marshalled)
}
func GenerateKittyTheme(colors []string) string {
kittyColors := []struct {
name string
index int
}{
{"color0", 0},
{"color1", 1},
{"color2", 2},
{"color3", 3},
{"color4", 4},
{"color5", 5},
{"color6", 6},
{"color7", 7},
{"color8", 8},
{"color9", 9},
{"color10", 10},
{"color11", 11},
{"color12", 12},
{"color13", 13},
{"color14", 14},
{"color15", 15},
}
func GenerateVariantJSON(p VariantPalette) string {
marshalled, _ := json.Marshal(p)
return string(marshalled)
}
func GenerateKittyTheme(p Palette) string {
var result strings.Builder
for _, kc := range kittyColors {
fmt.Fprintf(&result, "%s %s\n", kc.name, colors[kc.index])
}
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
fmt.Fprintf(&result, "color1 %s\n", p.Color1.Hex)
fmt.Fprintf(&result, "color2 %s\n", p.Color2.Hex)
fmt.Fprintf(&result, "color3 %s\n", p.Color3.Hex)
fmt.Fprintf(&result, "color4 %s\n", p.Color4.Hex)
fmt.Fprintf(&result, "color5 %s\n", p.Color5.Hex)
fmt.Fprintf(&result, "color6 %s\n", p.Color6.Hex)
fmt.Fprintf(&result, "color7 %s\n", p.Color7.Hex)
fmt.Fprintf(&result, "color8 %s\n", p.Color8.Hex)
fmt.Fprintf(&result, "color9 %s\n", p.Color9.Hex)
fmt.Fprintf(&result, "color10 %s\n", p.Color10.Hex)
fmt.Fprintf(&result, "color11 %s\n", p.Color11.Hex)
fmt.Fprintf(&result, "color12 %s\n", p.Color12.Hex)
fmt.Fprintf(&result, "color13 %s\n", p.Color13.Hex)
fmt.Fprintf(&result, "color14 %s\n", p.Color14.Hex)
fmt.Fprintf(&result, "color15 %s\n", p.Color15.Hex)
return result.String()
}
func GenerateFootTheme(colors []string) string {
footColors := []struct {
name string
index int
}{
{"regular0", 0},
{"regular1", 1},
{"regular2", 2},
{"regular3", 3},
{"regular4", 4},
{"regular5", 5},
{"regular6", 6},
{"regular7", 7},
{"bright0", 8},
{"bright1", 9},
{"bright2", 10},
{"bright3", 11},
{"bright4", 12},
{"bright5", 13},
{"bright6", 14},
{"bright7", 15},
}
func GenerateFootTheme(p Palette) string {
var result strings.Builder
for _, fc := range footColors {
fmt.Fprintf(&result, "%s=%s\n", fc.name, strings.TrimPrefix(colors[fc.index], "#"))
}
fmt.Fprintf(&result, "regular0=%s\n", p.Color0.HexStripped)
fmt.Fprintf(&result, "regular1=%s\n", p.Color1.HexStripped)
fmt.Fprintf(&result, "regular2=%s\n", p.Color2.HexStripped)
fmt.Fprintf(&result, "regular3=%s\n", p.Color3.HexStripped)
fmt.Fprintf(&result, "regular4=%s\n", p.Color4.HexStripped)
fmt.Fprintf(&result, "regular5=%s\n", p.Color5.HexStripped)
fmt.Fprintf(&result, "regular6=%s\n", p.Color6.HexStripped)
fmt.Fprintf(&result, "regular7=%s\n", p.Color7.HexStripped)
fmt.Fprintf(&result, "bright0=%s\n", p.Color8.HexStripped)
fmt.Fprintf(&result, "bright1=%s\n", p.Color9.HexStripped)
fmt.Fprintf(&result, "bright2=%s\n", p.Color10.HexStripped)
fmt.Fprintf(&result, "bright3=%s\n", p.Color11.HexStripped)
fmt.Fprintf(&result, "bright4=%s\n", p.Color12.HexStripped)
fmt.Fprintf(&result, "bright5=%s\n", p.Color13.HexStripped)
fmt.Fprintf(&result, "bright6=%s\n", p.Color14.HexStripped)
fmt.Fprintf(&result, "bright7=%s\n", p.Color15.HexStripped)
return result.String()
}
func GenerateAlacrittyTheme(colors []string) string {
alacrittyColors := []struct {
section string
name string
index int
}{
{"normal", "black", 0},
{"normal", "red", 1},
{"normal", "green", 2},
{"normal", "yellow", 3},
{"normal", "blue", 4},
{"normal", "magenta", 5},
{"normal", "cyan", 6},
{"normal", "white", 7},
{"bright", "black", 8},
{"bright", "red", 9},
{"bright", "green", 10},
{"bright", "yellow", 11},
{"bright", "blue", 12},
{"bright", "magenta", 13},
{"bright", "cyan", 14},
{"bright", "white", 15},
}
func GenerateAlacrittyTheme(p Palette) string {
var result strings.Builder
currentSection := ""
for _, ac := range alacrittyColors {
if ac.section != currentSection {
if currentSection != "" {
result.WriteString("\n")
}
fmt.Fprintf(&result, "[colors.%s]\n", ac.section)
currentSection = ac.section
}
fmt.Fprintf(&result, "%-7s = '%s'\n", ac.name, colors[ac.index])
}
result.WriteString("[colors.normal]\n")
fmt.Fprintf(&result, "black = '%s'\n", p.Color0.Hex)
fmt.Fprintf(&result, "red = '%s'\n", p.Color1.Hex)
fmt.Fprintf(&result, "green = '%s'\n", p.Color2.Hex)
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color3.Hex)
fmt.Fprintf(&result, "blue = '%s'\n", p.Color4.Hex)
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color5.Hex)
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color6.Hex)
fmt.Fprintf(&result, "white = '%s'\n", p.Color7.Hex)
result.WriteString("\n[colors.bright]\n")
fmt.Fprintf(&result, "black = '%s'\n", p.Color8.Hex)
fmt.Fprintf(&result, "red = '%s'\n", p.Color9.Hex)
fmt.Fprintf(&result, "green = '%s'\n", p.Color10.Hex)
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color11.Hex)
fmt.Fprintf(&result, "blue = '%s'\n", p.Color12.Hex)
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color13.Hex)
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color14.Hex)
fmt.Fprintf(&result, "white = '%s'\n", p.Color15.Hex)
return result.String()
}
func GenerateGhosttyTheme(colors []string) string {
func GenerateGhosttyTheme(p Palette) string {
var result strings.Builder
for i, color := range colors {
fmt.Fprintf(&result, "palette = %d=%s\n", i, color)
}
fmt.Fprintf(&result, "palette = 0=%s\n", p.Color0.Hex)
fmt.Fprintf(&result, "palette = 1=%s\n", p.Color1.Hex)
fmt.Fprintf(&result, "palette = 2=%s\n", p.Color2.Hex)
fmt.Fprintf(&result, "palette = 3=%s\n", p.Color3.Hex)
fmt.Fprintf(&result, "palette = 4=%s\n", p.Color4.Hex)
fmt.Fprintf(&result, "palette = 5=%s\n", p.Color5.Hex)
fmt.Fprintf(&result, "palette = 6=%s\n", p.Color6.Hex)
fmt.Fprintf(&result, "palette = 7=%s\n", p.Color7.Hex)
fmt.Fprintf(&result, "palette = 8=%s\n", p.Color8.Hex)
fmt.Fprintf(&result, "palette = 9=%s\n", p.Color9.Hex)
fmt.Fprintf(&result, "palette = 10=%s\n", p.Color10.Hex)
fmt.Fprintf(&result, "palette = 11=%s\n", p.Color11.Hex)
fmt.Fprintf(&result, "palette = 12=%s\n", p.Color12.Hex)
fmt.Fprintf(&result, "palette = 13=%s\n", p.Color13.Hex)
fmt.Fprintf(&result, "palette = 14=%s\n", p.Color14.Hex)
fmt.Fprintf(&result, "palette = 15=%s\n", p.Color15.Hex)
return result.String()
}
func GenerateWeztermTheme(colors []string) string {
func GenerateWeztermTheme(p Palette) string {
var result strings.Builder
labels := []string{"ansi", "brights"}
for j, label := range labels {
start := j * 8
colorSlice := make([]string, 8)
for i, color := range colors[start : start+8] {
colorSlice[i] = fmt.Sprintf("'%s'", color)
}
fmt.Fprintf(&result, "%s = [%s]\n", label, strings.Join(colorSlice, ", "))
}
fmt.Fprintf(&result, "ansi = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
p.Color0.Hex, p.Color1.Hex, p.Color2.Hex, p.Color3.Hex,
p.Color4.Hex, p.Color5.Hex, p.Color6.Hex, p.Color7.Hex)
fmt.Fprintf(&result, "brights = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
p.Color8.Hex, p.Color9.Hex, p.Color10.Hex, p.Color11.Hex,
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
return result.String()
}
func GenerateNeovimTheme(p Palette) string {
var result strings.Builder
fmt.Fprintf(&result, "vim.g.terminal_color_0 = \"%s\"\n", p.Color0.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_1 = \"%s\"\n", p.Color1.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_2 = \"%s\"\n", p.Color2.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_3 = \"%s\"\n", p.Color3.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_4 = \"%s\"\n", p.Color4.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_5 = \"%s\"\n", p.Color5.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_6 = \"%s\"\n", p.Color6.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_7 = \"%s\"\n", p.Color7.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_8 = \"%s\"\n", p.Color8.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_9 = \"%s\"\n", p.Color9.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_10 = \"%s\"\n", p.Color10.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_11 = \"%s\"\n", p.Color11.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_12 = \"%s\"\n", p.Color12.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_13 = \"%s\"\n", p.Color13.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_14 = \"%s\"\n", p.Color14.Hex)
fmt.Fprintf(&result, "vim.g.terminal_color_15 = \"%s\"\n", p.Color15.Hex)
return result.String()
}

View File

@@ -1,250 +0,0 @@
package dank16
import (
"encoding/json"
"fmt"
)
type VSCodeTheme struct {
Schema string `json:"$schema"`
Name string `json:"name"`
Type string `json:"type"`
Colors map[string]string `json:"colors"`
TokenColors []VSCodeTokenColor `json:"tokenColors"`
SemanticHighlighting bool `json:"semanticHighlighting"`
SemanticTokenColors map[string]VSCodeTokenSetting `json:"semanticTokenColors"`
}
type VSCodeTokenColor struct {
Scope interface{} `json:"scope"`
Settings VSCodeTokenSetting `json:"settings"`
}
type VSCodeTokenSetting struct {
Foreground string `json:"foreground,omitempty"`
FontStyle string `json:"fontStyle,omitempty"`
}
func updateTokenColor(tc interface{}, scopeToColor map[string]string) {
tcMap, ok := tc.(map[string]interface{})
if !ok {
return
}
scopes, ok := tcMap["scope"].([]interface{})
if !ok {
return
}
settings, ok := tcMap["settings"].(map[string]interface{})
if !ok {
return
}
isYaml := hasScopeContaining(scopes, "yaml")
for _, scope := range scopes {
scopeStr, ok := scope.(string)
if !ok {
continue
}
if scopeStr == "string" && isYaml {
continue
}
if applyColorToScope(settings, scope, scopeToColor) {
break
}
}
}
func applyColorToScope(settings map[string]interface{}, scope interface{}, scopeToColor map[string]string) bool {
scopeStr, ok := scope.(string)
if !ok {
return false
}
newColor, exists := scopeToColor[scopeStr]
if !exists {
return false
}
settings["foreground"] = newColor
return true
}
func hasScopeContaining(scopes []interface{}, substring string) bool {
for _, scope := range scopes {
scopeStr, ok := scope.(string)
if !ok {
continue
}
for i := 0; i <= len(scopeStr)-len(substring); i++ {
if scopeStr[i:i+len(substring)] == substring {
return true
}
}
}
return false
}
func EnrichVSCodeTheme(themeData []byte, colors []string) ([]byte, error) {
var theme map[string]interface{}
if err := json.Unmarshal(themeData, &theme); err != nil {
return nil, err
}
colorsMap, ok := theme["colors"].(map[string]interface{})
if !ok {
colorsMap = make(map[string]interface{})
theme["colors"] = colorsMap
}
bg := colors[0]
isLight := false
if len(bg) == 7 && bg[0] == '#' {
r, g, b := 0, 0, 0
fmt.Sscanf(bg[1:], "%02x%02x%02x", &r, &g, &b)
luminance := (0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 255.0
isLight = luminance > 0.5
}
if isLight {
theme["type"] = "light"
} else {
theme["type"] = "dark"
}
colorsMap["terminal.ansiBlack"] = colors[0]
colorsMap["terminal.ansiRed"] = colors[1]
colorsMap["terminal.ansiGreen"] = colors[2]
colorsMap["terminal.ansiYellow"] = colors[3]
colorsMap["terminal.ansiBlue"] = colors[4]
colorsMap["terminal.ansiMagenta"] = colors[5]
colorsMap["terminal.ansiCyan"] = colors[6]
colorsMap["terminal.ansiWhite"] = colors[7]
colorsMap["terminal.ansiBrightBlack"] = colors[8]
colorsMap["terminal.ansiBrightRed"] = colors[9]
colorsMap["terminal.ansiBrightGreen"] = colors[10]
colorsMap["terminal.ansiBrightYellow"] = colors[11]
colorsMap["terminal.ansiBrightBlue"] = colors[12]
colorsMap["terminal.ansiBrightMagenta"] = colors[13]
colorsMap["terminal.ansiBrightCyan"] = colors[14]
colorsMap["terminal.ansiBrightWhite"] = colors[15]
tokenColors, ok := theme["tokenColors"].([]interface{})
if ok {
scopeToColor := map[string]string{
"comment": colors[8],
"punctuation.definition.comment": colors[8],
"keyword": colors[5],
"storage.type": colors[13],
"storage.modifier": colors[5],
"variable": colors[15],
"variable.parameter": colors[7],
"meta.object-literal.key": colors[4],
"meta.property.object": colors[4],
"variable.other.property": colors[4],
"constant.other.symbol": colors[12],
"constant.numeric": colors[12],
"constant.language": colors[12],
"constant.character": colors[3],
"entity.name.type": colors[12],
"support.type": colors[13],
"entity.name.class": colors[12],
"entity.name.function": colors[2],
"support.function": colors[2],
"support.class": colors[15],
"support.variable": colors[15],
"variable.language": colors[12],
"entity.name.tag.yaml": colors[12],
"string.unquoted.plain.out.yaml": colors[15],
"string.unquoted.yaml": colors[15],
"string": colors[3],
}
for i, tc := range tokenColors {
updateTokenColor(tc, scopeToColor)
tokenColors[i] = tc
}
yamlRules := []VSCodeTokenColor{
{
Scope: "entity.name.tag.yaml",
Settings: VSCodeTokenSetting{Foreground: colors[12]},
},
{
Scope: []string{"string.unquoted.plain.out.yaml", "string.unquoted.yaml"},
Settings: VSCodeTokenSetting{Foreground: colors[15]},
},
}
for _, rule := range yamlRules {
tokenColors = append(tokenColors, rule)
}
theme["tokenColors"] = tokenColors
}
if semanticTokenColors, ok := theme["semanticTokenColors"].(map[string]interface{}); ok {
updates := map[string]string{
"variable": colors[15],
"variable.readonly": colors[12],
"property": colors[4],
"function": colors[2],
"method": colors[2],
"type": colors[12],
"class": colors[12],
"typeParameter": colors[13],
"enumMember": colors[12],
"string": colors[3],
"number": colors[12],
"comment": colors[8],
"keyword": colors[5],
"operator": colors[15],
"parameter": colors[7],
"namespace": colors[15],
}
for key, color := range updates {
if existing, ok := semanticTokenColors[key].(map[string]interface{}); ok {
existing["foreground"] = color
} else {
semanticTokenColors[key] = map[string]interface{}{
"foreground": color,
}
}
}
} else {
semanticTokenColors := make(map[string]interface{})
updates := map[string]string{
"variable": colors[7],
"variable.readonly": colors[12],
"property": colors[4],
"function": colors[2],
"method": colors[2],
"type": colors[12],
"class": colors[12],
"typeParameter": colors[13],
"enumMember": colors[12],
"string": colors[3],
"number": colors[12],
"comment": colors[8],
"keyword": colors[5],
"operator": colors[15],
"parameter": colors[7],
"namespace": colors[15],
}
for key, color := range updates {
semanticTokenColors[key] = map[string]interface{}{
"foreground": color,
}
}
theme["semanticTokenColors"] = semanticTokenColors
}
return json.MarshalIndent(theme, "", " ")
}

View File

@@ -7,6 +7,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -37,6 +38,9 @@ func init() {
Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
}
type ArchDistribution struct {
@@ -88,7 +92,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectWindowManager(wm))
dependencies = append(dependencies, a.detectQuickshell())
dependencies = append(dependencies, a.detectXDGPortal())
dependencies = append(dependencies, a.detectPolkitAgent())
dependencies = append(dependencies, a.detectAccountsService())
// Hyprland-specific tools
@@ -101,55 +104,18 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, a.detectMatugen())
dependencies = append(dependencies, a.detectDgop())
dependencies = append(dependencies, a.detectHyprpicker())
dependencies = append(dependencies, a.detectClipboardTools()...)
return dependencies, nil
}
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing
if a.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (a *ArchDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if a.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
}
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing
if a.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
}
func (a *ArchDistribution) packageInstalled(pkg string) bool {
@@ -172,21 +138,14 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = a.getNiriMapping(variants["niri"])
@@ -200,13 +159,11 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
// ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
}
@@ -375,6 +332,19 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
LogOutput: "Starting post-installation configuration...",
}
terminal := a.DetectTerminalFromDeps(dependencies)
if err := a.WriteEnvironmentConfig(terminal); err != nil {
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := a.WriteWindowManagerConfig(wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := a.EnableDMSService(ctx, wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
// Phase 7: Complete
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
@@ -545,12 +515,9 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
dmsShell = append(dmsShell, pkg)
} else {
isDep := false
for _, dep := range dmsDepencies {
if pkg == dep {
deps = append(deps, pkg)
isDep = true
break
}
if slices.Contains(dmsDepencies, pkg) {
deps = append(deps, pkg)
isDep = true
}
if !isDep {
others = append(others, pkg)
@@ -576,7 +543,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
}
if err := os.MkdirAll(buildDir, 0755); err != nil {
if err := os.MkdirAll(buildDir, 0o755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
defer func() {

View File

@@ -17,8 +17,10 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
)
const forceQuickshellGit = false
const forceDMSGit = false
const (
forceQuickshellGit = false
forceDMSGit = false
)
// BaseDistribution provides common functionality for all distributions
type BaseDistribution struct {
@@ -74,47 +76,42 @@ func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
}
// Common dependency detection methods
func (b *BaseDistribution) detectGit() deps.Dependency {
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
status := deps.StatusMissing
if b.commandExists("git") {
if b.commandExists(name) {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "git",
Name: name,
Status: status,
Description: "Version control system",
Description: description,
Required: true,
}
}
func (b *BaseDistribution) detectPackage(name, description string, installed bool) deps.Dependency {
status := deps.StatusMissing
if installed {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: true,
}
}
func (b *BaseDistribution) detectGit() deps.Dependency {
return b.detectCommand("git", "Version control system")
}
func (b *BaseDistribution) detectMatugen() deps.Dependency {
status := deps.StatusMissing
if b.commandExists("matugen") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "matugen",
Status: status,
Description: "Material Design color generation tool",
Required: true,
}
return b.detectCommand("matugen", "Material Design color generation tool")
}
func (b *BaseDistribution) detectDgop() deps.Dependency {
status := deps.StatusMissing
if b.commandExists("dgop") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dgop",
Status: status,
Description: "Desktop portal management tool",
Required: true,
}
return b.detectCommand("dgop", "Desktop portal management tool")
}
func (b *BaseDistribution) detectDMS() deps.Dependency {
@@ -188,51 +185,6 @@ func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.D
}
}
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
var dependencies []deps.Dependency
cliphist := deps.StatusMissing
if b.commandExists("cliphist") {
cliphist = deps.StatusInstalled
}
wlClipboard := deps.StatusMissing
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
wlClipboard = deps.StatusInstalled
}
dependencies = append(dependencies,
deps.Dependency{
Name: "cliphist",
Status: cliphist,
Description: "Wayland clipboard manager",
Required: true,
},
deps.Dependency{
Name: "wl-clipboard",
Status: wlClipboard,
Description: "Wayland clipboard utilities",
Required: true,
},
)
return dependencies
}
func (b *BaseDistribution) detectHyprpicker() deps.Dependency {
status := deps.StatusMissing
if b.commandExists("hyprpicker") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "hyprpicker",
Status: status,
Description: "Color picker for Wayland",
Required: true,
}
}
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
var dependencies []deps.Dependency
@@ -240,10 +192,7 @@ func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
name string
description string
}{
{"grim", "Screenshot utility for Wayland"},
{"slurp", "Region selection utility for Wayland"},
{"hyprctl", "Hyprland control utility"},
{"grimblast", "Screenshot script for Hyprland"},
{"jq", "JSON processor"},
}
@@ -564,6 +513,106 @@ func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressCha
}
}
func (b *BaseDistribution) DetectTerminalFromDeps(dependencies []deps.Dependency) deps.Terminal {
for _, dep := range dependencies {
switch dep.Name {
case "ghostty":
return deps.TerminalGhostty
case "kitty":
return deps.TerminalKitty
case "alacritty":
return deps.TerminalAlacritty
}
}
return deps.TerminalGhostty
}
func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
envDir := filepath.Join(homeDir, ".config", "environment.d")
if err := os.MkdirAll(envDir, 0755); err != nil {
return fmt.Errorf("failed to create environment.d directory: %w", err)
}
var terminalCmd string
switch terminal {
case deps.TerminalGhostty:
terminalCmd = "ghostty"
case deps.TerminalKitty:
terminalCmd = "kitty"
case deps.TerminalAlacritty:
terminalCmd = "alacritty"
default:
terminalCmd = "ghostty"
}
content := fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto
TERMINAL=%s
`, terminalCmd)
envFile := filepath.Join(envDir, "90-dms.conf")
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write environment config: %w", err)
}
b.log(fmt.Sprintf("Wrote environment config to %s", envFile))
return nil
}
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
switch wm {
case deps.WindowManagerNiri:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for niri.service")
}
case deps.WindowManagerHyprland:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for hyprland-session.target")
}
}
return nil
}
func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error {
if wm == deps.WindowManagerHyprland {
if err := b.WriteHyprlandSessionTarget(); err != nil {
return fmt.Errorf("failed to write hyprland session target: %w", err)
}
}
return nil
}
func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create systemd user directory: %w", err)
}
targetPath := filepath.Join(targetDir, "hyprland-session.target")
content := `[Unit]
Description=Hyprland Session Target
Requires=graphical-session.target
After=graphical-session.target
`
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
}
b.log(fmt.Sprintf("Wrote hyprland-session.target to %s", targetPath))
return nil
}
// installDMSBinary installs the DMS binary from GitHub releases
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
b.log("Installing/updating DMS binary...")
@@ -602,7 +651,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
return fmt.Errorf("failed to get user home directory: %w", err)
}
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)

View File

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
@@ -36,7 +37,7 @@ func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
}
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
if !commandExists("git") {
if !utils.CommandExists("git") {
t.Skip("git not available")
}
@@ -80,7 +81,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
}
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
if !commandExists("git") {
if !utils.CommandExists("git") {
t.Skip("git not available")
}
@@ -164,11 +165,6 @@ func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
}
}
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func TestBaseDistribution_versionCompare(t *testing.T) {
logChan := make(chan string, 10)
defer close(logChan)

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os/exec"
"runtime"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -61,7 +62,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectPolkitAgent())
dependencies = append(dependencies, d.detectAccountsService())
if wm == deps.WindowManagerNiri {
@@ -70,66 +70,20 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectHyprpicker())
dependencies = append(dependencies, d.detectClipboardTools()...)
return dependencies, nil
}
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing
if d.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if d.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
return d.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", d.packageInstalled("xdg-desktop-portal-gtk"))
}
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing
if d.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
return d.detectCommand("xwayland-satellite", "Xwayland support")
}
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing
if d.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
}
func (d *DebianDistribution) packageInstalled(pkg string) bool {
@@ -139,33 +93,63 @@ func (d *DebianDistribution) packageInstalled(pkg string) bool {
}
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
// Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
// DMS packages from OBS with variant support
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
if wm == deps.WindowManagerNiri {
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
niriVariant := variants["niri"]
packages["niri"] = d.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = d.getXwaylandSatelliteMapping(niriVariant)
}
return packages
}
func (d *DebianDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
}
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
}
func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "niri", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
@@ -192,7 +176,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err)
}
@@ -209,7 +193,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
}
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err)
}
@@ -238,8 +222,23 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
systemPkgs, obsPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Enable OBS repositories
if len(obsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling OBS repositories...",
IsComplete: false,
LogOutput: "Setting up OBS repositories for additional packages",
}
if err := d.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable OBS repositories: %w", err)
}
}
// System Packages
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
@@ -254,6 +253,22 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
}
}
// OBS Packages
obsPkgNames := d.extractPackageNames(obsPkgs)
if len(obsPkgNames) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
}
if err := d.installAPTPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err)
}
}
// Manual Builds
if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
@@ -286,6 +301,19 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...",
}
terminal := d.DetectTerminalFromDeps(dependencies)
if err := d.WriteEnvironmentConfig(terminal); err != nil {
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := d.WriteWindowManagerConfig(wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := d.EnableDMSService(ctx, wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
@@ -297,8 +325,9 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
return nil
}
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
systemPkgs := []string{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
@@ -306,7 +335,7 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
variantMap[dep.Name] = dep.Variant
}
packageMap := d.GetPackageMapping(wm)
packageMap := d.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
@@ -326,12 +355,118 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
switch pkgInfo.Repository {
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return systemPkgs, manualPkgs, variantMap
return systemPkgs, obsPkgs, manualPkgs, variantMap
}
func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
osInfo, err := GetOSInfo()
if err != nil {
return fmt.Errorf("failed to get OS info: %w", err)
}
// Determine Debian version for OBS repository URL
debianVersion := "Debian_13"
if osInfo.VersionID == "testing" {
debianVersion = "Debian_Testing"
} else if osInfo.VersionCodename == "sid" || osInfo.VersionID == "sid" || strings.Contains(strings.ToLower(osInfo.PrettyName), "sid") || strings.Contains(strings.ToLower(osInfo.PrettyName), "unstable") {
debianVersion = "Debian_Unstable"
}
for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
d.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
// RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, debianVersion)
// Check if repository already exists
listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName)
checkCmd := exec.CommandContext(ctx, "test", "-f", listFile)
if checkCmd.Run() == nil {
d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
continue
}
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
// Create keyrings directory if it doesn't exist
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
if err := mkdirCmd.Run(); err != nil {
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
}
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
}
// Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Adding OBS repository %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
}
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
d.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
}
}
if len(enabledRepos) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Updating package lists...",
NeedsSudo: true,
CommandInfo: "sudo apt-get update",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
}
}
return nil
}
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -341,7 +476,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"apt-get", "install", "-y"}
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
@@ -414,7 +549,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Rust: %w", err)
}
case "cliphist", "dgop":
case "dgop":
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err)
}
@@ -451,7 +586,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup",
}
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err)
}
@@ -490,34 +625,10 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
}
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
d.log("Installing Ghostty using Debian installer script...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Running Ghostty Debian installer...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
LogOutput: "Installing Ghostty using pre-built Debian package",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
return fmt.Errorf("failed to install Ghostty: %w", err)
}
d.log("Ghostty installed successfully using Debian installer")
return nil
}
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
@@ -527,10 +638,6 @@ func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages
for _, pkg := range packages {
switch pkg {
case "ghostty":
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
}
default:
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err)

View File

@@ -19,10 +19,12 @@ func init() {
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
}
type FedoraDistribution struct {
@@ -74,7 +76,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectWindowManager(wm))
dependencies = append(dependencies, f.detectQuickshell())
dependencies = append(dependencies, f.detectXDGPortal())
dependencies = append(dependencies, f.detectPolkitAgent())
dependencies = append(dependencies, f.detectAccountsService())
// Hyprland-specific tools
@@ -87,41 +88,14 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, f.detectMatugen())
dependencies = append(dependencies, f.detectDgop())
dependencies = append(dependencies, f.detectHyprpicker())
dependencies = append(dependencies, f.detectClipboardTools()...)
return dependencies, nil
}
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing
if f.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (f *FedoraDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if f.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
return f.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", f.packageInstalled("xdg-desktop-portal-gtk"))
}
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
@@ -141,16 +115,12 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
// COPR packages
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
}
@@ -158,10 +128,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = f.getNiriMapping(variants["niri"])
@@ -185,25 +152,15 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
}
func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
func (f *FedoraDistribution) getHyprpickerMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprpicker-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
return PackageMapping{Name: "hyprpicker", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"}
}
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
}
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
}
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -383,6 +340,19 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...",
}
terminal := f.DetectTerminalFromDeps(dependencies)
if err := f.WriteEnvironmentConfig(terminal); err != nil {
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := f.WriteWindowManagerConfig(wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := f.EnableDMSService(ctx, wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
// Phase 7: Complete
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
@@ -506,6 +476,14 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...)
progressChan <- InstallProgressMsg{

View File

@@ -95,7 +95,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectWindowManager(wm))
dependencies = append(dependencies, g.detectQuickshell())
dependencies = append(dependencies, g.detectXDGPortal())
dependencies = append(dependencies, g.detectPolkitAgent())
dependencies = append(dependencies, g.detectAccountsService())
if wm == deps.WindowManagerHyprland {
@@ -108,66 +107,20 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectMatugen())
dependencies = append(dependencies, g.detectDgop())
dependencies = append(dependencies, g.detectHyprpicker())
dependencies = append(dependencies, g.detectClipboardTools()...)
return dependencies, nil
}
func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if g.packageInstalled("mate-extra/mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
return g.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", g.packageInstalled("sys-apps/xdg-desktop-portal-gtk"))
}
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing
if g.packageInstalled("gui-apps/xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
return g.detectPackage("xwayland-satellite", "Xwayland support", g.packageInstalled("gui-apps/xwayland-satellite"))
}
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing
if g.packageInstalled("sys-apps/accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
return g.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", g.packageInstalled("sys-apps/accountsservice"))
}
func (g *GentooDistribution) packageInstalled(pkg string) bool {
@@ -186,11 +139,8 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
@@ -199,7 +149,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
}
@@ -207,10 +156,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
packages["grimblast"] = PackageMapping{Name: "gui-wm/hyprland-contrib", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = g.getNiriMapping(variants["niri"])
@@ -228,16 +174,8 @@ func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
}
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
archKeyword := g.getArchKeyword()
if variant == deps.VariantGit {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
}
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
}
func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()}
}
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
@@ -460,6 +398,19 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...",
}
terminal := g.DetectTerminalFromDeps(dependencies)
if err := g.WriteEnvironmentConfig(terminal); err != nil {
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := g.WriteWindowManagerConfig(wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := g.EnableDMSService(ctx, wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,

View File

@@ -39,6 +39,7 @@ const (
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
RepoTypeFlake RepositoryType = "flake" // Nix flake
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
RepoTypeManual RepositoryType = "manual" // Manual build from source

View File

@@ -18,8 +18,8 @@ type ManualPackageInstaller struct {
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
lines := strings.Split(output, "\n")
for _, line := range lines {
lines := strings.SplitSeq(output, "\n")
for line := range lines {
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
parts := strings.Split(line, "refs/tags/")
if len(parts) > 1 {
@@ -62,10 +62,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install dgop: %w", err)
}
case "grimblast":
if err := m.installGrimblast(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install grimblast: %w", err)
}
case "niri":
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install niri: %w", err)
@@ -78,10 +74,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install hyprland: %w", err)
}
case "hyprpicker":
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install hyprpicker: %w", err)
}
case "ghostty":
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
@@ -90,10 +82,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install matugen: %w", err)
}
case "cliphist":
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install cliphist: %w", err)
}
case "xwayland-satellite":
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
@@ -115,12 +103,12 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "dgop-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -166,72 +154,16 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
return nil
}
func (m *ManualPackageInstaller) installGrimblast(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing grimblast script for Hyprland...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Downloading grimblast script...",
IsComplete: false,
CommandInfo: "curl grimblast script",
}
grimblastURL := "https://raw.githubusercontent.com/hyprwm/contrib/refs/heads/main/grimblast/grimblast"
tmpPath := filepath.Join(os.TempDir(), "grimblast")
downloadCmd := exec.CommandContext(ctx, "curl", "-L", "-o", tmpPath, grimblastURL)
if err := downloadCmd.Run(); err != nil {
m.logError("failed to download grimblast", err)
return fmt.Errorf("failed to download grimblast: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.5,
Step: "Making grimblast executable...",
IsComplete: false,
CommandInfo: "chmod +x grimblast",
}
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", tmpPath)
if err := chmodCmd.Run(); err != nil {
m.logError("failed to make grimblast executable", err)
return fmt.Errorf("failed to make grimblast executable: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing grimblast to /usr/local/bin...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cp grimblast /usr/local/bin/",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("cp %s /usr/local/bin/grimblast", tmpPath))
if err := installCmd.Run(); err != nil {
m.logError("failed to install grimblast", err)
return fmt.Errorf("failed to install grimblast: %w", err)
}
os.Remove(tmpPath)
m.log("grimblast installed successfully to /usr/local/bin")
return nil
}
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing niri from source...")
homeDir, _ := os.UserHomeDir()
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
if err := os.MkdirAll(buildDir, 0755); err != nil {
if err := os.MkdirAll(buildDir, 0o755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer func() {
@@ -305,12 +237,12 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "quickshell-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -341,7 +273,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
}
buildDir := tmpDir + "/build"
if err := os.MkdirAll(buildDir, 0755); err != nil {
if err := os.MkdirAll(buildDir, 0o755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
@@ -411,12 +343,12 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "hyprland-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -465,184 +397,6 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
return nil
}
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing hyprpicker from source...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
// Install hyprutils first
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.05,
Step: "Building hyprutils dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
}
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
return fmt.Errorf("failed to create hyprutils directory: %w", err)
}
defer os.RemoveAll(hyprutilsDir)
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
if err := cloneUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprutils: %w", err)
}
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-DBUILD_TESTING=off",
"-S", ".",
"-B", "./build")
configureUtilsCmd.Dir = hyprutilsDir
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
return fmt.Errorf("failed to configure hyprutils: %w", err)
}
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
buildUtilsCmd.Dir = hyprutilsDir
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
return fmt.Errorf("failed to build hyprutils: %w", err)
}
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installUtilsCmd.Dir = hyprutilsDir
if err := installUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprutils: %w", err)
}
// Install hyprwayland-scanner
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.2,
Step: "Building hyprwayland-scanner dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
}
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
if err := os.MkdirAll(scannerDir, 0755); err != nil {
return fmt.Errorf("failed to create scanner directory: %w", err)
}
defer os.RemoveAll(scannerDir)
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
if err := cloneScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
}
configureScannerCmd := exec.CommandContext(ctx, "cmake",
"-DCMAKE_INSTALL_PREFIX=/usr",
"-B", "build")
configureScannerCmd.Dir = scannerDir
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
}
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
buildScannerCmd.Dir = scannerDir
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
}
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installScannerCmd.Dir = scannerDir
if err := installScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
}
// Now build hyprpicker
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: "Cloning hyprpicker repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprpicker: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.45,
Step: "Configuring hyprpicker build...",
IsComplete: false,
CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
}
configureCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-S", ".",
"-B", "./build")
configureCmd.Dir = tmpDir
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
output, err := configureCmd.CombinedOutput()
if err != nil {
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
return fmt.Errorf("failed to configure hyprpicker: %w\nCMake output:\n%s", err, string(output))
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.55,
Step: "Building hyprpicker...",
IsComplete: false,
CommandInfo: "cmake --build build --target hyprpicker",
}
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "hyprpicker")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
return fmt.Errorf("failed to build hyprpicker: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing hyprpicker...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cmake --install build",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprpicker: %w", err)
}
m.log("hyprpicker installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing Ghostty from source...")
@@ -652,12 +406,12 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "ghostty-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -774,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
}
configDir := filepath.Dir(dmsPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
if err := os.MkdirAll(configDir, 0o755); err != nil {
return fmt.Errorf("failed to create quickshell config directory: %w", err)
}
@@ -863,52 +617,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing cliphist from source...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Installing cliphist via go install...",
IsComplete: false,
CommandInfo: "go install go.senan.xyz/cliphist@latest",
}
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
return fmt.Errorf("failed to install cliphist: %w", err)
}
homeDir := os.Getenv("HOME")
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
targetPath := "/usr/local/bin/cliphist"
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.7,
Step: "Installing cliphist binary to system...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
}
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
}
// Make it executable
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to make cliphist executable: %w", err)
}
m.log("cliphist installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing xwayland-satellite from source...")

View File

@@ -1,466 +0,0 @@
package distros
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func init() {
Register("nixos", "#7EBAE4", FamilyNix, func(config DistroConfig, logChan chan<- string) Distribution {
return NewNixOSDistribution(config, logChan)
})
}
type NixOSDistribution struct {
*BaseDistribution
config DistroConfig
}
func NewNixOSDistribution(config DistroConfig, logChan chan<- string) *NixOSDistribution {
base := NewBaseDistribution(logChan)
return &NixOSDistribution{
BaseDistribution: base,
config: config,
}
}
func (n *NixOSDistribution) GetID() string {
return n.config.ID
}
func (n *NixOSDistribution) GetColorHex() string {
return n.config.ColorHex
}
func (n *NixOSDistribution) GetFamily() DistroFamily {
return n.config.Family
}
func (n *NixOSDistribution) GetPackageManager() PackageManagerType {
return PackageManagerNix
}
func (n *NixOSDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return n.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (n *NixOSDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
// DMS at the top (shell is prominent)
dependencies = append(dependencies, n.detectDMS())
// Terminal with choice support
dependencies = append(dependencies, n.detectSpecificTerminal(terminal))
// Common detections using base methods
dependencies = append(dependencies, n.detectGit())
dependencies = append(dependencies, n.detectWindowManager(wm))
dependencies = append(dependencies, n.detectQuickshell())
dependencies = append(dependencies, n.detectXDGPortal())
dependencies = append(dependencies, n.detectPolkitAgent())
dependencies = append(dependencies, n.detectAccountsService())
// Hyprland-specific tools
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, n.detectHyprlandTools()...)
}
// Niri-specific tools
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, n.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, n.detectMatugen())
dependencies = append(dependencies, n.detectDgop())
dependencies = append(dependencies, n.detectHyprpicker())
dependencies = append(dependencies, n.detectClipboardTools()...)
return dependencies, nil
}
func (n *NixOSDistribution) detectDMS() deps.Dependency {
status := deps.StatusMissing
// For NixOS, check if quickshell can find the dms config
cmd := exec.Command("qs", "-c", "dms", "--list")
if err := cmd.Run(); err == nil {
status = deps.StatusInstalled
} else if n.packageInstalled("DankMaterialShell") {
// Fallback: check if flake is in profile
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Description: "Desktop Management System configuration (installed as flake)",
Required: true,
}
}
func (n *NixOSDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (n *NixOSDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
switch wm {
case deps.WindowManagerHyprland:
status := deps.StatusMissing
description := "Dynamic tiling Wayland compositor"
if n.commandExists("hyprland") || n.commandExists("Hyprland") {
status = deps.StatusInstalled
} else {
description = "Install system-wide: programs.hyprland.enable = true; in configuration.nix"
}
return deps.Dependency{
Name: "hyprland",
Status: status,
Description: description,
Required: true,
}
case deps.WindowManagerNiri:
status := deps.StatusMissing
description := "Scrollable-tiling Wayland compositor"
if n.commandExists("niri") {
status = deps.StatusInstalled
} else {
description = "Install system-wide: environment.systemPackages = [ pkgs.niri ]; in configuration.nix"
}
return deps.Dependency{
Name: "niri",
Status: status,
Description: description,
Required: true,
}
default:
return deps.Dependency{
Name: "unknown-wm",
Status: deps.StatusMissing,
Description: "Unknown window manager",
Required: true,
}
}
}
func (n *NixOSDistribution) detectHyprlandTools() []deps.Dependency {
var dependencies []deps.Dependency
tools := []struct {
name string
description string
}{
{"grim", "Screenshot utility for Wayland"},
{"slurp", "Region selection utility for Wayland"},
{"hyprctl", "Hyprland control utility (comes with system Hyprland)"},
{"hyprpicker", "Color picker for Hyprland"},
{"grimblast", "Screenshot script for Hyprland"},
{"jq", "JSON processor"},
}
for _, tool := range tools {
status := deps.StatusMissing
// Special handling for hyprctl - it comes with system hyprland
if tool.name == "hyprctl" {
if n.commandExists("hyprctl") {
status = deps.StatusInstalled
}
} else {
if n.commandExists(tool.name) {
status = deps.StatusInstalled
}
}
dependencies = append(dependencies, deps.Dependency{
Name: tool.name,
Status: status,
Description: tool.description,
Required: true,
})
}
return dependencies
}
func (n *NixOSDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing
if n.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
}
func (n *NixOSDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
}
func (n *NixOSDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
}
func (n *NixOSDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("nix", "profile", "list")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(output), pkg)
}
func (n *NixOSDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
packages := map[string]PackageMapping{
"git": {Name: "nixpkgs#git", Repository: RepoTypeSystem},
"quickshell": {Name: "github:quickshell-mirror/quickshell", Repository: RepoTypeFlake},
"matugen": {Name: "github:InioX/matugen", Repository: RepoTypeFlake},
"dgop": {Name: "github:AvengeMedia/dgop", Repository: RepoTypeFlake},
"dms (DankMaterialShell)": {Name: "github:AvengeMedia/DankMaterialShell", Repository: RepoTypeFlake},
"ghostty": {Name: "nixpkgs#ghostty", Repository: RepoTypeSystem},
"alacritty": {Name: "nixpkgs#alacritty", Repository: RepoTypeSystem},
"cliphist": {Name: "nixpkgs#cliphist", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "nixpkgs#wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "nixpkgs#xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "nixpkgs#mate.mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "nixpkgs#accountsservice", Repository: RepoTypeSystem},
"hyprpicker": {Name: "nixpkgs#hyprpicker", Repository: RepoTypeSystem},
}
// Note: Window managers (hyprland/niri) should be installed system-wide on NixOS
// We only install the tools here
switch wm {
case deps.WindowManagerHyprland:
// Skip hyprland itself - should be installed system-wide
packages["grim"] = PackageMapping{Name: "nixpkgs#grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "nixpkgs#slurp", Repository: RepoTypeSystem}
packages["grimblast"] = PackageMapping{Name: "github:hyprwm/contrib#grimblast", Repository: RepoTypeFlake}
packages["jq"] = PackageMapping{Name: "nixpkgs#jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
// Skip niri itself - should be installed system-wide
packages["xwayland-satellite"] = PackageMapping{Name: "nixpkgs#xwayland-satellite", Repository: RepoTypeFlake}
}
return packages
}
func (n *NixOSDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.10,
Step: "NixOS prerequisites ready",
IsComplete: false,
LogOutput: "NixOS package manager is ready to use",
}
return nil
}
func (n *NixOSDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
// Phase 1: Check Prerequisites
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if err := n.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
nixpkgsPkgs, flakePkgs, _ := n.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Phase 2: Nixpkgs Packages
if len(nixpkgsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d packages from nixpkgs...", len(nixpkgsPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(nixpkgsPkgs, ", ")),
}
if err := n.installNixpkgsPackages(ctx, nixpkgsPkgs, progressChan); err != nil {
return fmt.Errorf("failed to install nixpkgs packages: %w", err)
}
}
// Phase 3: Flake Packages
if len(flakePkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d packages from flakes...", len(flakePkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing flake packages: %s", strings.Join(flakePkgs, ", ")),
}
if err := n.installFlakePackages(ctx, flakePkgs, progressChan); err != nil {
return fmt.Errorf("failed to install flake packages: %w", err)
}
}
// Phase 4: Configuration
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
if err := n.postInstallConfig(progressChan); err != nil {
return fmt.Errorf("failed to configure system: %w", err)
}
// Phase 5: Complete
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (n *NixOSDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
nixpkgsPkgs := []string{}
flakePkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := n.GetPackageMapping(wm)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
n.log(fmt.Sprintf("Warning: No package mapping found for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeSystem:
nixpkgsPkgs = append(nixpkgsPkgs, pkgInfo.Name)
case RepoTypeFlake:
flakePkgs = append(flakePkgs, pkgInfo.Name)
}
}
return nixpkgsPkgs, flakePkgs, variantMap
}
func (n *NixOSDistribution) installNixpkgsPackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
n.log(fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(packages, ", ")))
args := []string{"profile", "install"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing nixpkgs packages...",
IsComplete: false,
CommandInfo: fmt.Sprintf("nix %s", strings.Join(args, " ")),
}
cmd := exec.CommandContext(ctx, "nix", args...)
return n.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (n *NixOSDistribution) installFlakePackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
n.log(fmt.Sprintf("Installing flake packages: %s", strings.Join(packages, ", ")))
baseProgress := 0.65
progressStep := 0.20 / float64(len(packages))
for i, pkg := range packages {
currentProgress := baseProgress + (float64(i) * progressStep)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: currentProgress,
Step: fmt.Sprintf("Installing flake package %s (%d/%d)...", pkg, i+1, len(packages)),
IsComplete: false,
CommandInfo: fmt.Sprintf("nix profile install %s", pkg),
}
cmd := exec.CommandContext(ctx, "nix", "profile", "install", pkg)
if err := n.runWithProgress(cmd, progressChan, PhaseAURPackages, currentProgress, currentProgress+progressStep); err != nil {
return fmt.Errorf("failed to install flake package %s: %w", pkg, err)
}
}
return nil
}
func (n *NixOSDistribution) postInstallConfig(progressChan chan<- InstallProgressMsg) error {
// For NixOS, DMS is installed as a flake package, so we skip both the binary installation and git clone
// The flake installation handles both the binary and config files correctly
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.95,
Step: "NixOS configuration complete",
IsComplete: false,
LogOutput: "DMS installed via flake - binary and config handled by Nix",
}
return nil
}

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