1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-25 14:02:53 -05:00

Compare commits

..

767 Commits

Author SHA1 Message Date
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
github-actions[bot]
e39465aece chore: bump version to v0.6.2 2025-11-19 13:54:50 +00:00
bbedward
8fd616b680 osd: suppression fix from cc 2025-11-19 08:52:37 -05:00
bbedward
cc054b27de filebrowser: fix auto closing from ddash 2025-11-19 08:33:07 -05:00
github-actions[bot]
dfdaa82245 chore: bump version to v0.6.1 2025-11-19 03:16:35 +00:00
bbedward
99a307e0ad dankbar: hot fix color moda & systm tray item positions 2025-11-18 22:13:06 -05:00
github-actions[bot]
5ddea836a1 chore: bump version to v0.6.0 2025-11-18 23:52:39 +00:00
bbedward
208d92aa06 launcher: re-create grid on open 2025-11-18 18:50:42 -05:00
bbedward
6ef9ddd4f3 hyprland: fix right click overview 2025-11-18 17:53:00 -05:00
bbedward
1c92d39185 i18n: update translations 2025-11-18 17:21:45 -05:00
bbedward
c0f072217c dankbar: split up monolithic file 2025-11-18 16:18:24 -05:00
bbedward
542562f988 dankbar: missing background click handler for plugin popout 2025-11-18 16:03:30 -05:00
bbedward
4e6f0d5e87 bluez: fix disappearing popouts with modal maanger 2025-11-18 14:36:10 -05:00
bbedward
10639a5ead re-add bound lost my qmlfmt 2025-11-17 20:53:55 -05:00
bbedward
06d668e710 launcher: new search algo
- replace fzf.js with custom levenshtein distance matching
- tweak scoring system
- more graceful fuzzy, more weight to prefixes
- basic tokenization
2025-11-17 20:52:04 -05:00
bbedward
d1472dfcba osd: also have left center and right center options 2025-11-17 14:05:04 -05:00
bbedward
ccb4da3cd8 extws: fix force option 2025-11-17 10:08:06 -05:00
bbedward
46e96b49f0 extws: fix capability check & don't show names 2025-11-17 09:50:06 -05:00
bbedward
984cfe7f98 labwc: use dms dpms off/on for idle service 2025-11-17 09:12:38 -05:00
bbedward
d769300137 core/cli: add dpms off/on via wlr-output-power-management 2025-11-17 00:31:00 -05:00
Hikiru
d175d66828 Add NixOS module (#734)
* default.nix: fix "wavelength" typo

* Add nixos module

typo

fix

* nix: refactor and fix nix modules

* nix: fix NixOS module import

* nix: revert quickshell option change

* nix: fix nixosModules dmsPkgs definition

---------

Co-authored-by: LuckShiba <luckshiba@protonmail.com>
2025-11-16 21:12:01 -05:00
bbedward
c1a314332e wallpaper: rename blur layer option 2025-11-16 19:50:19 -05:00
bbedward
046ac59d21 core/extworkspace: only register outputs on name received 2025-11-16 19:40:46 -05:00
bbedward
00c06f07d0 workspace: fix ext-ws hiding 2025-11-16 18:52:12 -05:00
bbedward
3e2ab40c6a ws: 0 width when 0 workspaces, restore labwc to README 2025-11-16 17:53:50 -05:00
bbedward
350ffd0052 i18n: update terms 2025-11-16 16:33:55 -05:00
bbedward
ecd1a622d2 display: fix wallpaper when using monitor model 2025-11-16 16:33:21 -05:00
bbedward
f13968aa61 osd: configurable position 2025-11-16 16:27:01 -05:00
bbedward
4d1ffde54c launcher: allow launch prefix to run in shell 2025-11-16 16:14:19 -05:00
bbedward
d69017a706 also update per-monitor wallpaper to accout for display setting 2025-11-16 16:01:11 -05:00
bbedward
f2deaeccdb scaling: snap value reported by wlr-output 2025-11-16 15:56:59 -05:00
bbedward
ea9b0d2a79 powermenu: use consistent new-style on locker + greeter
fixes #739
2025-11-16 15:05:06 -05:00
bbedward
2e6dbedb8b dwl/mango: support keyboard layout 2025-11-16 14:24:56 -05:00
bbedward
6f359df8f9 displays: allow filtering by model over name 2025-11-16 13:58:53 -05:00
claymorwan
f6db20cd06 confirm-modal:add layer namespace (#743) 2025-11-16 13:09:44 -05:00
bbedward
6287fae065 running apps: don't wrap on scroll wheel
fixes #740
2025-11-16 13:06:40 -05:00
bbedward
e441607ce3 colorpicker: don't include line break in copy
fixes #741
2025-11-16 13:00:13 -05:00
bbedward
b5379a95fa qs/dankbar/meta: add a mask region to the bar
- Allows bar items to be clickable evn when popouts open
- Add state machines to manage state across monitors
- change focuses to ondemand on hyprland
2025-11-16 12:52:13 -05:00
bbedward
64ec5be919 wallpaper: empty input region 2025-11-15 23:41:24 -05:00
bbedward
3916512d66 systemtray: fix erroneous undefined condition 2025-11-15 21:46:34 -05:00
bbedward
e2f426a1bd Revert "systemtray: fix UI thread freeze when opening menu on Hyprland"
This reverts commit 4cb652abd9.
2025-11-15 21:42:50 -05:00
bbedward
aa1df8dfcf core: more syncmap conversions 2025-11-15 20:00:47 -05:00
bbedward
67557555f2 core: refactor to use a generic-compatible syncmap 2025-11-15 19:45:19 -05:00
bbedward
4cb652abd9 systemtray: fix UI thread freeze when opening menu on Hyprland
- Similar pattern as fix from Noctalia
2025-11-15 17:57:23 -05:00
bbedward
d11868b99f systray: don't try to force focus of menus 2025-11-15 14:57:47 -05:00
bbedward
1798417e6a systemtray: don't take keyboard focus
- bricks hyprland
2025-11-15 14:48:13 -05:00
github-actions[bot]
43dc3e5bb1 nix: update vendorHash for go.mod changes 2025-11-15 19:43:35 +00:00
bbedward
91891a14ed core/wayland: thread-safety meta fixes + cleanups + hypr workaround
- fork go-wayland/client and modify to make it thread-safe internally
- use sync.Map and atomic values in many places to cut down on mutex
  boilerplate
- do not create extworkspace client unless explicitly requested
2025-11-15 14:41:00 -05:00
bbedward
20f7d60147 settings: various consistency issues fixed
part of #725
2025-11-15 12:05:44 -05:00
bbedward
7e17e7d37a osd: fix opacity
part of #725
2025-11-15 11:43:05 -05:00
bbedward
cbb244f785 osd: add option to disable each OSD 2025-11-15 11:36:33 -05:00
Sunner
1c264d858b Follow symlinks when searching for sessions (#728) 2025-11-15 10:29:34 -05:00
bbedward
217037c2ae evdev: fix test 2025-11-14 23:26:14 -05:00
bbedward
b4dbd0b69c evdev: enhance keyboard detection for capslock 2025-11-14 23:22:06 -05:00
github-actions[bot]
89a2b5c00b chore: bump version to v0.5.2 2025-11-15 00:31:06 +00:00
bbedward
929b6dae1a widgets: fix some 0-width issues 2025-11-14 19:26:51 -05:00
Pi Home Server
52fe493da9 Feature/privacy widget - Settings to force icons on (#715)
* Update

* Update

* Update

* Update

* Update

* Set default to false

* Update SettingsData.qml

Set default visibility to false

* privacy widget: fix truncated settings menu

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-11-14 19:16:17 -05:00
purian23
3e6be3e762 Greet path updates 2025-11-14 17:54:35 -05:00
purian23
7a8cc449b9 Add local ACL greeter permissions to dms core installer 2025-11-14 16:32:06 -05:00
purian23
8f5a9d6e9f Update dms greeter to scan system & local directories 2025-11-14 15:36:14 -05:00
bbedward
1c5e31fea9 greeter: allow mangowc as compositor 2025-11-14 14:51:28 -05:00
claymorwan
fd08ae18ab feat: plugin layer namespace (#717) 2025-11-14 14:50:29 -05:00
bbedward
a7eb3de06e dankbar: configurable auto-hide delay 2025-11-14 14:00:37 -05:00
bbedward
8902dd7c44 launcher: grid re-style and customizable column counts 2025-11-14 13:54:44 -05:00
bbedward
6387d8400c osd: account for bar position when on bottom 2025-11-14 13:47:26 -05:00
bbedward
597cacb9cc matugen: update gtk4/gtk3-dark colors
- also some change to dankinstall to use niri/xwls from system repos,
  too lazy to split the commits
2025-11-14 13:20:59 -05:00
bbedward
3e285ad9ff dankdash: remove useless tint rectangle
part of #716
2025-11-14 13:09:46 -05:00
bbedward
cc1fa89790 clock: use precision minutes instead of seconds, unless needed
part of #716
2025-11-14 12:42:23 -05:00
bbedward
b0ed007751 core/dankinstall: more deb fixes 2025-11-14 12:22:13 -05:00
bbedward
e1e2650d2b core/dankinstall: fix hyprland util manual compile on debian 2025-11-14 12:13:49 -05:00
bbedward
b23f17b633 core/dankinstall: fix hyprpicker build 2025-11-14 12:07:03 -05:00
github-actions[bot]
818e40b2df nix: update vendorHash for go.mod changes 2025-11-14 17:06:06 +00:00
bbedward
5685e39631 core: improve evdev capslock detection, wayland context fixes 2025-11-14 12:04:47 -05:00
kritag
72534b7674 adding tokyonight, everforest, nord and rose-pine themes (#714)
Co-authored-by: Kristian Tagesen <kristian.tagesen@tietoevry.com>
2025-11-14 11:40:26 -05:00
bbedward
328490d23d powermenu: smarter positioning in control center 2025-11-14 10:45:16 -05:00
bbedward
97a0696930 clock: fix overview clock when seconds is on 2025-11-14 10:29:41 -05:00
bbedward
cb4e0660e0 dock: add reveal IPCs 2025-11-14 10:08:16 -05:00
bbedward
67c642de4c keybinds: add toggleWithPath 2025-11-14 09:03:27 -05:00
bbedward
0d7c2e1024 core/cli: fix keybind provider path override 2025-11-14 08:56:16 -05:00
bbedward
16a779a41b powermenu: restore grid as an option
fixes #712
2025-11-14 08:51:15 -05:00
purian23
c4ca3c8644 Add root dms-cli build script 2025-11-14 00:22:49 -05:00
bbedward
aabcbe34f3 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-14 00:06:50 -05:00
bbedward
f06626e441 dock: use modded app IDs for grouping logic
fixes #710
2025-11-14 00:06:27 -05:00
purian23
c4e1a71776 Relocate notification tests to scripts dir 2025-11-13 23:53:18 -05:00
bbedward
77e6c16bd2 core/extworkspace: fix some thread-safety issues 2025-11-13 23:52:32 -05:00
purian23
9d1fac3570 Relocate Nix dir under distro/nix 2025-11-13 23:47:00 -05:00
bbedward
b7aeaa7fc5 systemtray: better hide/unhide behavioro 2025-11-13 22:49:30 -05:00
bbedward
f6d8c9ff61 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-13 22:41:47 -05:00
bbedward
0490794d6c dankbar: add caps lock indicator widget 2025-11-13 22:41:33 -05:00
github-actions[bot]
335c83dd3c nix: update vendorHash for go.mod changes 2025-11-14 03:26:50 +00:00
bbedward
91da720c26 i18n:update translations 2025-11-13 22:25:22 -05:00
bbedward
b6ac744a68 Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-13 22:24:51 -05:00
bbedward
526c4092fd evdev: add evdev monitor for caps lock state 2025-11-13 22:24:27 -05:00
github-actions[bot]
ed06dda384 nix: update vendorHash for go.mod changes 2025-11-14 02:54:15 +00:00
bbedward
6465b11e9b core: ensure all NM tests use mock backend + re-orgs + dep updates 2025-11-13 21:44:03 -05:00
purian23
b2879878a1 feat: Priority pinned items in Control Center 2025-11-13 21:23:54 -05:00
bbedward
3e17b086fb ci: add docs to release archive 2025-11-13 20:19:54 -05:00
purian23
0545e6bcda Remove release tags 2025-11-13 20:01:38 -05:00
purian23
27a907433f Test Copr workflow update 2025-11-13 19:40:16 -05:00
purian23
69616800e3 Release update 2025-11-13 18:54:01 -05:00
github-actions[bot]
abf1f53432 chore: bump version to v0.5.1 2025-11-13 23:45:49 +00:00
bbedward
881c5f75cb ci: ensure version on tag 2025-11-13 18:44:03 -05:00
bbedward
4e45796ade ci: no flake version update 2025-11-13 18:38:47 -05:00
bbedward
1ce4ea5230 ci: update 2025-11-13 18:30:34 -05:00
purian23
f2a2437baa fix Copr dms-greeter 2025-11-13 18:00:30 -05:00
bbedward
508dc9db1e weather: imperial switch not just fahrenheit
fixes #699
2025-11-13 17:41:03 -05:00
bbedward
a914e3557f Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2025-11-13 17:31:29 -05:00
bbedward
f489dc062f dankinstall: fix variant passing 2025-11-13 17:31:02 -05:00
purian23
a7e09f4850 Update Copr dms-greeter paths 2025-11-13 17:29:22 -05:00
bbedward
8ea97530d4 matugen: add terminals always dark option 2025-11-13 17:19:37 -05:00
bbedward
13ab54e83a matugen: vscode theme repairs 2025-11-13 17:06:04 -05:00
bbedward
4bc40325cb hyprland: re-add special workspace filtering 2025-11-13 16:56:12 -05:00
bbedward
58d9355ea3 matugen: fix multi vscode themes 2025-11-13 16:51:16 -05:00
bbedward
d46b7528e7 systemtray: new tray detail menu 2025-11-13 16:30:07 -05:00
bbedward
1858597fc9 fix sudo usages 2025-11-13 15:41:41 -05:00
bbedward
83cce5afe4 dankinstall: re-simplify installation 2025-11-13 14:34:42 -05:00
bbedward
201bd8dc1f cli: fix greeter enable, and color sync 2025-11-13 13:21:18 -05:00
bbedward
b62ba69060 dankbar: fix hiding widgets that should not be enabled 2025-11-13 12:55:52 -05:00
bbedward
5d2f5557e5 dwl/mangowc: add layout switcher and viewer widget 2025-11-13 12:44:56 -05:00
bbedward
cf75c1aad0 show a power profile OSD 2025-11-13 10:23:14 -05:00
Saurabh
76a60df88b Feat: wezterm theming support (#705)
* implemented logic for wezterm theming

added matugen configs and dank16 functions, updated matugen worked
scripta

* fixed theme dir

fixed path and moved output location to default wezterm dir
2025-11-13 08:54:47 -05:00
bbedward
9322c79b4e nix: fix greeter path 2025-11-13 08:53:02 -05:00
Lucas
12365edcf0 flake: update to new monorepo structure (#701)
* nix: move alejandra.toml to root

* nix: build using local dms cli

* workflow: update update-vendor-hash to new structure
2025-11-13 00:26:03 -05:00
bbedward
5efc1f9dad powermenu: switch back to a list based style 2025-11-12 23:26:56 -05:00
bbedward
ab976cbb24 popout: add separate variable for layer override
fixes #700
2025-11-12 23:20:04 -05:00
bbedward
db584b7897 rename backend to core 2025-11-12 23:12:31 -05:00
bbedward
0fdc0748cf nix: fix flake 2025-11-12 22:44:17 -05:00
bbedward
2e79c21dc2 fedora: fix spec 2025-11-12 22:24:38 -05:00
bbedward
5490a230bd systemtray: fix menu positioning 2025-11-12 22:21:02 -05:00
bbedward
a6b059b30d don't gitignore Makefile 2025-11-12 22:19:08 -05:00
bbedward
712e6011aa fix contributing ref 2025-11-12 22:14:27 -05:00
bbedward
68f6f87410 disable vendor hash update 2025-11-12 22:06:46 -05:00
bbedward
50cdd68b7b un-gitignore dankinstall 2025-11-12 20:36:50 -05:00
bbedward
e8510b925e meta: monorepo updates 2025-11-12 20:34:58 -05:00
bbedward
24e800501a switch hto monorepo structure 2025-11-12 17:18:45 -05:00
1201 changed files with 269198 additions and 70047 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,24 +0,0 @@
#!/bin/bash
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

@@ -6,20 +6,12 @@ labels: "bug"
assignees: ""
---
<!-- If your issue is related to ICONS
<!-- 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://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
- 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. -->
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
Quickshell does not ever update its DesktopEntires.
There is an open PR for it, that has been stuck unmerged over there to fix it.
We unfortunately are at the mercy of quickshell to merge it.
Until then, newly installed and removed apps will not react until the
shell is restarted.
-->
## Compositor
- [ ] niri
@@ -70,4 +62,4 @@ Paste error messages or logs here
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->
<!-- If applicable, add screenshots or screen recordings -->

View File

@@ -30,4 +30,4 @@ Is this feature specific to one compositor?
## Alternatives/Existing Solutions
<!-- Include any similar/pre-existing products that solve this problem -->
<!-- Include any similar/pre-existing products that solve this problem -->

View File

@@ -37,4 +37,4 @@ assignees: ""
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->
<!-- If applicable, add screenshots or screen recordings -->

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,310 +0,0 @@
name: DMS Copr Stable Release
on:
workflow_run:
workflows: ["Create Release from DMS"]
types: [completed]
branches: [master]
workflow_dispatch:
inputs:
version:
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
required: false
default: ''
jobs:
build-and-upload:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
echo "Using manual version: $VERSION"
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
echo "Using latest release version from workflow_run: $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
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}
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 }}"
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/danklinux
%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/danklinux/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 -f %{buildroot}%{_datadir}/quickshell/dms/*.spec
%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-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
echo "✅ Spec file generated for v${VERSION}"
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 }}" >> $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

46
.github/workflows/go-ci.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Go CI
on:
push:
branches:
- "**"
paths:
- "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:
lint-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: core
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- 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

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

@@ -0,0 +1,15 @@
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: run pre-commit hooks
uses: j178/prek-action@v1

View File

@@ -1,47 +1,202 @@
# Release from a dispatch event from the danklinux repo
name: Create Release from DMS
name: Release
on:
repository_dispatch:
types: [dms_release]
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.event.client_payload.tag }}
group: release-${{ inputs.tag }}
cancel-in-progress: true
jobs:
create_release_from_dms:
runs-on: ubuntu-24.04
build-core:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [amd64, arm64]
defaults:
run:
working-directory: core
env:
TAG: ${{ github.event.client_payload.tag }}
DMS_REPO: ${{ github.event.client_payload.dms_repo }}
TAG: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Ensure VERSION and tag
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Format check
run: |
set -euxo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
echo "${TAG}" > VERSION
git add -A VERSION
if ! git diff --cached --quiet; then
git commit -m "Update VERSION to ${TAG} (from DMS)"
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
gofmt -s -l .
exit 1
fi
git tag -f "${TAG}"
- name: Run tests
run: go test -v ./...
git push origin HEAD
git push -f origin "${TAG}"
- name: Build dankinstall (${{ matrix.arch }})
env:
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ matrix.arch }}
cd ../..
gzip -9 -k dankinstall-${{ matrix.arch }}
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
- name: Build dms (${{ matrix.arch }})
env:
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-${{ matrix.arch }}
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
- name: Generate shell completions
if: matrix.arch == 'amd64'
run: |
set -eux
chmod +x dms-amd64
./dms-amd64 completion bash > completion.bash
./dms-amd64 completion fish > completion.fish
./dms-amd64 completion zsh > completion.zsh
- name: Build dms-distropkg (${{ matrix.arch }})
env:
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-distropkg-${{ matrix.arch }}
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
- name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v4
with:
name: core-assets-${{ matrix.arch }}
path: |
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts with completions
if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v4
with:
name: core-assets-${{ matrix.arch }}
path: |
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
core/completion.bash
core/completion.fish
core/completion.zsh
if-no-files-found: error
# 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: Checkout
# uses: actions/checkout@v4
# with:
# token: ${{ steps.app_token.outputs.token }}
# fetch-depth: 0
# - 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"
# version="${GITHUB_REF#refs/tags/}"
# echo "Updating to version: $version"
# echo "${version}" > 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
# 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]
env:
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 ${TAG}
git checkout ${TAG}
- name: Download core artifacts
uses: actions/download-artifact@v4
with:
pattern: core-assets-*
merge-multiple: true
path: ./_core_assets
- name: Generate Changelog
id: changelog
@@ -55,6 +210,12 @@ jobs:
fi
cat > RELEASE_BODY.md << 'EOF'
## Installation
```bash
curl -fsSL https://install.danklinux.com | sh
```
## Assets
### Complete Packages
@@ -66,6 +227,8 @@ jobs:
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-qml.tar.gz`** - QML source code only
### Checksums
@@ -89,30 +252,14 @@ jobs:
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create/Update DankMaterialShell Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
name: Release ${{ env.TAG }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ contains(env.TAG, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download and prepare release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare release assets
run: |
set -euxo pipefail
mkdir -p _release_assets
# Download DMS CLI binaries from the danklinux repo
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
# Rename CLI binaries to dms-cli-* format and copy distropkg binaries
for file in _dms_assets/dms-*.gz*; do
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
for file in _core_assets/dms-*.gz*; do
if [ -f "$file" ]; then
basename=$(basename "$file")
if [[ "$basename" == dms-distropkg-* ]]; then
@@ -124,40 +271,62 @@ jobs:
fi
done
# Create QML source package (exclude .git, .github, build artifacts)
tar --exclude='.git' \
# Copy dankinstall binaries
cp _core_assets/dankinstall-*.gz* _release_assets/
# Copy completions
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Create QML source package (exclude build artifacts and git files)
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
# 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' \
--exclude='_dms_assets' \
--exclude='_release_assets' \
--exclude='*.tar.gz' \
-czf _release_assets/dms-qml.tar.gz .
-czf ../_release_assets/dms-qml.tar.gz .)
# Generate checksum for QML package
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
# Create full packages for each architecture
for arch in amd64 arm64; do
mkdir -p _temp_full/dms
mkdir -p _temp_full/bin
mkdir -p _temp_full/completions
# Extract QML source
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
if [ -f "_dms_assets/dms-${arch}.gz" ]; then
gunzip -c "_dms_assets/dms-${arch}.gz" > _temp_full/bin/dms
# Add CLI binaries
if [ -f "_core_assets/dms-${arch}.gz" ]; then
gunzip -c "_core_assets/dms-${arch}.gz" > _temp_full/bin/dms
chmod +x _temp_full/bin/dms
fi
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
gunzip -c "_core_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
chmod +x _temp_full/bin/dms-distropkg
fi
for completion in _dms_assets/completion.*; do
# Add shell completions
for completion in _core_assets/completion.*; do
if [ -f "$completion" ]; then
cp "$completion" _temp_full/completions/
fi
done
cat > _temp_full/INSTALL.md << 'EOF'
# Copy docs directory
if [ -d "docs" ]; then
cp -r docs _temp_full/
fi
# Create installation guide
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
# DankMaterialShell Installation
## Requirements
@@ -204,10 +373,9 @@ jobs:
## Troubleshooting
- Run with verbose output: `quickshell -v -p ~/.config/quickshell/dms`
- Check logs in `~/.local/state/DankMaterialShell/`
- Run with verbose output: `DMS_LOG_LEVEL=debug dms run`
- Ensure all dependencies are installed
EOF
EOFINSTALL
# Create the full package
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
@@ -219,11 +387,14 @@ jobs:
rm -rf _temp_full
done
- name: Attach all assets to release
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
name: Release ${{ env.TAG }}
body: ${{ steps.changelog.outputs.changelog }}
files: _release_assets/**
draft: false
prerelease: ${{ contains(env.TAG, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

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

@@ -0,0 +1,406 @@
name: Update OBS Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to update (dms, dms-git, or all)"
required: false
default: "all"
tag_version:
description: "Specific tag version for dms stable (e.g., v1.0.2). Leave empty to auto-detect latest release."
required: false
default: ""
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
check_dms_stable() {
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\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.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag push - 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")
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
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
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: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag push event - use the pushed tag
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
# 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
# For explicit dms selection, require tag_version
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
VERSION="${{ github.event.inputs.tag_version }}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using specified tag: $VERSION"
else
echo "ERROR: tag_version is required when package=dms"
echo "Please specify a tag version (e.g., v1.0.2) or use package=all for auto-detection"
exit 1
fi
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
# For "all", auto-detect if tag_version not specified
if [[ -n "${{ github.event.inputs.tag_version }}" ]]; then
VERSION="${{ github.event.inputs.tag_version }}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using specified tag: $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 }}" ]] && [[ -z "${{ github.event.inputs.tag_version }}" ]]; 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
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

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

@@ -0,0 +1,19 @@
name: Update stable branch
on:
push:
tags:
- "v*"
jobs:
update-stable:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push to stable branch
run: git push origin HEAD:refs/heads/stable --force

View File

@@ -0,0 +1,66 @@
name: Update Vendor Hash
on:
workflow_dispatch:
push:
paths:
- "core/go.mod"
- "core/go.sum"
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
- name: Update vendorHash in flake.nix
run: |
set -euo pipefail
echo "Attempting nix build to get new vendorHash..."
if output=$(nix build .#dms-shell 2>&1); then
echo "Build succeeded, no hash update needed"
exit 0
fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' 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
echo "Verifying build with new vendorHash..."
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 "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" || 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

12
.gitignore vendored
View File

@@ -27,7 +27,6 @@ qrc_*.cpp
ui_*.h
*.qmlc
*.jsc
Makefile*
*build-*
*.qm
*.prl
@@ -97,8 +96,15 @@ go.work
go.work.sum
# env file
.env
.env*
# Editor/IDE
# .idea/
# .vscode/
# .vscode/
vim/
bin/
# direnv
.envrc
.direnv/

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]

17
CHANGELOG.MD Normal file
View File

@@ -0,0 +1,17 @@
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

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

View File

@@ -1,16 +0,0 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: modalManager
signal closeAllModalsExcept(var excludedModal)
function openModal(modal) {
if (!modal.allowStacking) {
closeAllModalsExcept(modal)
}
}
}

View File

@@ -1,65 +0,0 @@
pragma Singleton
import Quickshell
import QtCore
Singleton {
id: root
readonly property url home: StandardPaths.standardLocations(
StandardPaths.HomeLocation)[0]
readonly property url pictures: StandardPaths.standardLocations(
StandardPaths.PicturesLocation)[0]
readonly property url data: `${StandardPaths.standardLocations(
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
readonly property url state: `${StandardPaths.standardLocations(
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
readonly property url cache: `${StandardPaths.standardLocations(
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
readonly property url config: `${StandardPaths.standardLocations(
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
readonly property url imagecache: `${cache}/imagecache`
function stringify(path: url): string {
return path.toString().replace(/%20/g, " ")
}
function expandTilde(path: string): string {
return strip(path.replace("~", stringify(root.home)))
}
function shortenHome(path: string): string {
return path.replace(strip(root.home), "~")
}
function strip(path: url): string {
return stringify(path).replace("file://", "")
}
function toFileUrl(path: string): string {
return path.startsWith("file://") ? path : "file://" + path
}
function mkdir(path: url): void {
Quickshell.execDetached(["mkdir", "-p", strip(path)])
}
function copy(from: url, to: url): void {
Quickshell.execDetached(["cp", strip(from), strip(to)])
}
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
function moddedAppId(appId: string): string {
if (appId === "Spotify")
return "spotify"
if (appId === "beepertexts")
return "beeper"
if (appId === "home assistant desktop")
return "homeassistant-desktop"
if (appId.includes("com.transmissionbt.transmission"))
return "transmission-gtk"
return appId
}
}

View File

@@ -1,110 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property int defaultDebounceMs: 50
property int defaultTimeoutMs: 10000
property var _procDebouncers: ({})
function runCommand(id, command, callback, debounceMs, timeoutMs) {
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
const timeout = (typeof timeoutMs === "number" && timeoutMs > 0) ? timeoutMs : defaultTimeoutMs
let procId = id ? id : Math.random()
const isRandomId = !id
if (!_procDebouncers[procId]) {
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
t.triggered.connect(function() { _launchProc(procId, isRandomId) })
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait, timeoutMs: timeout, isRandomId: isRandomId }
} else {
_procDebouncers[procId].command = command
_procDebouncers[procId].callback = callback
_procDebouncers[procId].waitMs = wait
_procDebouncers[procId].timeoutMs = timeout
}
const entry = _procDebouncers[procId]
entry.timer.interval = entry.waitMs
entry.timer.restart()
}
function _launchProc(id, isRandomId) {
const entry = _procDebouncers[id]
if (!entry) return
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root)
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
proc.stdout = out
proc.stderr = err
proc.command = entry.command
let capturedOut = ""
let capturedErr = ""
let exitSeen = false
let exitCodeValue = -1
let outSeen = false
let errSeen = false
let timedOut = false
timeoutTimer.interval = entry.timeoutMs
timeoutTimer.triggered.connect(function() {
if (!exitSeen) {
timedOut = true
proc.running = false
exitSeen = true
exitCodeValue = 124
maybeComplete()
}
})
out.streamFinished.connect(function() {
capturedOut = out.text || ""
outSeen = true
maybeComplete()
})
err.streamFinished.connect(function() {
capturedErr = err.text || ""
errSeen = true
maybeComplete()
})
proc.exited.connect(function(code) {
timeoutTimer.stop()
exitSeen = true
exitCodeValue = code
maybeComplete()
})
function maybeComplete() {
if (!exitSeen || !outSeen || !errSeen) return
timeoutTimer.stop()
if (typeof entry.callback === "function") {
try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) }
}
try { proc.destroy() } catch (_) {}
try { timeoutTimer.destroy() } catch (_) {}
if (isRandomId || entry.isRandomId) {
Qt.callLater(function() {
if (_procDebouncers[id]) {
try { _procDebouncers[id].timer.destroy() } catch (_) {}
delete _procDebouncers[id]
}
})
}
}
proc.running = true
timeoutTimer.start()
}
}

View File

@@ -1,975 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
Singleton {
id: root
readonly property int sessionConfigVersion: 1
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
property bool hasTriedDefaultSession: false
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl)
property bool isLightMode: false
property bool doNotDisturb: false
property bool isSwitchingMode: false
property string wallpaperPath: ""
property bool perMonitorWallpaper: false
property var monitorWallpapers: ({})
property bool perModeWallpaper: false
property string wallpaperPathLight: ""
property string wallpaperPathDark: ""
property var monitorWallpapersLight: ({})
property var monitorWallpapersDark: ({})
property string wallpaperTransition: "fade"
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
property bool wallpaperCyclingEnabled: false
property string wallpaperCyclingMode: "interval"
property int wallpaperCyclingInterval: 300
property string wallpaperCyclingTime: "06:00"
property var monitorCyclingSettings: ({})
property bool nightModeEnabled: false
property int nightModeTemperature: 4500
property int nightModeHighTemperature: 6500
property bool nightModeAutoEnabled: false
property string nightModeAutoMode: "time"
property int nightModeStartHour: 18
property int nightModeStartMinute: 0
property int nightModeEndHour: 6
property int nightModeEndMinute: 0
property real latitude: 0.0
property real longitude: 0.0
property bool nightModeUseIPLocation: false
property string nightModeLocationProvider: ""
property var pinnedApps: []
property var hiddenTrayIds: []
property var recentColors: []
property bool showThirdPartyPlugins: false
property string launchPrefix: ""
property string lastBrightnessDevice: ""
property var brightnessExponentialDevices: ({})
property var brightnessUserSetValues: ({})
property var brightnessExponentValues: ({})
property int selectedGpuIndex: 0
property bool nvidiaGpuTempEnabled: false
property bool nonNvidiaGpuTempEnabled: false
property var enabledGpuPciIds: []
Component.onCompleted: {
if (!isGreeterMode) {
loadSettings()
}
}
function loadSettings() {
if (isGreeterMode) {
parseSettings(greeterSessionFile.text())
} else {
parseSettings(settingsFile.text())
}
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
if (settings.wallpaperPath && settings.wallpaperPath.startsWith("we:")) {
console.warn("WallpaperEngine wallpaper detected, resetting wallpaper")
wallpaperPath = ""
Quickshell.execDetached([
"notify-send",
"-u", "critical",
"-a", "DMS",
"-i", "dialog-warning",
"WallpaperEngine Support Moved",
"WallpaperEngine support has been moved to a plugin. Please enable the Linux Wallpaper Engine plugin in Settings → Plugins to continue using WallpaperEngine."
])
} else {
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
}
perMonitorWallpaper = settings.perMonitorWallpaper !== undefined ? settings.perMonitorWallpaper : false
monitorWallpapers = settings.monitorWallpapers !== undefined ? settings.monitorWallpapers : {}
perModeWallpaper = settings.perModeWallpaper !== undefined ? settings.perModeWallpaper : false
wallpaperPathLight = settings.wallpaperPathLight !== undefined ? settings.wallpaperPathLight : ""
wallpaperPathDark = settings.wallpaperPathDark !== undefined ? settings.wallpaperPathDark : ""
monitorWallpapersLight = settings.monitorWallpapersLight !== undefined ? settings.monitorWallpapersLight : {}
monitorWallpapersDark = settings.monitorWallpapersDark !== undefined ? settings.monitorWallpapersDark : {}
brightnessExponentialDevices = settings.brightnessExponentialDevices !== undefined ? settings.brightnessExponentialDevices : (settings.brightnessLogarithmicDevices || {})
brightnessUserSetValues = settings.brightnessUserSetValues !== undefined ? settings.brightnessUserSetValues : {}
brightnessExponentValues = settings.brightnessExponentValues !== undefined ? settings.brightnessExponentValues : {}
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
nightModeHighTemperature = settings.nightModeHighTemperature !== undefined ? settings.nightModeHighTemperature : 6500
nightModeAutoEnabled = settings.nightModeAutoEnabled !== undefined ? settings.nightModeAutoEnabled : false
nightModeAutoMode = settings.nightModeAutoMode !== undefined ? settings.nightModeAutoMode : "time"
if (settings.nightModeStartTime !== undefined) {
const parts = settings.nightModeStartTime.split(":")
nightModeStartHour = parseInt(parts[0]) || 18
nightModeStartMinute = parseInt(parts[1]) || 0
} else {
nightModeStartHour = settings.nightModeStartHour !== undefined ? settings.nightModeStartHour : 18
nightModeStartMinute = settings.nightModeStartMinute !== undefined ? settings.nightModeStartMinute : 0
}
if (settings.nightModeEndTime !== undefined) {
const parts = settings.nightModeEndTime.split(":")
nightModeEndHour = parseInt(parts[0]) || 6
nightModeEndMinute = parseInt(parts[1]) || 0
} else {
nightModeEndHour = settings.nightModeEndHour !== undefined ? settings.nightModeEndHour : 6
nightModeEndMinute = settings.nightModeEndMinute !== undefined ? settings.nightModeEndMinute : 0
}
latitude = settings.latitude !== undefined ? settings.latitude : 0.0
longitude = settings.longitude !== undefined ? settings.longitude : 0.0
nightModeUseIPLocation = settings.nightModeUseIPLocation !== undefined ? settings.nightModeUseIPLocation : false
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
hiddenTrayIds = settings.hiddenTrayIds !== undefined ? settings.hiddenTrayIds : []
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
monitorCyclingSettings = settings.monitorCyclingSettings !== undefined ? settings.monitorCyclingSettings : {}
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
launchPrefix = settings.launchPrefix !== undefined ? settings.launchPrefix : ""
wallpaperTransition = settings.wallpaperTransition !== undefined ? settings.wallpaperTransition : "fade"
includedTransitions = settings.includedTransitions !== undefined ? settings.includedTransitions : availableWallpaperTransitions.filter(t => t !== "none")
recentColors = settings.recentColors !== undefined ? settings.recentColors : []
showThirdPartyPlugins = settings.showThirdPartyPlugins !== undefined ? settings.showThirdPartyPlugins : false
if (settings.configVersion === undefined) {
migrateFromUndefinedToV1(settings)
saveSettings()
} else if (settings.configVersion === sessionConfigVersion) {
cleanupUnusedKeys()
}
if (!isGreeterMode) {
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
if (typeof WallpaperCyclingService !== "undefined") {
WallpaperCyclingService.updateCyclingState()
}
}
} catch (e) {
}
}
function saveSettings() {
if (isGreeterMode)
return
settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode,
"wallpaperPath": wallpaperPath,
"perMonitorWallpaper": perMonitorWallpaper,
"monitorWallpapers": monitorWallpapers,
"perModeWallpaper": perModeWallpaper,
"wallpaperPathLight": wallpaperPathLight,
"wallpaperPathDark": wallpaperPathDark,
"monitorWallpapersLight": monitorWallpapersLight,
"monitorWallpapersDark": monitorWallpapersDark,
"brightnessExponentialDevices": brightnessExponentialDevices,
"brightnessUserSetValues": brightnessUserSetValues,
"brightnessExponentValues": brightnessExponentValues,
"doNotDisturb": doNotDisturb,
"nightModeEnabled": nightModeEnabled,
"nightModeTemperature": nightModeTemperature,
"nightModeHighTemperature": nightModeHighTemperature,
"nightModeAutoEnabled": nightModeAutoEnabled,
"nightModeAutoMode": nightModeAutoMode,
"nightModeStartHour": nightModeStartHour,
"nightModeStartMinute": nightModeStartMinute,
"nightModeEndHour": nightModeEndHour,
"nightModeEndMinute": nightModeEndMinute,
"latitude": latitude,
"longitude": longitude,
"nightModeUseIPLocation": nightModeUseIPLocation,
"nightModeLocationProvider": nightModeLocationProvider,
"pinnedApps": pinnedApps,
"hiddenTrayIds": hiddenTrayIds,
"selectedGpuIndex": selectedGpuIndex,
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
"enabledGpuPciIds": enabledGpuPciIds,
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
"wallpaperCyclingMode": wallpaperCyclingMode,
"wallpaperCyclingInterval": wallpaperCyclingInterval,
"wallpaperCyclingTime": wallpaperCyclingTime,
"monitorCyclingSettings": monitorCyclingSettings,
"lastBrightnessDevice": lastBrightnessDevice,
"launchPrefix": launchPrefix,
"wallpaperTransition": wallpaperTransition,
"includedTransitions": includedTransitions,
"recentColors": recentColors,
"showThirdPartyPlugins": showThirdPartyPlugins,
"configVersion": sessionConfigVersion
}, null, 2))
}
function migrateFromUndefinedToV1(settings) {
console.info("SessionData: Migrating configuration from undefined to version 1")
if (typeof SettingsData !== "undefined") {
if (settings.acMonitorTimeout !== undefined) {
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout)
}
if (settings.acLockTimeout !== undefined) {
SettingsData.set("acLockTimeout", settings.acLockTimeout)
}
if (settings.acSuspendTimeout !== undefined) {
SettingsData.set("acSuspendTimeout", settings.acSuspendTimeout)
}
if (settings.acHibernateTimeout !== undefined) {
SettingsData.set("acHibernateTimeout", settings.acHibernateTimeout)
}
if (settings.batteryMonitorTimeout !== undefined) {
SettingsData.set("batteryMonitorTimeout", settings.batteryMonitorTimeout)
}
if (settings.batteryLockTimeout !== undefined) {
SettingsData.set("batteryLockTimeout", settings.batteryLockTimeout)
}
if (settings.batterySuspendTimeout !== undefined) {
SettingsData.set("batterySuspendTimeout", settings.batterySuspendTimeout)
}
if (settings.batteryHibernateTimeout !== undefined) {
SettingsData.set("batteryHibernateTimeout", settings.batteryHibernateTimeout)
}
if (settings.lockBeforeSuspend !== undefined) {
SettingsData.set("lockBeforeSuspend", settings.lockBeforeSuspend)
}
if (settings.loginctlLockIntegration !== undefined) {
SettingsData.set("loginctlLockIntegration", settings.loginctlLockIntegration)
}
if (settings.launchPrefix !== undefined) {
SettingsData.set("launchPrefix", settings.launchPrefix)
}
}
if (typeof CacheData !== "undefined") {
if (settings.wallpaperLastPath !== undefined) {
CacheData.wallpaperLastPath = settings.wallpaperLastPath
}
if (settings.profileLastPath !== undefined) {
CacheData.profileLastPath = settings.profileLastPath
}
CacheData.saveCache()
}
}
function cleanupUnusedKeys() {
const validKeys = ["isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper", "wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight", "monitorWallpapersDark", "doNotDisturb", "nightModeEnabled", "nightModeTemperature", "nightModeHighTemperature", "nightModeAutoEnabled", "nightModeAutoMode", "nightModeStartHour", "nightModeStartMinute", "nightModeEndHour", "nightModeEndMinute", "latitude", "longitude", "nightModeUseIPLocation", "nightModeLocationProvider", "pinnedApps", "hiddenTrayIds", "selectedGpuIndex", "nvidiaGpuTempEnabled", "nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled", "wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime", "monitorCyclingSettings", "lastBrightnessDevice", "brightnessExponentialDevices", "brightnessUserSetValues", "brightnessExponentValues", "launchPrefix", "wallpaperTransition", "includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"]
try {
const content = settingsFile.text()
if (!content || !content.trim())
return
const settings = JSON.parse(content)
let needsSave = false
for (const key in settings) {
if (!validKeys.includes(key)) {
console.log("SessionData: Removing unused key:", key)
delete settings[key]
needsSave = true
}
}
if (needsSave) {
settingsFile.setText(JSON.stringify(settings, null, 2))
}
} catch (e) {
console.warn("SessionData: Failed to cleanup unused keys:", e.message)
}
}
function setLightMode(lightMode) {
isSwitchingMode = true
isLightMode = lightMode
syncWallpaperForCurrentMode()
saveSettings()
Qt.callLater(() => { isSwitchingMode = false })
}
function setDoNotDisturb(enabled) {
doNotDisturb = enabled
saveSettings()
}
function setWallpaperPath(path) {
wallpaperPath = path
saveSettings()
}
function setWallpaper(imagePath) {
wallpaperPath = imagePath
if (perModeWallpaper) {
if (isLightMode) {
wallpaperPathLight = imagePath
} else {
wallpaperPathDark = imagePath
}
}
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setWallpaperColor(color) {
wallpaperPath = color
if (perModeWallpaper) {
if (isLightMode) {
wallpaperPathLight = color
} else {
wallpaperPathDark = color
}
}
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function clearWallpaper() {
wallpaperPath = ""
saveSettings()
if (typeof Theme !== "undefined") {
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
Theme.switchTheme(SettingsData.theme)
} else {
Theme.switchTheme("blue")
}
}
}
function setPerMonitorWallpaper(enabled) {
perMonitorWallpaper = enabled
if (enabled && perModeWallpaper) {
syncWallpaperForCurrentMode()
}
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setPerModeWallpaper(enabled) {
if (enabled && wallpaperCyclingEnabled) {
setWallpaperCyclingEnabled(false)
}
if (enabled && perMonitorWallpaper) {
var monitorCyclingAny = false
for (var key in monitorCyclingSettings) {
if (monitorCyclingSettings[key].enabled) {
monitorCyclingAny = true
break
}
}
if (monitorCyclingAny) {
var newSettings = Object.assign({}, monitorCyclingSettings)
for (var screenName in newSettings) {
newSettings[screenName].enabled = false
}
monitorCyclingSettings = newSettings
}
}
perModeWallpaper = enabled
if (enabled) {
if (perMonitorWallpaper) {
monitorWallpapersLight = Object.assign({}, monitorWallpapers)
monitorWallpapersDark = Object.assign({}, monitorWallpapers)
} else {
wallpaperPathLight = wallpaperPath
wallpaperPathDark = wallpaperPath
}
} else {
syncWallpaperForCurrentMode()
}
saveSettings()
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setMonitorWallpaper(screenName, path) {
var newMonitorWallpapers = Object.assign({}, monitorWallpapers)
if (path && path !== "") {
newMonitorWallpapers[screenName] = path
} else {
delete newMonitorWallpapers[screenName]
}
monitorWallpapers = newMonitorWallpapers
if (perModeWallpaper) {
if (isLightMode) {
var newLight = Object.assign({}, monitorWallpapersLight)
if (path && path !== "") {
newLight[screenName] = path
} else {
delete newLight[screenName]
}
monitorWallpapersLight = newLight
} else {
var newDark = Object.assign({}, monitorWallpapersDark)
if (path && path !== "") {
newDark[screenName] = path
} else {
delete newDark[screenName]
}
monitorWallpapersDark = newDark
}
}
saveSettings()
if (typeof Theme !== "undefined" && typeof Quickshell !== "undefined" && typeof SettingsData !== "undefined") {
var screens = Quickshell.screens
if (screens.length > 0) {
var targetMonitor = (SettingsData.matugenTargetMonitor && SettingsData.matugenTargetMonitor !== "") ? SettingsData.matugenTargetMonitor : screens[0].name
if (screenName === targetMonitor) {
Theme.generateSystemThemesFromCurrentTheme()
}
}
}
}
function setWallpaperTransition(transition) {
wallpaperTransition = transition
saveSettings()
}
function setWallpaperCyclingEnabled(enabled) {
wallpaperCyclingEnabled = enabled
saveSettings()
}
function setWallpaperCyclingMode(mode) {
wallpaperCyclingMode = mode
saveSettings()
}
function setWallpaperCyclingInterval(interval) {
wallpaperCyclingInterval = interval
saveSettings()
}
function setWallpaperCyclingTime(time) {
wallpaperCyclingTime = time
saveSettings()
}
function setMonitorCyclingEnabled(screenName, enabled) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
}
newSettings[screenName].enabled = enabled
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingMode(screenName, mode) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
}
newSettings[screenName].mode = mode
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingInterval(screenName, interval) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
}
newSettings[screenName].interval = interval
monitorCyclingSettings = newSettings
saveSettings()
}
function setMonitorCyclingTime(screenName, time) {
var newSettings = Object.assign({}, monitorCyclingSettings)
if (!newSettings[screenName]) {
newSettings[screenName] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
}
newSettings[screenName].time = time
monitorCyclingSettings = newSettings
saveSettings()
}
function setNightModeEnabled(enabled) {
nightModeEnabled = enabled
saveSettings()
}
function setNightModeTemperature(temperature) {
nightModeTemperature = temperature
saveSettings()
}
function setNightModeHighTemperature(temperature) {
nightModeHighTemperature = temperature
saveSettings()
}
function setNightModeAutoEnabled(enabled) {
console.log("SessionData: Setting nightModeAutoEnabled to", enabled)
nightModeAutoEnabled = enabled
saveSettings()
}
function setNightModeAutoMode(mode) {
nightModeAutoMode = mode
saveSettings()
}
function setNightModeStartHour(hour) {
nightModeStartHour = hour
saveSettings()
}
function setNightModeStartMinute(minute) {
nightModeStartMinute = minute
saveSettings()
}
function setNightModeEndHour(hour) {
nightModeEndHour = hour
saveSettings()
}
function setNightModeEndMinute(minute) {
nightModeEndMinute = minute
saveSettings()
}
function setNightModeUseIPLocation(use) {
nightModeUseIPLocation = use
saveSettings()
}
function setLatitude(lat) {
console.log("SessionData: Setting latitude to", lat)
latitude = lat
saveSettings()
}
function setLongitude(lng) {
console.log("SessionData: Setting longitude to", lng)
longitude = lng
saveSettings()
}
function setNightModeLocationProvider(provider) {
nightModeLocationProvider = provider
saveSettings()
}
function setPinnedApps(apps) {
pinnedApps = apps
saveSettings()
}
function addPinnedApp(appId) {
if (!appId)
return
var currentPinned = [...pinnedApps]
if (currentPinned.indexOf(appId) === -1) {
currentPinned.push(appId)
setPinnedApps(currentPinned)
}
}
function removePinnedApp(appId) {
if (!appId)
return
var currentPinned = pinnedApps.filter(id => id !== appId)
setPinnedApps(currentPinned)
}
function isPinnedApp(appId) {
return appId && pinnedApps.indexOf(appId) !== -1
}
function hideTrayId(trayId) {
if (!trayId) return
const current = [...hiddenTrayIds]
if (current.indexOf(trayId) === -1) {
current.push(trayId)
hiddenTrayIds = current
saveSettings()
}
}
function showTrayId(trayId) {
if (!trayId) return
hiddenTrayIds = hiddenTrayIds.filter(id => id !== trayId)
saveSettings()
}
function isHiddenTrayId(trayId) {
return trayId && hiddenTrayIds.indexOf(trayId) !== -1
}
function addRecentColor(color) {
const colorStr = color.toString()
let recent = recentColors.slice()
recent = recent.filter(c => c !== colorStr)
recent.unshift(colorStr)
if (recent.length > 5)
recent = recent.slice(0, 5)
recentColors = recent
saveSettings()
}
function setShowThirdPartyPlugins(enabled) {
showThirdPartyPlugins = enabled
saveSettings()
}
function setLaunchPrefix(prefix) {
launchPrefix = prefix
saveSettings()
}
function setLastBrightnessDevice(device) {
lastBrightnessDevice = device
saveSettings()
}
function setBrightnessExponential(deviceName, enabled) {
var newSettings = Object.assign({}, brightnessExponentialDevices)
if (enabled) {
newSettings[deviceName] = true
} else {
delete newSettings[deviceName]
}
brightnessExponentialDevices = newSettings
saveSettings()
if (typeof DisplayService !== "undefined") {
DisplayService.updateDeviceBrightnessDisplay(deviceName)
}
}
function getBrightnessExponential(deviceName) {
return brightnessExponentialDevices[deviceName] === true
}
function setBrightnessUserSetValue(deviceName, value) {
var newValues = Object.assign({}, brightnessUserSetValues)
newValues[deviceName] = value
brightnessUserSetValues = newValues
saveSettings()
}
function getBrightnessUserSetValue(deviceName) {
return brightnessUserSetValues[deviceName]
}
function setBrightnessExponent(deviceName, exponent) {
var newValues = Object.assign({}, brightnessExponentValues)
if (exponent !== undefined && exponent !== null) {
newValues[deviceName] = exponent
} else {
delete newValues[deviceName]
}
brightnessExponentValues = newValues
saveSettings()
}
function getBrightnessExponent(deviceName) {
const value = brightnessExponentValues[deviceName]
return value !== undefined ? value : 1.2
}
function setSelectedGpuIndex(index) {
selectedGpuIndex = index
saveSettings()
}
function setNvidiaGpuTempEnabled(enabled) {
nvidiaGpuTempEnabled = enabled
saveSettings()
}
function setNonNvidiaGpuTempEnabled(enabled) {
nonNvidiaGpuTempEnabled = enabled
saveSettings()
}
function setEnabledGpuPciIds(pciIds) {
enabledGpuPciIds = pciIds
saveSettings()
}
function syncWallpaperForCurrentMode() {
if (!perModeWallpaper)
return
if (perMonitorWallpaper) {
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark)
return
}
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark
}
function getMonitorWallpaper(screenName) {
if (!perMonitorWallpaper) {
return wallpaperPath
}
return monitorWallpapers[screenName] || wallpaperPath
}
function getMonitorCyclingSettings(screenName) {
return monitorCyclingSettings[screenName] || {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
}
}
FileView {
id: settingsFile
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: isGreeterMode
blockWrites: true
watchChanges: !isGreeterMode
onLoaded: {
if (!isGreeterMode) {
parseSettings(settingsFile.text())
hasTriedDefaultSession = false
}
}
onLoadFailed: error => {
if (!isGreeterMode && !hasTriedDefaultSession) {
hasTriedDefaultSession = true
defaultSessionCheckProcess.running = true
}
}
}
FileView {
id: greeterSessionFile
path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
return greetCfgDir + "/session.json"
}
preload: isGreeterMode
blockLoading: false
blockWrites: true
watchChanges: false
printErrors: true
onLoaded: {
if (isGreeterMode) {
parseSettings(greeterSessionFile.text())
}
}
}
Process {
id: defaultSessionCheckProcess
command: ["sh", "-c", "CONFIG_DIR=\"" + _stateDir
+ "/DankMaterialShell\"; if [ -f \"$CONFIG_DIR/default-session.json\" ] && [ ! -f \"$CONFIG_DIR/session.json\" ]; then cp --no-preserve=mode \"$CONFIG_DIR/default-session.json\" \"$CONFIG_DIR/session.json\" && echo 'copied'; else echo 'not_found'; fi"]
running: false
onExited: exitCode => {
if (exitCode === 0) {
console.info("Copied default-session.json to session.json")
settingsFile.reload()
}
}
}
IpcHandler {
target: "wallpaper"
function get(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use getFor(screenName) instead."
}
return root.wallpaperPath || ""
}
function set(path: string): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use setFor(screenName, path) instead."
}
if (!path) {
return "ERROR: No path provided"
}
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
try {
root.setWallpaper(absolutePath)
return "SUCCESS: Wallpaper set to " + absolutePath
} catch (e) {
return "ERROR: Failed to set wallpaper: " + e.toString()
}
}
function clear(): string {
root.setWallpaper("")
root.setPerMonitorWallpaper(false)
root.monitorWallpapers = {}
root.saveSettings()
return "SUCCESS: All wallpapers cleared"
}
function next(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use nextFor(screenName) instead."
}
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set"
}
try {
WallpaperCyclingService.cycleNextManually()
return "SUCCESS: Cycling to next wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
function prev(): string {
if (root.perMonitorWallpaper) {
return "ERROR: Per-monitor mode enabled. Use prevFor(screenName) instead."
}
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set"
}
try {
WallpaperCyclingService.cyclePrevManually()
return "SUCCESS: Cycling to previous wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
function getFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided"
}
return root.getMonitorWallpaper(screenName) || ""
}
function setFor(screenName: string, path: string): string {
if (!screenName) {
return "ERROR: No screen name provided"
}
if (!path) {
return "ERROR: No path provided"
}
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
try {
if (!root.perMonitorWallpaper) {
root.setPerMonitorWallpaper(true)
}
root.setMonitorWallpaper(screenName, absolutePath)
return "SUCCESS: Wallpaper set for " + screenName + " to " + absolutePath
} catch (e) {
return "ERROR: Failed to set wallpaper for " + screenName + ": " + e.toString()
}
}
function nextFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided"
}
var currentWallpaper = root.getMonitorWallpaper(screenName)
if (!currentWallpaper) {
return "ERROR: No wallpaper set for " + screenName
}
try {
WallpaperCyclingService.cycleNextForMonitor(screenName)
return "SUCCESS: Cycling to next wallpaper for " + screenName
} catch (e) {
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
}
}
function prevFor(screenName: string): string {
if (!screenName) {
return "ERROR: No screen name provided"
}
var currentWallpaper = root.getMonitorWallpaper(screenName)
if (!currentWallpaper) {
return "ERROR: No wallpaper set for " + screenName
}
try {
WallpaperCyclingService.cyclePrevForMonitor(screenName)
return "SUCCESS: Cycling to previous wallpaper for " + screenName
} catch (e) {
return "ERROR: Failed to cycle wallpaper for " + screenName + ": " + e.toString()
}
}
}
}

View File

@@ -1,943 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Common.settings
import qs.Services
import "settings/SettingsSpec.js" as Spec
import "settings/SettingsStore.js" as Store
Singleton {
id: root
readonly property int settingsConfigVersion: 1
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
enum Position {
Top,
Bottom,
Left,
Right
}
enum AnimationSpeed {
None,
Short,
Medium,
Long,
Custom
}
enum SuspendBehavior {
Suspend,
Hibernate,
SuspendThenHibernate
}
readonly property string defaultFontFamily: "Inter Variable"
readonly property string defaultMonoFontFamily: "Fira Code"
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
readonly property string _configDir: Paths.strip(_configUrl)
readonly property string pluginSettingsPath: _configDir + "/DankMaterialShell/plugin_settings.json"
property bool _loading: false
property bool _pluginSettingsLoading: false
property bool hasTriedDefaultSettings: false
property var pluginSettings: ({})
property alias dankBarLeftWidgetsModel: leftWidgetsModel
property alias dankBarCenterWidgetsModel: centerWidgetsModel
property alias dankBarRightWidgetsModel: rightWidgetsModel
property string currentThemeName: "blue"
property string customThemeFile: ""
property string matugenScheme: "scheme-tonal-spot"
property bool runUserMatugenTemplates: true
property string matugenTargetMonitor: ""
property real dankBarTransparency: 1.0
property real dankBarWidgetTransparency: 1.0
property real popupTransparency: 1.0
property real dockTransparency: 1
property string widgetBackgroundColor: "sch"
property real cornerRadius: 12
property bool use24HourClock: true
property bool showSeconds: false
property bool useFahrenheit: false
property bool nightModeEnabled: false
property int animationSpeed: SettingsData.AnimationSpeed.Short
property int customAnimationDuration: 500
property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false
property bool showLauncherButton: true
property bool showWorkspaceSwitcher: true
property bool showFocusedWindow: true
property bool showWeather: true
property bool showMusic: true
property bool showClipboard: true
property bool showCpuUsage: true
property bool showMemUsage: true
property bool showCpuTemp: true
property bool showGpuTemp: true
property int selectedGpuIndex: 0
property var enabledGpuPciIds: []
property bool showSystemTray: true
property bool showClock: true
property bool showNotificationButton: true
property bool showBattery: true
property bool showControlCenterButton: true
property bool controlCenterShowNetworkIcon: true
property bool controlCenterShowBluetoothIcon: true
property bool controlCenterShowAudioIcon: true
property var controlCenterWidgets: [{
"id": "volumeSlider",
"enabled": true,
"width": 50
}, {
"id": "brightnessSlider",
"enabled": true,
"width": 50
}, {
"id": "wifi",
"enabled": true,
"width": 50
}, {
"id": "bluetooth",
"enabled": true,
"width": 50
}, {
"id": "audioOutput",
"enabled": true,
"width": 50
}, {
"id": "audioInput",
"enabled": true,
"width": 50
}, {
"id": "nightMode",
"enabled": true,
"width": 50
}, {
"id": "darkMode",
"enabled": true,
"width": 50
}]
property bool showWorkspaceIndex: false
property bool showWorkspacePadding: false
property bool workspaceScrolling: false
property bool showWorkspaceApps: false
property int maxWorkspaceIcons: 3
property bool workspacesPerMonitor: true
property bool dwlShowAllTags: false
property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true
property bool clockCompactMode: false
property bool focusedWindowCompactMode: false
property bool runningAppsCompactMode: true
property bool keyboardLayoutNameCompactMode: false
property bool runningAppsCurrentWorkspace: false
property bool runningAppsGroupByApp: false
property string clockDateFormat: ""
property string lockDateFormat: ""
property int mediaSize: 1
property var dankBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
property var dankBarCenterWidgets: ["music", "clock", "weather"]
property var dankBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
property var dankBarWidgetOrder: []
property string appLauncherViewMode: "list"
property string spotlightModalViewMode: "list"
property bool sortAppsAlphabetically: false
property string weatherLocation: "New York, NY"
property string weatherCoordinates: "40.7128,-74.0060"
property bool useAutoLocation: false
property bool weatherEnabled: true
property string networkPreference: "auto"
property string vpnLastConnected: ""
property string iconTheme: "System Default"
property var availableIconThemes: ["System Default"]
property string systemDefaultIconTheme: ""
property bool qt5ctAvailable: false
property bool qt6ctAvailable: false
property bool gtkAvailable: false
property string launcherLogoMode: "apps"
property string launcherLogoCustomPath: ""
property string launcherLogoColorOverride: ""
property bool launcherLogoColorInvertOnMode: false
property real launcherLogoBrightness: 0.5
property real launcherLogoContrast: 1
property int launcherLogoSizeOffset: 0
property string fontFamily: "Inter Variable"
property string monoFontFamily: "Fira Code"
property int fontWeight: Font.Normal
property real fontScale: 1.0
property real dankBarFontScale: 1.0
property bool notepadUseMonospace: true
property string notepadFontFamily: ""
property real notepadFontSize: 14
property bool notepadShowLineNumbers: false
property real notepadTransparencyOverride: -1
property real notepadLastCustomTransparency: 0.7
onNotepadUseMonospaceChanged: saveSettings()
onNotepadFontFamilyChanged: saveSettings()
onNotepadFontSizeChanged: saveSettings()
onNotepadShowLineNumbersChanged: saveSettings()
onNotepadTransparencyOverrideChanged: {
if (notepadTransparencyOverride > 0) {
notepadLastCustomTransparency = notepadTransparencyOverride
}
saveSettings()
}
onNotepadLastCustomTransparencyChanged: saveSettings()
property bool soundsEnabled: true
property bool useSystemSoundTheme: false
property bool soundNewNotification: true
property bool soundVolumeChanged: true
property bool soundPluggedIn: true
property int acMonitorTimeout: 0
property int acLockTimeout: 0
property int acSuspendTimeout: 0
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
property int batteryMonitorTimeout: 0
property int batteryLockTimeout: 0
property int batterySuspendTimeout: 0
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
property bool lockBeforeSuspend: false
property bool preventIdleForMedia: false
property bool loginctlLockIntegration: true
property string launchPrefix: ""
property var brightnessDevicePins: ({})
property bool gtkThemingEnabled: false
property bool qtThemingEnabled: false
property bool syncModeWithPortal: true
property bool showDock: false
property bool dockAutoHide: false
property bool dockGroupByApp: false
property bool dockOpenOnOverview: false
property int dockPosition: SettingsData.Position.Bottom
property real dockSpacing: 4
property real dockBottomGap: 0
property real dockMargin: 0
property real dockIconSize: 40
property string dockIndicatorStyle: "circle"
property bool notificationOverlayEnabled: false
property bool dankBarAutoHide: false
property bool dankBarOpenOnOverview: false
property bool dankBarVisible: true
property int overviewRows: 2
property int overviewColumns: 5
property real overviewScale: 0.16
property real dankBarSpacing: 4
property real dankBarBottomGap: 0
property real dankBarInnerPadding: 4
property int dankBarPosition: SettingsData.Position.Top
property bool dankBarIsVertical: dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right
property bool dankBarSquareCorners: false
property bool dankBarNoBackground: false
property bool dankBarGothCornersEnabled: false
property bool dankBarGothCornerRadiusOverride: false
property real dankBarGothCornerRadiusValue: 12
property bool dankBarBorderEnabled: false
property string dankBarBorderColor: "surfaceText"
property real dankBarBorderOpacity: 1.0
property real dankBarBorderThickness: 1
onDankBarGothCornerRadiusOverrideChanged: saveSettings()
onDankBarGothCornerRadiusValueChanged: saveSettings()
onDankBarBorderColorChanged: saveSettings()
onDankBarBorderOpacityChanged: saveSettings()
onDankBarBorderThicknessChanged: saveSettings()
property bool popupGapsAuto: true
property int popupGapsManual: 4
property bool modalDarkenBackground: true
property bool lockScreenShowPowerActions: true
property bool enableFprint: false
property int maxFprintTries: 3
property bool fprintdAvailable: false
property bool hideBrightnessSlider: false
property int notificationTimeoutLow: 5000
property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0
property int notificationPopupPosition: SettingsData.Position.Top
property bool osdAlwaysShowValue: false
property bool powerActionConfirm: true
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
property string powerMenuDefaultAction: "logout"
property string customPowerActionLock: ""
property string customPowerActionLogout: ""
property string customPowerActionSuspend: ""
property string customPowerActionHibernate: ""
property string customPowerActionReboot: ""
property string customPowerActionPowerOff: ""
property bool updaterUseCustomCommand: false
property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: ""
property var screenPreferences: ({})
property var showOnLastDisplay: ({})
signal forceDankBarLayoutRefresh
signal forceDockLayoutRefresh
signal widgetDataChanged
signal workspaceIconsUpdated
Component.onCompleted: {
if (!isGreeterMode) {
Processes.settingsRoot = root
loadSettings()
initializeListModels()
Processes.detectFprintd()
Processes.checkPluginSettings()
}
}
function applyStoredTheme() {
if (typeof Theme !== "undefined") {
Theme.switchTheme(currentThemeName, false, false)
} else {
Qt.callLater(function() {
if (typeof Theme !== "undefined") {
Theme.switchTheme(currentThemeName, false, false)
}
})
}
}
function regenSystemThemes() {
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function updateNiriLayout() {
if (typeof NiriService !== "undefined" && typeof CompositorService !== "undefined" && CompositorService.isNiri) {
NiriService.generateNiriLayoutConfig()
}
}
function applyStoredIconTheme() {
updateGtkIconTheme()
updateQtIconTheme()
}
function updateGtkIconTheme() {
const gtkThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme
if (gtkThemeName === "System Default" || gtkThemeName === "") return
if (typeof DMSService !== "undefined" && DMSService.apiVersion >= 3 && typeof PortalService !== "undefined") {
PortalService.setSystemIconTheme(gtkThemeName)
}
const configScript = `mkdir -p ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0
for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do
settings_file="$config_dir/settings.ini"
if [ -f "$settings_file" ]; then
if grep -q "^gtk-icon-theme-name=" "$settings_file"; then
sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file"
else
if grep -q "\\[Settings\\]" "$settings_file"; then
sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file"
else
echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file"
fi
fi
else
echo -e '[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' > "$settings_file"
fi
done
rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true
pkill -HUP -f 'gtk' 2>/dev/null || true`
Quickshell.execDetached(["sh", "-lc", configScript])
}
function updateQtIconTheme() {
const qtThemeName = (iconTheme === "System Default") ? "" : iconTheme
if (!qtThemeName) return
const home = _homeUrl.replace("file://", "").replace(/'/g, "'\\''")
const qtThemeNameEscaped = qtThemeName.replace(/'/g, "'\\''")
const script = `mkdir -p ${_configDir}/qt5ct ${_configDir}/qt6ct ${_configDir}/environment.d 2>/dev/null || true
update_qt_icon_theme() {
local config_file="$1"
local theme_name="$2"
if [ -f "$config_file" ]; then
if grep -q "^\\[Appearance\\]" "$config_file"; then
if grep -q "^icon_theme=" "$config_file"; then
sed -i "s/^icon_theme=.*/icon_theme=$theme_name/" "$config_file"
else
sed -i "/^\\[Appearance\\]/a icon_theme=$theme_name" "$config_file"
fi
else
printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file"
fi
else
printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file"
fi
}
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'
rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`
Quickshell.execDetached(["sh", "-lc", script])
}
readonly property var _hooks: ({
applyStoredTheme: applyStoredTheme,
regenSystemThemes: regenSystemThemes,
updateNiriLayout: updateNiriLayout,
applyStoredIconTheme: applyStoredIconTheme
})
function set(key, value) {
Spec.set(root, key, value, saveSettings, _hooks)
}
function loadSettings() {
_loading = true
try {
const txt = settingsFile.text()
const obj = (txt && txt.trim()) ? JSON.parse(txt) : null
Store.parse(root, obj)
const shouldMigrate = Store.migrate(root, obj)
applyStoredTheme()
applyStoredIconTheme()
Processes.detectIcons()
Processes.detectQtTools()
if (obj && obj.configVersion === undefined) {
const cleaned = Store.cleanup(txt)
if (cleaned) {
settingsFile.setText(cleaned)
}
saveSettings()
}
if (shouldMigrate) {
savePluginSettings()
saveSettings()
}
} catch (e) {
console.warn("SettingsData: Failed to load settings:", e.message)
applyStoredTheme()
applyStoredIconTheme()
} finally {
_loading = false
}
loadPluginSettings()
}
function loadPluginSettings() {
_pluginSettingsLoading = true
parsePluginSettings(pluginSettingsFile.text())
_pluginSettingsLoading = false
}
function parsePluginSettings(content) {
_pluginSettingsLoading = true
try {
if (content && content.trim()) {
pluginSettings = JSON.parse(content)
} else {
pluginSettings = {}
}
} catch (e) {
console.warn("SettingsData: Failed to parse plugin settings:", e.message)
pluginSettings = {}
} finally {
_pluginSettingsLoading = false
}
}
function saveSettings() {
if (_loading) return
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2))
}
function savePluginSettings() {
if (_pluginSettingsLoading) return
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2))
}
function detectAvailableIconThemes() {
Processes.detectIcons()
}
function getEffectiveTimeFormat() {
if (use24HourClock) {
return showSeconds ? "hh:mm:ss" : "hh:mm"
} else {
return showSeconds ? "h:mm:ss AP" : "h:mm AP"
}
}
function getEffectiveClockDateFormat() {
return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d"
}
function getEffectiveLockDateFormat() {
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat
}
function initializeListModels() {
Lists.init(leftWidgetsModel, centerWidgetsModel, rightWidgetsModel, dankBarLeftWidgets, dankBarCenterWidgets, dankBarRightWidgets)
}
function updateListModel(listModel, order) {
Lists.update(listModel, order)
widgetDataChanged()
}
function hasNamedWorkspaces() {
if (typeof NiriService === "undefined" || !CompositorService.isNiri) return false
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i]
if (ws.name && ws.name.trim() !== "") return true
}
return false
}
function getNamedWorkspaces() {
var namedWorkspaces = []
if (typeof NiriService === "undefined" || !CompositorService.isNiri) return namedWorkspaces
for (const ws of NiriService.allWorkspaces) {
if (ws.name && ws.name.trim() !== "") {
namedWorkspaces.push(ws.name)
}
}
return namedWorkspaces
}
function getPopupYPosition(barHeight) {
const gothOffset = dankBarGothCornersEnabled ? Theme.cornerRadius : 0
return barHeight + dankBarSpacing + dankBarBottomGap - gothOffset + Theme.popupDistance
}
function getPopupTriggerPosition(globalPos, screen, barThickness, widgetWidth) {
const screenX = screen ? screen.x : 0
const screenY = screen ? screen.y : 0
const relativeX = globalPos.x - screenX
const relativeY = globalPos.y - screenY
if (dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right) {
return {
"x": relativeY,
"y": barThickness + dankBarSpacing + Theme.popupDistance,
"width": widgetWidth
}
}
return {
"x": relativeX,
"y": barThickness + dankBarSpacing + Theme.popupDistance,
"width": widgetWidth
}
}
function getFilteredScreens(componentId) {
var prefs = screenPreferences && screenPreferences[componentId] || ["all"]
if (prefs.includes("all")) {
return Quickshell.screens
}
var filtered = Quickshell.screens.filter(screen => prefs.includes(screen.name))
if (filtered.length === 0 && showOnLastDisplay && showOnLastDisplay[componentId] && Quickshell.screens.length === 1) {
return Quickshell.screens
}
return filtered
}
function sendTestNotifications() {
sendTestNotification(0)
testNotifTimer1.start()
testNotifTimer2.start()
}
function sendTestNotification(index) {
const notifications = [["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"], ["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"], ["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]]
if (index < 0 || index >= notifications.length) {
return
}
const notif = notifications[index]
testNotificationProcess.command = ["notify-send", "-h", "int:transient:1", "-a", "DMS", "-i", notif[2], notif[0], notif[1]]
testNotificationProcess.running = true
}
function setMatugenScheme(scheme) {
var normalized = scheme || "scheme-tonal-spot"
if (matugenScheme === normalized) return
set("matugenScheme", normalized)
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setRunUserMatugenTemplates(enabled) {
if (runUserMatugenTemplates === enabled) return
set("runUserMatugenTemplates", enabled)
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setMatugenTargetMonitor(monitorName) {
if (matugenTargetMonitor === monitorName) return
set("matugenTargetMonitor", monitorName)
if (typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setCornerRadius(radius) {
set("cornerRadius", radius)
NiriService.generateNiriLayoutConfig()
}
function setWeatherLocation(displayName, coordinates) {
weatherLocation = displayName
weatherCoordinates = coordinates
saveSettings()
}
function setIconTheme(themeName) {
iconTheme = themeName
updateGtkIconTheme()
updateQtIconTheme()
saveSettings()
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) Theme.generateSystemThemesFromCurrentTheme()
}
function setGtkThemingEnabled(enabled) {
set("gtkThemingEnabled", enabled)
if (enabled && typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setQtThemingEnabled(enabled) {
set("qtThemingEnabled", enabled)
if (enabled && typeof Theme !== "undefined") {
Theme.generateSystemThemesFromCurrentTheme()
}
}
function setShowDock(enabled) {
showDock = enabled
if (enabled && dockPosition === dankBarPosition) {
if (dankBarPosition === SettingsData.Position.Top) {
setDockPosition(SettingsData.Position.Bottom)
return
}
if (dankBarPosition === SettingsData.Position.Bottom) {
setDockPosition(SettingsData.Position.Top)
return
}
if (dankBarPosition === SettingsData.Position.Left) {
setDockPosition(SettingsData.Position.Right)
return
}
if (dankBarPosition === SettingsData.Position.Right) {
setDockPosition(SettingsData.Position.Left)
return
}
}
saveSettings()
}
function setDockPosition(position) {
dockPosition = position
if (position === SettingsData.Position.Bottom && dankBarPosition === SettingsData.Position.Bottom && showDock) {
setDankBarPosition(SettingsData.Position.Top)
}
if (position === SettingsData.Position.Top && dankBarPosition === SettingsData.Position.Top && showDock) {
setDankBarPosition(SettingsData.Position.Bottom)
}
if (position === SettingsData.Position.Left && dankBarPosition === SettingsData.Position.Left && showDock) {
setDankBarPosition(SettingsData.Position.Right)
}
if (position === SettingsData.Position.Right && dankBarPosition === SettingsData.Position.Right && showDock) {
setDankBarPosition(SettingsData.Position.Left)
}
saveSettings()
Qt.callLater(() => forceDockLayoutRefresh())
}
function setDankBarSpacing(spacing) {
set("dankBarSpacing", spacing)
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
NiriService.generateNiriLayoutConfig()
}
}
function setDankBarPosition(position) {
dankBarPosition = position
if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) {
setDockPosition(SettingsData.Position.Top)
return
}
if (position === SettingsData.Position.Top && dockPosition === SettingsData.Position.Top && showDock) {
setDockPosition(SettingsData.Position.Bottom)
return
}
if (position === SettingsData.Position.Left && dockPosition === SettingsData.Position.Left && showDock) {
setDockPosition(SettingsData.Position.Right)
return
}
if (position === SettingsData.Position.Right && dockPosition === SettingsData.Position.Right && showDock) {
setDockPosition(SettingsData.Position.Left)
return
}
saveSettings()
}
function setDankBarLeftWidgets(order) {
dankBarLeftWidgets = order
updateListModel(leftWidgetsModel, order)
saveSettings()
}
function setDankBarCenterWidgets(order) {
dankBarCenterWidgets = order
updateListModel(centerWidgetsModel, order)
saveSettings()
}
function setDankBarRightWidgets(order) {
dankBarRightWidgets = order
updateListModel(rightWidgetsModel, order)
saveSettings()
}
function resetDankBarWidgetsToDefault() {
var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"]
var defaultCenter = ["music", "clock", "weather"]
var defaultRight = ["systemTray", "clipboard", "notificationButton", "battery", "controlCenterButton"]
dankBarLeftWidgets = defaultLeft
dankBarCenterWidgets = defaultCenter
dankBarRightWidgets = defaultRight
updateListModel(leftWidgetsModel, defaultLeft)
updateListModel(centerWidgetsModel, defaultCenter)
updateListModel(rightWidgetsModel, defaultRight)
showLauncherButton = true
showWorkspaceSwitcher = true
showFocusedWindow = true
showWeather = true
showMusic = true
showClipboard = true
showCpuUsage = true
showMemUsage = true
showCpuTemp = true
showGpuTemp = true
showSystemTray = true
showClock = true
showNotificationButton = true
showBattery = true
showControlCenterButton = true
saveSettings()
}
function setWorkspaceNameIcon(workspaceName, iconData) {
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
iconMap[workspaceName] = iconData
workspaceNameIcons = iconMap
saveSettings()
workspaceIconsUpdated()
}
function removeWorkspaceNameIcon(workspaceName) {
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
delete iconMap[workspaceName]
workspaceNameIcons = iconMap
saveSettings()
workspaceIconsUpdated()
}
function getWorkspaceNameIcon(workspaceName) {
return workspaceNameIcons[workspaceName] || null
}
function toggleDankBarVisible() {
dankBarVisible = !dankBarVisible
saveSettings()
}
function getPluginSetting(pluginId, key, defaultValue) {
if (!pluginSettings[pluginId]) {
return defaultValue
}
return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue
}
function setPluginSetting(pluginId, key, value) {
const updated = JSON.parse(JSON.stringify(pluginSettings))
if (!updated[pluginId]) {
updated[pluginId] = {}
}
updated[pluginId][key] = value
pluginSettings = updated
savePluginSettings()
}
function removePluginSettings(pluginId) {
if (pluginSettings[pluginId]) {
delete pluginSettings[pluginId]
savePluginSettings()
}
}
function getPluginSettingsForPlugin(pluginId) {
const settings = pluginSettings[pluginId]
return settings ? JSON.parse(JSON.stringify(settings)) : {}
}
ListModel {
id: leftWidgetsModel
}
ListModel {
id: centerWidgetsModel
}
ListModel {
id: rightWidgetsModel
}
property Process testNotificationProcess
testNotificationProcess: Process {
command: []
running: false
}
property Timer testNotifTimer1
testNotifTimer1: Timer {
interval: 400
repeat: false
onTriggered: sendTestNotification(1)
}
property Timer testNotifTimer2
testNotifTimer2: Timer {
interval: 800
repeat: false
onTriggered: sendTestNotification(2)
}
property alias settingsFile: settingsFile
FileView {
id: settingsFile
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
blockLoading: true
blockWrites: true
atomicWrites: true
watchChanges: !isGreeterMode
onLoaded: {
if (!isGreeterMode) {
try {
const txt = settingsFile.text()
const obj = (txt && txt.trim()) ? JSON.parse(txt) : null
Store.parse(root, obj)
Store.migrate(root, obj)
} catch (e) {
console.warn("SettingsData: Failed to reload settings:", e.message)
}
hasTriedDefaultSettings = false
}
}
onLoadFailed: error => {
if (!isGreeterMode && !hasTriedDefaultSettings) {
hasTriedDefaultSettings = true
Processes.checkDefaultSettings()
} else if (!isGreeterMode) {
applyStoredTheme()
}
}
}
FileView {
id: pluginSettingsFile
path: isGreeterMode ? "" : pluginSettingsPath
blockLoading: true
blockWrites: true
atomicWrites: true
watchChanges: !isGreeterMode
onLoaded: {
if (!isGreeterMode) {
parsePluginSettings(pluginSettingsFile.text())
}
}
onLoadFailed: error => {
if (!isGreeterMode) {
pluginSettings = {}
}
}
}
property bool pluginSettingsFileExists: false
IpcHandler {
function reveal(): string {
root.setDankBarVisible(true)
return "BAR_SHOW_SUCCESS"
}
function hide(): string {
root.setDankBarVisible(false)
return "BAR_HIDE_SUCCESS"
}
function toggle(): string {
root.toggleDankBarVisible()
return root.dankBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS"
}
function status(): string {
return root.dankBarVisible ? "visible" : "hidden"
}
target: "bar"
}
}

View File

@@ -1,545 +0,0 @@
// Stock theme definitions for DankMaterialShell
// Separated from Theme.qml to keep that file clean
const CatppuccinMocha = {
surface: "#313244",
surfaceText: "#cdd6f4",
surfaceVariant: "#313244",
surfaceVariantText: "#a6adc8",
background: "#1e1e2e",
backgroundText: "#cdd6f4",
outline: "#6c7086",
surfaceContainer: "#45475a",
surfaceContainerHigh: "#585b70",
surfaceContainerHighest: "#6c7086"
}
const CatppuccinLatte = {
surface: "#e6e9ef",
surfaceText: "#4c4f69",
surfaceVariant: "#e6e9ef",
surfaceVariantText: "#6c6f85",
background: "#eff1f5",
backgroundText: "#4c4f69",
outline: "#9ca0b0",
surfaceContainer: "#dce0e8",
surfaceContainerHigh: "#ccd0da",
surfaceContainerHighest: "#bcc0cc"
}
const CatppuccinVariants = {
"cat-rosewater": {
name: "Rosewater",
dark: { primary: "#f5e0dc", secondary: "#f2cdcd", primaryText: "#1e1e2e", primaryContainer: "#7d5d56", surfaceTint: "#f5e0dc" },
light: { primary: "#dc8a78", secondary: "#dd7878", primaryText: "#ffffff", primaryContainer: "#f6e7e3", surfaceTint: "#dc8a78" }
},
"cat-flamingo": {
name: "Flamingo",
dark: { primary: "#f2cdcd", secondary: "#f5e0dc", primaryText: "#1e1e2e", primaryContainer: "#7a555a", surfaceTint: "#f2cdcd" },
light: { primary: "#dd7878", secondary: "#dc8a78", primaryText: "#ffffff", primaryContainer: "#f6e5e5", surfaceTint: "#dd7878" }
},
"cat-pink": {
name: "Pink",
dark: { primary: "#f5c2e7", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#7a3f69", surfaceTint: "#f5c2e7" },
light: { primary: "#ea76cb", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#f7d7ee", surfaceTint: "#ea76cb" }
},
"cat-mauve": {
name: "Mauve",
dark: { primary: "#cba6f7", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#55307f", surfaceTint: "#cba6f7" },
light: { primary: "#8839ef", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#eadcff", surfaceTint: "#8839ef" }
},
"cat-red": {
name: "Red",
dark: { primary: "#f38ba8", secondary: "#eba0ac", primaryText: "#1e1e2e", primaryContainer: "#6f2438", surfaceTint: "#f38ba8" },
light: { primary: "#d20f39", secondary: "#e64553", primaryText: "#ffffff", primaryContainer: "#f6d0d6", surfaceTint: "#d20f39" }
},
"cat-maroon": {
name: "Maroon",
dark: { primary: "#eba0ac", secondary: "#f38ba8", primaryText: "#1e1e2e", primaryContainer: "#6d3641", surfaceTint: "#eba0ac" },
light: { primary: "#e64553", secondary: "#d20f39", primaryText: "#ffffff", primaryContainer: "#f7d8dc", surfaceTint: "#e64553" }
},
"cat-peach": {
name: "Peach",
dark: { primary: "#fab387", secondary: "#f9e2af", primaryText: "#1e1e2e", primaryContainer: "#734226", surfaceTint: "#fab387" },
light: { primary: "#fe640b", secondary: "#df8e1d", primaryText: "#ffffff", primaryContainer: "#ffe4d5", surfaceTint: "#fe640b" }
},
"cat-yellow": {
name: "Yellow",
dark: { primary: "#f9e2af", secondary: "#a6e3a1", primaryText: "#1e1e2e", primaryContainer: "#6e5a2f", surfaceTint: "#f9e2af" },
light: { primary: "#df8e1d", secondary: "#40a02b", primaryText: "#ffffff", primaryContainer: "#fff6d6", surfaceTint: "#df8e1d" }
},
"cat-green": {
name: "Green",
dark: { primary: "#a6e3a1", secondary: "#94e2d5", primaryText: "#1e1e2e", primaryContainer: "#2f5f36", surfaceTint: "#a6e3a1" },
light: { primary: "#40a02b", secondary: "#179299", primaryText: "#ffffff", primaryContainer: "#dff4e0", surfaceTint: "#40a02b" }
},
"cat-teal": {
name: "Teal",
dark: { primary: "#94e2d5", secondary: "#89dceb", primaryText: "#1e1e2e", primaryContainer: "#2e5e59", surfaceTint: "#94e2d5" },
light: { primary: "#179299", secondary: "#04a5e5", primaryText: "#ffffff", primaryContainer: "#daf3f1", surfaceTint: "#179299" }
},
"cat-sky": {
name: "Sky",
dark: { primary: "#89dceb", secondary: "#74c7ec", primaryText: "#1e1e2e", primaryContainer: "#24586a", surfaceTint: "#89dceb" },
light: { primary: "#04a5e5", secondary: "#209fb5", primaryText: "#ffffff", primaryContainer: "#dbf1fb", surfaceTint: "#04a5e5" }
},
"cat-sapphire": {
name: "Sapphire",
dark: { primary: "#74c7ec", secondary: "#89b4fa", primaryText: "#1e1e2e", primaryContainer: "#1f4d6f", surfaceTint: "#74c7ec" },
light: { primary: "#209fb5", secondary: "#1e66f5", primaryText: "#ffffff", primaryContainer: "#def3f8", surfaceTint: "#209fb5" }
},
"cat-blue": {
name: "Blue",
dark: { primary: "#89b4fa", secondary: "#b4befe", primaryText: "#1e1e2e", primaryContainer: "#243f75", surfaceTint: "#89b4fa" },
light: { primary: "#1e66f5", secondary: "#7287fd", primaryText: "#ffffff", primaryContainer: "#e0e9ff", surfaceTint: "#1e66f5" }
},
"cat-lavender": {
name: "Lavender",
dark: { primary: "#b4befe", secondary: "#cba6f7", primaryText: "#1e1e2e", primaryContainer: "#3f4481", surfaceTint: "#b4befe" },
light: { primary: "#7287fd", secondary: "#8839ef", primaryText: "#ffffff", primaryContainer: "#e5e8ff", surfaceTint: "#7287fd" }
}
}
function getCatppuccinTheme(variant, isLight = false) {
const variantData = CatppuccinVariants[variant]
if (!variantData) return null
const baseColors = isLight ? CatppuccinLatte : CatppuccinMocha
const accentColors = isLight ? variantData.light : variantData.dark
return Object.assign({
name: `${variantData.name}${isLight ? ' Light' : ''}`
}, baseColors, accentColors)
}
const StockThemes = {
DARK: {
blue: {
name: "Blue",
primary: "#42a5f5",
primaryText: "#000000",
primaryContainer: "#0d47a1",
secondary: "#8ab4f8",
surface: "#101418",
surfaceText: "#e0e2e8",
surfaceVariant: "#42474e",
surfaceVariantText: "#c2c7cf",
surfaceTint: "#8ab4f8",
background: "#101418",
backgroundText: "#e0e2e8",
outline: "#8c9199",
surfaceContainer: "#1d2024",
surfaceContainerHigh: "#272a2f",
surfaceContainerHighest: "#32353a"
},
purple: {
name: "Purple",
primary: "#D0BCFF",
primaryText: "#381E72",
primaryContainer: "#4F378B",
secondary: "#CCC2DC",
surface: "#141218",
surfaceText: "#e6e0e9",
surfaceVariant: "#49454e",
surfaceVariantText: "#cac4cf",
surfaceTint: "#D0BCFF",
background: "#141218",
backgroundText: "#e6e0e9",
outline: "#948f99",
surfaceContainer: "#211f24",
surfaceContainerHigh: "#2b292f",
surfaceContainerHighest: "#36343a"
},
green: {
name: "Green",
primary: "#4caf50",
primaryText: "#000000",
primaryContainer: "#1b5e20",
secondary: "#81c995",
surface: "#10140f",
surfaceText: "#e0e4db",
surfaceVariant: "#424940",
surfaceVariantText: "#c2c9bd",
surfaceTint: "#81c995",
background: "#10140f",
backgroundText: "#e0e4db",
outline: "#8c9388",
surfaceContainer: "#1d211b",
surfaceContainerHigh: "#272b25",
surfaceContainerHighest: "#323630"
},
orange: {
name: "Orange",
primary: "#ff6d00",
primaryText: "#000000",
primaryContainer: "#3e2723",
secondary: "#ffb74d",
surface: "#1a120e",
surfaceText: "#f0dfd8",
surfaceVariant: "#52443d",
surfaceVariantText: "#d7c2b9",
surfaceTint: "#ffb74d",
background: "#1a120e",
backgroundText: "#f0dfd8",
outline: "#a08d85",
surfaceContainer: "#271e1a",
surfaceContainerHigh: "#322824",
surfaceContainerHighest: "#3d332e"
},
red: {
name: "Red",
primary: "#f44336",
primaryText: "#000000",
primaryContainer: "#4a0e0e",
secondary: "#f28b82",
surface: "#1a1110",
surfaceText: "#f1dedc",
surfaceVariant: "#534341",
surfaceVariantText: "#d8c2be",
surfaceTint: "#f28b82",
background: "#1a1110",
backgroundText: "#f1dedc",
outline: "#a08c89",
surfaceContainer: "#271d1c",
surfaceContainerHigh: "#322826",
surfaceContainerHighest: "#3d3231"
},
cyan: {
name: "Cyan",
primary: "#00bcd4",
primaryText: "#000000",
primaryContainer: "#004d5c",
secondary: "#4dd0e1",
surface: "#0e1416",
surfaceText: "#dee3e5",
surfaceVariant: "#3f484a",
surfaceVariantText: "#bfc8ca",
surfaceTint: "#4dd0e1",
background: "#0e1416",
backgroundText: "#dee3e5",
outline: "#899295",
surfaceContainer: "#1b2122",
surfaceContainerHigh: "#252b2c",
surfaceContainerHighest: "#303637"
},
pink: {
name: "Pink",
primary: "#e91e63",
primaryText: "#000000",
primaryContainer: "#4a0e2f",
secondary: "#f8bbd9",
surface: "#191112",
surfaceText: "#f0dee0",
surfaceVariant: "#524345",
surfaceVariantText: "#d6c2c3",
surfaceTint: "#f8bbd9",
background: "#191112",
backgroundText: "#f0dee0",
outline: "#9f8c8e",
surfaceContainer: "#261d1e",
surfaceContainerHigh: "#312829",
surfaceContainerHighest: "#3c3233"
},
amber: {
name: "Amber",
primary: "#ffc107",
primaryText: "#000000",
primaryContainer: "#4a3c00",
secondary: "#ffd54f",
surface: "#17130b",
surfaceText: "#ebe1d4",
surfaceVariant: "#4d4639",
surfaceVariantText: "#d0c5b4",
surfaceTint: "#ffd54f",
background: "#17130b",
backgroundText: "#ebe1d4",
outline: "#998f80",
surfaceContainer: "#231f17",
surfaceContainerHigh: "#2e2921",
surfaceContainerHighest: "#39342b"
},
coral: {
name: "Coral",
primary: "#ffb4ab",
primaryText: "#000000",
primaryContainer: "#8c1d18",
secondary: "#f9dedc",
surface: "#1a1110",
surfaceText: "#f1dedc",
surfaceVariant: "#534341",
surfaceVariantText: "#d8c2bf",
surfaceTint: "#ffb4ab",
background: "#1a1110",
backgroundText: "#f1dedc",
outline: "#a08c8a",
surfaceContainer: "#271d1c",
surfaceContainerHigh: "#322826",
surfaceContainerHighest: "#3d3231"
},
monochrome: {
name: "Monochrome",
primary: "#ffffff",
primaryText: "#2b303c",
primaryContainer: "#424753",
secondary: "#c4c6d0",
surface: "#2a2a2a",
surfaceText: "#e4e2e3",
surfaceVariant: "#474648",
surfaceVariantText: "#c8c6c7",
surfaceTint: "#c2c6d6",
background: "#131315",
backgroundText: "#e4e2e3",
outline: "#929092",
surfaceContainer: "#353535",
surfaceContainerHigh: "#424242",
surfaceContainerHighest: "#505050",
error: "#ffb4ab",
warning: "#3f4759",
info: "#595e6c",
matugen_type: "scheme-monochrome"
}
},
LIGHT: {
blue: {
name: "Blue Light",
primary: "#1976d2",
primaryText: "#ffffff",
primaryContainer: "#e3f2fd",
secondary: "#42a5f5",
surface: "#f7f9ff",
surfaceText: "#181c20",
surfaceVariant: "#dee3eb",
surfaceVariantText: "#42474e",
surfaceTint: "#1976d2",
background: "#f7f9ff",
backgroundText: "#181c20",
outline: "#72777f",
surfaceContainer: "#eceef4",
surfaceContainerHigh: "#e6e8ee",
surfaceContainerHighest: "#e0e2e8"
},
purple: {
name: "Purple Light",
primary: "#6750A4",
primaryText: "#ffffff",
primaryContainer: "#EADDFF",
secondary: "#625B71",
surface: "#fef7ff",
surfaceText: "#1d1b20",
surfaceVariant: "#e7e0eb",
surfaceVariantText: "#49454e",
surfaceTint: "#6750A4",
background: "#fef7ff",
backgroundText: "#1d1b20",
outline: "#7a757f",
surfaceContainer: "#f2ecf4",
surfaceContainerHigh: "#ece6ee",
surfaceContainerHighest: "#e6e0e9"
},
green: {
name: "Green Light",
primary: "#2e7d32",
primaryText: "#ffffff",
primaryContainer: "#e8f5e8",
secondary: "#4caf50",
surface: "#f7fbf1",
surfaceText: "#191d17",
surfaceVariant: "#dee5d8",
surfaceVariantText: "#424940",
surfaceTint: "#2e7d32",
background: "#f7fbf1",
backgroundText: "#191d17",
outline: "#72796f",
surfaceContainer: "#ecefe6",
surfaceContainerHigh: "#e6e9e0",
surfaceContainerHighest: "#e0e4db"
},
orange: {
name: "Orange Light",
primary: "#e65100",
primaryText: "#ffffff",
primaryContainer: "#ffecb3",
secondary: "#ff9800",
surface: "#fff8f6",
surfaceText: "#221a16",
surfaceVariant: "#f4ded5",
surfaceVariantText: "#52443d",
surfaceTint: "#e65100",
background: "#fff8f6",
backgroundText: "#221a16",
outline: "#85736c",
surfaceContainer: "#fceae3",
surfaceContainerHigh: "#f6e5de",
surfaceContainerHighest: "#f0dfd8"
},
red: {
name: "Red Light",
primary: "#d32f2f",
primaryText: "#ffffff",
primaryContainer: "#ffebee",
secondary: "#f44336",
surface: "#fff8f7",
surfaceText: "#231918",
surfaceVariant: "#f5ddda",
surfaceVariantText: "#534341",
surfaceTint: "#d32f2f",
background: "#fff8f7",
backgroundText: "#231918",
outline: "#857370",
surfaceContainer: "#fceae7",
surfaceContainerHigh: "#f7e4e1",
surfaceContainerHighest: "#f1dedc"
},
cyan: {
name: "Cyan Light",
primary: "#0097a7",
primaryText: "#ffffff",
primaryContainer: "#e0f2f1",
secondary: "#00bcd4",
surface: "#f5fafc",
surfaceText: "#171d1e",
surfaceVariant: "#dbe4e6",
surfaceVariantText: "#3f484a",
surfaceTint: "#0097a7",
background: "#f5fafc",
backgroundText: "#171d1e",
outline: "#6f797b",
surfaceContainer: "#e9eff0",
surfaceContainerHigh: "#e3e9eb",
surfaceContainerHighest: "#dee3e5"
},
pink: {
name: "Pink Light",
primary: "#c2185b",
primaryText: "#ffffff",
primaryContainer: "#fce4ec",
secondary: "#e91e63",
surface: "#fff8f7",
surfaceText: "#22191a",
surfaceVariant: "#f3dddf",
surfaceVariantText: "#524345",
surfaceTint: "#c2185b",
background: "#fff8f7",
backgroundText: "#22191a",
outline: "#847375",
surfaceContainer: "#fbeaeb",
surfaceContainerHigh: "#f5e4e5",
surfaceContainerHighest: "#f0dee0"
},
amber: {
name: "Amber Light",
primary: "#ff8f00",
primaryText: "#000000",
primaryContainer: "#fff8e1",
secondary: "#ffc107",
surface: "#fff8f2",
surfaceText: "#1f1b13",
surfaceVariant: "#ede1cf",
surfaceVariantText: "#4d4639",
surfaceTint: "#ff8f00",
background: "#fff8f2",
backgroundText: "#1f1b13",
outline: "#7f7667",
surfaceContainer: "#f6ecdf",
surfaceContainerHigh: "#f1e7d9",
surfaceContainerHighest: "#ebe1d4"
},
coral: {
name: "Coral Light",
primary: "#8c1d18",
primaryText: "#ffffff",
primaryContainer: "#ffdad6",
secondary: "#ff5449",
surface: "#fff8f7",
surfaceText: "#231918",
surfaceVariant: "#f5ddda",
surfaceVariantText: "#534341",
surfaceTint: "#8c1d18",
background: "#fff8f7",
backgroundText: "#231918",
outline: "#857371",
surfaceContainer: "#fceae7",
surfaceContainerHigh: "#f6e4e2",
surfaceContainerHighest: "#f1dedc"
},
monochrome: {
name: "Monochrome Light",
primary: "#2b303c",
primaryText: "#ffffff",
primaryContainer: "#d6d7dc",
secondary: "#4a4d56",
surface: "#f5f5f6",
surfaceText: "#2a2a2a",
surfaceVariant: "#e0e0e2",
surfaceVariantText: "#424242",
surfaceTint: "#5a5f6e",
background: "#ffffff",
backgroundText: "#1a1a1a",
outline: "#757577",
surfaceContainer: "#e8e8ea",
surfaceContainerHigh: "#dcdcde",
surfaceContainerHighest: "#d0d0d2",
error: "#ba1a1a",
warning: "#f9e79f",
info: "#5d6475",
matugen_type: "scheme-monochrome"
}
}
}
const ThemeCategories = {
GENERIC: {
name: "Generic",
variants: ["blue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral", "monochrome"]
},
CATPPUCCIN: {
name: "Catppuccin",
variants: Object.keys(CatppuccinVariants)
}
}
const ThemeNames = {
BLUE: "blue",
PURPLE: "purple",
GREEN: "green",
ORANGE: "orange",
RED: "red",
CYAN: "cyan",
PINK: "pink",
AMBER: "amber",
CORAL: "coral",
MONOCHROME: "monochrome",
DYNAMIC: "dynamic"
}
function isStockTheme(themeName) {
return Object.keys(StockThemes.DARK).includes(themeName)
}
function isCatppuccinVariant(themeName) {
return Object.keys(CatppuccinVariants).includes(themeName)
}
function getAvailableThemes(isLight = false) {
return isLight ? StockThemes.LIGHT : StockThemes.DARK
}
function getThemeByName(themeName, isLight = false) {
if (isCatppuccinVariant(themeName)) {
return getCatppuccinTheme(themeName, isLight)
}
const themes = getAvailableThemes(isLight)
return themes[themeName] || themes.blue
}
function getAllThemeNames() {
return Object.keys(StockThemes.DARK)
}
function getCatppuccinVariantNames() {
return Object.keys(CatppuccinVariants)
}
function getThemeCategories() {
return ThemeCategories
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
Singleton {
id: root
function init(leftModel, centerModel, rightModel, left, center, right) {
const dummy = {
widgetId: "dummy",
enabled: true,
size: 20,
selectedGpuIndex: 0,
pciId: "",
mountPath: "/",
minimumWidth: true,
showSwap: false
}
leftModel.append(dummy)
centerModel.append(dummy)
rightModel.append(dummy)
update(leftModel, left)
update(centerModel, center)
update(rightModel, right)
}
function update(model, order) {
model.clear()
for (var i = 0; i < order.length; i++) {
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id
var enabled = typeof order[i] === "string" ? true : order[i].enabled
var size = typeof order[i] === "string" ? undefined : order[i].size
var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex
var pciId = typeof order[i] === "string" ? undefined : order[i].pciId
var mountPath = typeof order[i] === "string" ? undefined : order[i].mountPath
var minimumWidth = typeof order[i] === "string" ? undefined : order[i].minimumWidth
var showSwap = typeof order[i] === "string" ? undefined : order[i].showSwap
var item = {
widgetId: widgetId,
enabled: enabled
}
if (size !== undefined) item.size = size
if (selectedGpuIndex !== undefined) item.selectedGpuIndex = selectedGpuIndex
if (pciId !== undefined) item.pciId = pciId
if (mountPath !== undefined) item.mountPath = mountPath
if (minimumWidth !== undefined) item.minimumWidth = minimumWidth
if (showSwap !== undefined) item.showSwap = showSwap
model.append(item)
}
}
}

View File

@@ -1,118 +0,0 @@
.pragma library
.import "./SettingsSpec.js" as SpecModule
function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC;
for (var k in SPEC) {
var spec = SPEC[k];
root[k] = spec.def;
}
if (!jsonObj) return;
for (var k in jsonObj) {
if (!SPEC[k]) continue;
var raw = jsonObj[k];
var spec = SPEC[k];
var coerce = spec.coerce;
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
}
}
function toJson(root) {
var SPEC = SpecModule.SPEC;
var out = {};
for (var k in SPEC) {
if (SPEC[k].persist === false) continue;
out[k] = root[k];
}
out.configVersion = root.settingsConfigVersion;
return out;
}
function migrate(root, jsonObj) {
var SPEC = SpecModule.SPEC;
if (!jsonObj) return;
if (jsonObj.themeIndex !== undefined || jsonObj.themeIsDynamic !== undefined) {
var themeNames = ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"];
if (jsonObj.themeIsDynamic) {
root.currentThemeName = "dynamic";
} else if (jsonObj.themeIndex >= 0 && jsonObj.themeIndex < themeNames.length) {
root.currentThemeName = themeNames[jsonObj.themeIndex];
}
console.info("Auto-migrated theme from index", jsonObj.themeIndex, "to", root.currentThemeName);
}
if ((jsonObj.dankBarWidgetOrder && jsonObj.dankBarWidgetOrder.length > 0) ||
(jsonObj.topBarWidgetOrder && jsonObj.topBarWidgetOrder.length > 0)) {
if (jsonObj.dankBarLeftWidgets === undefined && jsonObj.dankBarCenterWidgets === undefined && jsonObj.dankBarRightWidgets === undefined) {
var widgetOrder = jsonObj.dankBarWidgetOrder || jsonObj.topBarWidgetOrder;
root.dankBarLeftWidgets = widgetOrder.filter(function(w) { return ["launcherButton", "workspaceSwitcher", "focusedWindow"].indexOf(w) >= 0; });
root.dankBarCenterWidgets = widgetOrder.filter(function(w) { return ["clock", "music", "weather"].indexOf(w) >= 0; });
root.dankBarRightWidgets = widgetOrder.filter(function(w) { return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].indexOf(w) >= 0; });
}
}
if (jsonObj.useOSLogo !== undefined) {
root.launcherLogoMode = jsonObj.useOSLogo ? "os" : "apps";
root.launcherLogoColorOverride = jsonObj.osLogoColorOverride !== undefined ? jsonObj.osLogoColorOverride : "";
root.launcherLogoBrightness = jsonObj.osLogoBrightness !== undefined ? jsonObj.osLogoBrightness : 0.5;
root.launcherLogoContrast = jsonObj.osLogoContrast !== undefined ? jsonObj.osLogoContrast : 1;
}
if (jsonObj.mediaCompactMode !== undefined && jsonObj.mediaSize === undefined) {
root.mediaSize = jsonObj.mediaCompactMode ? 0 : 1;
}
for (var k in SPEC) {
var spec = SPEC[k];
if (!spec.migrate) continue;
for (var i = 0; i < spec.migrate.length; i++) {
var oldKey = spec.migrate[i];
if (jsonObj[oldKey] !== undefined && jsonObj[k] === undefined) {
var raw = jsonObj[oldKey];
var coerce = spec.coerce;
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
break;
}
}
}
if (jsonObj.dankBarAtBottom !== undefined || jsonObj.topBarAtBottom !== undefined) {
var atBottom = jsonObj.dankBarAtBottom !== undefined ? jsonObj.dankBarAtBottom : jsonObj.topBarAtBottom;
root.dankBarPosition = atBottom ? 1 : 0;
}
if (jsonObj.pluginSettings !== undefined) {
root.pluginSettings = jsonObj.pluginSettings;
return true;
}
return false;
}
function cleanup(fileText) {
var getValidKeys = SpecModule.getValidKeys;
if (!fileText || !fileText.trim()) return;
try {
var settings = JSON.parse(fileText);
var validKeys = getValidKeys();
var needsSave = false;
for (var key in settings) {
if (validKeys.indexOf(key) < 0) {
console.log("SettingsData: Removing unused key:", key);
delete settings[key];
needsSave = true;
}
}
return needsSave ? JSON.stringify(settings, null, 2) : null;
} catch (e) {
console.warn("SettingsData: Failed to cleanup unused keys:", e.message);
return null;
}
}

View File

@@ -1,588 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals
import qs.Modals.Clipboard
import qs.Modals.Common
import qs.Modals.Settings
import qs.Modals.Spotlight
import qs.Modules
import qs.Modules.AppDrawer
import qs.Modules.DankDash
import qs.Modules.ControlCenter
import qs.Modules.Dock
import qs.Modules.Lock
import qs.Modules.Notepad
import qs.Modules.Notifications.Center
import qs.Widgets
import qs.Modules.Notifications.Popup
import qs.Modules.OSD
import qs.Modules.ProcessList
import qs.Modules.Settings
import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts
import qs.Modules.HyprWorkspaces
import qs.Modules.Plugins
import qs.Services
Item {
id: root
Instantiator {
id: daemonPluginInstantiator
asynchronous: true
model: Object.keys(PluginService.pluginDaemonComponents)
delegate: Loader {
id: daemonLoader
property string pluginId: modelData
sourceComponent: PluginService.pluginDaemonComponents[pluginId]
onLoaded: {
if (item) {
item.pluginService = PluginService
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
item.pluginId = pluginId
console.info("Daemon plugin loaded:", pluginId)
}
}
}
}
Loader {
id: blurredWallpaperBackgroundLoader
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
asynchronous: false
sourceComponent: BlurredWallpaperBackground {}
}
WallpaperBackground {}
Lock {
id: lock
}
Loader {
id: dankBarLoader
asynchronous: false
property var currentPosition: SettingsData.dankBarPosition
property bool initialized: false
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
sourceComponent: DankBar {
hyprlandOverviewLoader: dankBarLoader.hyprlandOverviewLoaderRef
onColorPickerRequested: {
if (colorPickerModal.shouldBeVisible) {
colorPickerModal.close()
} else {
colorPickerModal.show()
}
}
}
Component.onCompleted: {
initialized = true
}
onCurrentPositionChanged: {
if (!initialized)
return
const component = sourceComponent
sourceComponent = null
sourceComponent = component
}
}
Loader {
id: dockLoader
active: true
asynchronous: false
property var currentPosition: SettingsData.dockPosition
property bool initialized: false
sourceComponent: Dock {
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
}
onLoaded: {
if (item) {
dockContextMenuLoader.active = true
}
}
Component.onCompleted: {
initialized = true
}
onCurrentPositionChanged: {
if (!initialized)
return
console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock")
const comp = sourceComponent
sourceComponent = null
sourceComponent = comp
}
}
Loader {
id: dankDashPopoutLoader
active: false
asynchronous: true
sourceComponent: Component {
DankDashPopout {
id: dankDashPopout
Component.onCompleted: {
PopoutService.dankDashPopout = dankDashPopout
}
}
}
}
LazyLoader {
id: dockContextMenuLoader
active: false
DockContextMenu {
id: dockContextMenu
}
}
LazyLoader {
id: notificationCenterLoader
active: false
NotificationCenterPopout {
id: notificationCenter
Component.onCompleted: {
PopoutService.notificationCenterPopout = notificationCenter
}
}
}
Variants {
model: SettingsData.getFilteredScreens("notifications")
delegate: NotificationPopupManager {
modelData: item
}
}
LazyLoader {
id: controlCenterLoader
active: false
property var modalRef: colorPickerModal
property LazyLoader powerModalLoaderRef: powerMenuModalLoader
ControlCenterPopout {
id: controlCenterPopout
colorPickerModal: controlCenterLoader.modalRef
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
onLockRequested: {
lock.activate()
}
Component.onCompleted: {
PopoutService.controlCenterPopout = controlCenterPopout
}
}
}
WifiPasswordModal {
id: wifiPasswordModal
Component.onCompleted: {
PopoutService.wifiPasswordModal = wifiPasswordModal
}
}
PolkitAuthModal {
id: polkitAuthModal
}
property string lastCredentialsToken: ""
property var lastCredentialsTime: 0
Connections {
target: NetworkService
function onCredentialsNeeded(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
const now = Date.now()
const timeSinceLastPrompt = now - lastCredentialsTime
if (wifiPasswordModal.shouldBeVisible && timeSinceLastPrompt < 1000) {
NetworkService.cancelCredentials(lastCredentialsToken)
lastCredentialsToken = token
lastCredentialsTime = now
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService)
return
}
lastCredentialsToken = token
lastCredentialsTime = now
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService)
}
}
LazyLoader {
id: networkInfoModalLoader
active: false
NetworkInfoModal {
id: networkInfoModal
Component.onCompleted: {
PopoutService.networkInfoModal = networkInfoModal
}
}
}
LazyLoader {
id: batteryPopoutLoader
active: false
BatteryPopout {
id: batteryPopout
Component.onCompleted: {
PopoutService.batteryPopout = batteryPopout
}
}
}
LazyLoader {
id: vpnPopoutLoader
active: false
VpnPopout {
id: vpnPopout
Component.onCompleted: {
PopoutService.vpnPopout = vpnPopout
}
}
}
LazyLoader {
id: powerMenuLoader
active: false
PowerMenu {
id: powerMenu
onPowerActionRequested: (action, title, message) => {
if (SettingsData.powerActionConfirm) {
powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) {
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
}
} else {
actionApply(action)
}
}
function actionApply(action) {
switch (action) {
case "logout":
SessionService.logout()
break
case "suspend":
SessionService.suspend()
break
case "hibernate":
SessionService.hibernate()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}
}
}
LazyLoader {
id: powerConfirmModalLoader
active: false
ConfirmModal {
id: powerConfirmModal
}
}
LazyLoader {
id: processListPopoutLoader
active: false
ProcessListPopout {
id: processListPopout
Component.onCompleted: {
PopoutService.processListPopout = processListPopout
}
}
}
SettingsModal {
id: settingsModal
Component.onCompleted: {
PopoutService.settingsModal = settingsModal
}
}
LazyLoader {
id: appDrawerLoader
active: false
AppDrawerPopout {
id: appDrawerPopout
Component.onCompleted: {
PopoutService.appDrawerPopout = appDrawerPopout
}
}
}
SpotlightModal {
id: spotlightModal
Component.onCompleted: {
PopoutService.spotlightModal = spotlightModal
}
}
ClipboardHistoryModal {
id: clipboardHistoryModalPopup
Component.onCompleted: {
PopoutService.clipboardHistoryModal = clipboardHistoryModalPopup
}
}
NotificationModal {
id: notificationModal
Component.onCompleted: {
PopoutService.notificationModal = notificationModal
}
}
DankColorPickerModal {
id: colorPickerModal
Component.onCompleted: {
PopoutService.colorPickerModal = colorPickerModal
}
}
LazyLoader {
id: processListModalLoader
active: false
ProcessListModal {
id: processListModal
Component.onCompleted: {
PopoutService.processListModal = processListModal
}
}
}
LazyLoader {
id: systemUpdateLoader
active: false
SystemUpdatePopout {
id: systemUpdatePopout
Component.onCompleted: {
PopoutService.systemUpdatePopout = systemUpdatePopout
}
}
}
Variants {
id: notepadSlideoutVariants
model: SettingsData.getFilteredScreens("notepad")
delegate: DankSlideout {
id: notepadSlideout
modelData: item
title: I18n.tr("Notepad")
slideoutWidth: 480
expandable: true
expandedWidthValue: 960
customTransparency: SettingsData.notepadTransparencyOverride
content: Component {
Notepad {
onHideRequested: {
notepadSlideout.hide()
}
}
}
function toggle() {
if (isVisible) {
hide()
} else {
show()
}
}
}
}
LazyLoader {
id: powerMenuModalLoader
active: false
PowerMenuModal {
id: powerMenuModal
onPowerActionRequested: (action, title, message) => {
if (SettingsData.powerActionConfirm) {
powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) {
powerConfirmModalLoader.item.confirmButtonColor = action === "poweroff" ? Theme.error : action === "reboot" ? Theme.warning : Theme.primary
powerConfirmModalLoader.item.show(title, message, () => actionApply(action), function () {})
}
} else {
actionApply(action)
}
}
onLockRequested: {
lock.activate()
}
function actionApply(action) {
switch (action) {
case "logout":
SessionService.logout()
break
case "suspend":
SessionService.suspend()
break
case "hibernate":
SessionService.hibernate()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}
Component.onCompleted: {
PopoutService.powerMenuModal = powerMenuModal
}
}
}
LazyLoader {
id: hyprKeybindsModalLoader
active: false
KeybindsModal {
id: keybindsModal
Component.onCompleted: {
PopoutService.hyprKeybindsModal = keybindsModal
}
}
}
DMSShellIPC {
powerMenuModalLoader: powerMenuModalLoader
processListModalLoader: processListModalLoader
controlCenterLoader: controlCenterLoader
dankDashPopoutLoader: dankDashPopoutLoader
notepadSlideoutVariants: notepadSlideoutVariants
hyprKeybindsModalLoader: hyprKeybindsModalLoader
dankBarLoader: dankBarLoader
hyprlandOverviewLoader: hyprlandOverviewLoader
}
Variants {
model: SettingsData.getFilteredScreens("toast")
delegate: Toast {
modelData: item
visible: ToastService.toastVisible
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: VolumeOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: MicMuteOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: BrightnessOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: IdleInhibitorOSD {
modelData: item
}
}
LazyLoader {
id: hyprlandOverviewLoader
active: CompositorService.isHyprland
component: HyprlandOverview {
id: hyprlandOverview
}
}
}

View File

@@ -1,434 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.Common
import qs.Services
Item {
id: root
required property var powerMenuModalLoader
required property var processListModalLoader
required property var controlCenterLoader
required property var dankDashPopoutLoader
required property var notepadSlideoutVariants
required property var hyprKeybindsModalLoader
required property var dankBarLoader
required property var hyprlandOverviewLoader
IpcHandler {
function open() {
root.powerMenuModalLoader.active = true
if (root.powerMenuModalLoader.item)
root.powerMenuModalLoader.item.openCentered()
return "POWERMENU_OPEN_SUCCESS"
}
function close() {
if (root.powerMenuModalLoader.item)
root.powerMenuModalLoader.item.close()
return "POWERMENU_CLOSE_SUCCESS"
}
function toggle() {
root.powerMenuModalLoader.active = true
if (root.powerMenuModalLoader.item) {
if (root.powerMenuModalLoader.item.shouldBeVisible) {
root.powerMenuModalLoader.item.close()
} else {
root.powerMenuModalLoader.item.openCentered()
}
}
return "POWERMENU_TOGGLE_SUCCESS"
}
target: "powermenu"
}
IpcHandler {
function open(): string {
root.processListModalLoader.active = true
if (root.processListModalLoader.item)
root.processListModalLoader.item.show()
return "PROCESSLIST_OPEN_SUCCESS"
}
function close(): string {
if (root.processListModalLoader.item)
root.processListModalLoader.item.hide()
return "PROCESSLIST_CLOSE_SUCCESS"
}
function toggle(): string {
root.processListModalLoader.active = true
if (root.processListModalLoader.item)
root.processListModalLoader.item.toggle()
return "PROCESSLIST_TOGGLE_SUCCESS"
}
target: "processlist"
}
IpcHandler {
function open(): string {
if (root.dankBarLoader.item) {
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
return "CONTROL_CENTER_OPEN_SUCCESS"
}
return "CONTROL_CENTER_OPEN_FAILED"
}
function close(): string {
if (root.controlCenterLoader.item) {
root.controlCenterLoader.item.close()
return "CONTROL_CENTER_CLOSE_SUCCESS"
}
return "CONTROL_CENTER_CLOSE_FAILED"
}
function toggle(): string {
if (root.dankBarLoader.item) {
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
return "CONTROL_CENTER_TOGGLE_SUCCESS"
}
return "CONTROL_CENTER_TOGGLE_FAILED"
}
target: "control-center"
}
IpcHandler {
function open(tab: string): string {
root.dankDashPopoutLoader.active = true
if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1
break
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
break
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0
break
}
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
root.dankDashPopoutLoader.item.dashVisible = true
return "DASH_OPEN_SUCCESS"
}
return "DASH_OPEN_FAILED"
}
function close(): string {
if (root.dankDashPopoutLoader.item) {
root.dankDashPopoutLoader.item.dashVisible = false
return "DASH_CLOSE_SUCCESS"
}
return "DASH_CLOSE_FAILED"
}
function toggle(tab: string): string {
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1
break
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2
break
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0
break
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0
break
}
}
return "DASH_TOGGLE_SUCCESS"
}
return "DASH_TOGGLE_FAILED"
}
target: "dash"
}
IpcHandler {
function getFocusedScreenName() {
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
return Hyprland.focusedWorkspace.monitor.name
}
if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput
}
return ""
}
function getActiveNotepadInstance() {
if (root.notepadSlideoutVariants.instances.length === 0) {
return null
}
if (root.notepadSlideoutVariants.instances.length === 1) {
return root.notepadSlideoutVariants.instances[0]
}
var focusedScreen = getFocusedScreenName()
if (focusedScreen && root.notepadSlideoutVariants.instances.length > 0) {
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
var slideout = root.notepadSlideoutVariants.instances[i]
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
return slideout
}
}
}
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
var slideout = root.notepadSlideoutVariants.instances[i]
if (slideout.isVisible) {
return slideout
}
}
return root.notepadSlideoutVariants.instances[0]
}
function open(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.show()
return "NOTEPAD_OPEN_SUCCESS"
}
return "NOTEPAD_OPEN_FAILED"
}
function close(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.hide()
return "NOTEPAD_CLOSE_SUCCESS"
}
return "NOTEPAD_CLOSE_FAILED"
}
function toggle(): string {
var instance = getActiveNotepadInstance()
if (instance) {
instance.toggle()
return "NOTEPAD_TOGGLE_SUCCESS"
}
return "NOTEPAD_TOGGLE_FAILED"
}
target: "notepad"
}
IpcHandler {
function toggle(): string {
SessionService.toggleIdleInhibit()
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
}
function enable(): string {
SessionService.enableIdleInhibit()
return "Idle inhibit enabled"
}
function disable(): string {
SessionService.disableIdleInhibit()
return "Idle inhibit disabled"
}
function status(): string {
return SessionService.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
}
function reason(newReason: string): string {
if (!newReason) {
return `Current reason: ${SessionService.inhibitReason}`
}
SessionService.setInhibitReason(newReason)
return `Inhibit reason set to: ${newReason}`
}
target: "inhibit"
}
IpcHandler {
function list(): string {
return MprisController.availablePlayers.map(p => p.identity).join("\n")
}
function play(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canPlay) {
MprisController.activePlayer.play()
}
}
function pause(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canPause) {
MprisController.activePlayer.pause()
}
}
function playPause(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) {
MprisController.activePlayer.togglePlaying()
}
}
function previous(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
MprisController.activePlayer.previous()
}
}
function next(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
MprisController.activePlayer.next()
}
}
function stop(): void {
if (MprisController.activePlayer) {
MprisController.activePlayer.stop()
}
}
target: "mpris"
}
IpcHandler {
function toggle(provider: string): string {
if (!provider) {
return "ERROR: No provider specified"
}
KeybindsService.loadProvider(provider)
root.hyprKeybindsModalLoader.active = true
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close()
} else {
root.hyprKeybindsModalLoader.item.open()
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`
}
return `KEYBINDS_TOGGLE_FAILED: ${provider}`
}
function open(provider: string): string {
if (!provider) {
return "ERROR: No provider specified"
}
KeybindsService.loadProvider(provider)
root.hyprKeybindsModalLoader.active = true
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open()
return `KEYBINDS_OPEN_SUCCESS: ${provider}`
}
return `KEYBINDS_OPEN_FAILED: ${provider}`
}
function close(): string {
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close()
return "KEYBINDS_CLOSE_SUCCESS"
}
return "KEYBINDS_CLOSE_FAILED"
}
target: "keybinds"
}
IpcHandler {
function openBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
}
KeybindsService.loadProvider("hyprland")
root.hyprKeybindsModalLoader.active = true
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open()
return "HYPR_KEYBINDS_OPEN_SUCCESS"
}
return "HYPR_KEYBINDS_OPEN_FAILED"
}
function closeBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
}
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close()
return "HYPR_KEYBINDS_CLOSE_SUCCESS"
}
return "HYPR_KEYBINDS_CLOSE_FAILED"
}
function toggleBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
}
KeybindsService.loadProvider("hyprland")
root.hyprKeybindsModalLoader.active = true
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close()
} else {
root.hyprKeybindsModalLoader.item.open()
}
return "HYPR_KEYBINDS_TOGGLE_SUCCESS"
}
return "HYPR_KEYBINDS_TOGGLE_FAILED"
}
function toggleOverview(): string {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE"
}
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS"
}
function closeOverview(): string {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE"
}
root.hyprlandOverviewLoader.item.overviewOpen = false
return "OVERVIEW_CLOSE_SUCCESS"
}
function openOverview(): string {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE"
}
root.hyprlandOverviewLoader.item.overviewOpen = true
return "OVERVIEW_OPEN_SUCCESS"
}
target: "hypr"
}
IpcHandler {
function wallpaper(): string {
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
return "SUCCESS: Toggled wallpaper browser"
}
return "ERROR: Failed to toggle wallpaper browser"
}
target: "dankdash"
}
}

View File

@@ -17,4 +17,5 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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

@@ -1,219 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: clipboardHistoryModal
layerNamespace: "dms:clipboard"
property int totalCount: 0
property var clipboardEntries: []
property string searchText: ""
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property bool showKeyboardHints: false
property Component clipboardContent
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
function updateFilteredModel() {
filteredClipboardModel.clear()
for (var i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry
if (searchText.trim().length === 0) {
filteredClipboardModel.append({
"entry": entry
})
} else {
const content = getEntryPreview(entry).toLowerCase()
if (content.includes(searchText.toLowerCase())) {
filteredClipboardModel.append({
"entry": entry
})
}
}
}
clipboardHistoryModal.totalCount = filteredClipboardModel.count
if (filteredClipboardModel.count === 0) {
keyboardNavigationActive = false
selectedIndex = 0
} else if (selectedIndex >= filteredClipboardModel.count) {
selectedIndex = filteredClipboardModel.count - 1
}
}
function toggle() {
if (shouldBeVisible) {
hide()
} else {
show()
}
}
function show() {
open()
clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
clipboardHistoryModal.shouldHaveFocus = true
refreshClipboard()
keyboardController.reset()
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
contentLoader.item.searchField.forceActiveFocus()
}
})
}
function hide() {
close()
clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
updateFilteredModel()
keyboardController.reset()
cleanupTempFiles()
}
function cleanupTempFiles() {
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
}
function refreshClipboard() {
clipboardProcesses.refresh()
}
function copyEntry(entry) {
const entryId = entry.split('\t')[0]
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
ToastService.showInfo(I18n.tr("Copied to clipboard"))
hide()
}
function deleteEntry(entry) {
clipboardProcesses.deleteEntry(entry)
}
function clearAll() {
clipboardProcesses.clearAll()
}
function getEntryPreview(entry) {
let content = entry.replace(/^\s*\d+\s+/, "")
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
const dimensionMatch = content.match(/(\d+)x(\d+)/)
if (dimensionMatch) {
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
}
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
if (typeMatch) {
return `Image (${typeMatch[1].toUpperCase()})`
}
return "Image"
}
if (content.length > ClipboardConstants.previewLength) {
return content.substring(0, ClipboardConstants.previewLength) + "..."
}
return content
}
function getEntryType(entry) {
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
return "image"
}
if (entry.length > ClipboardConstants.longTextThreshold) {
return "long_text"
}
return "text"
}
visible: false
width: ClipboardConstants.modalWidth
height: ClipboardConstants.modalHeight
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event)
}
content: clipboardContent
ClipboardKeyboardController {
id: keyboardController
modal: clipboardHistoryModal
}
ConfirmModal {
id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary
onVisibleChanged: {
if (visible) {
clipboardHistoryModal.shouldHaveFocus = false
} else if (clipboardHistoryModal.shouldBeVisible) {
clipboardHistoryModal.shouldHaveFocus = true
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
}
}
}
}
property alias filteredClipboardModel: filteredClipboardModel
property alias clipboardModel: clipboardModel
property var confirmDialog: clearConfirmDialog
ListModel {
id: clipboardModel
}
ListModel {
id: filteredClipboardModel
}
ClipboardProcesses {
id: clipboardProcesses
modal: clipboardHistoryModal
clipboardModel: clipboardModel
filteredClipboardModel: filteredClipboardModel
}
IpcHandler {
function open(): string {
clipboardHistoryModal.show()
return "CLIPBOARD_OPEN_SUCCESS"
}
function close(): string {
clipboardHistoryModal.hide()
return "CLIPBOARD_CLOSE_SUCCESS"
}
function toggle(): string {
clipboardHistoryModal.toggle()
return "CLIPBOARD_TOGGLE_SUCCESS"
}
target: "clipboard"
}
clipboardContent: Component {
ClipboardContent {
modal: clipboardHistoryModal
filteredModel: filteredClipboardModel
clearConfirmDialog: clipboardHistoryModal.confirmDialog
}
}
}

View File

@@ -1,131 +0,0 @@
import QtQuick
import qs.Common
QtObject {
id: keyboardController
required property var modal
function reset() {
modal.selectedIndex = 0
modal.keyboardNavigationActive = false
modal.showKeyboardHints = false
}
function selectNext() {
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
return
}
modal.keyboardNavigationActive = true
modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.filteredClipboardModel.count - 1)
}
function selectPrevious() {
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
return
}
modal.keyboardNavigationActive = true
modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0)
}
function copySelected() {
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
return
}
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
modal.copyEntry(selectedEntry)
}
function deleteSelected() {
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
return
}
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
modal.deleteEntry(selectedEntry)
}
function handleKey(event) {
if (event.key === Qt.Key_Escape) {
if (modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = false
event.accepted = true
} else {
modal.hide()
event.accepted = true
}
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
event.accepted = true
} else {
selectNext()
event.accepted = true
}
} else if (event.key === Qt.Key_Up || event.key === Qt.Key_Backtab) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
event.accepted = true
} else if (modal.selectedIndex === 0) {
modal.keyboardNavigationActive = false
event.accepted = true
} else {
selectPrevious()
event.accepted = true
}
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
} else {
selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
} else if (modal.selectedIndex === 0) {
modal.keyboardNavigationActive = false
} else {
selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
} else {
selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true
modal.selectedIndex = 0
} else if (modal.selectedIndex === 0) {
modal.keyboardNavigationActive = false
} else {
selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_Delete && (event.modifiers & Qt.ShiftModifier)) {
modal.clearAll()
modal.hide()
event.accepted = true
} else if (modal.keyboardNavigationActive) {
if ((event.key === Qt.Key_C && (event.modifiers & Qt.ControlModifier)) || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
copySelected()
event.accepted = true
} else if (event.key === Qt.Key_Delete) {
deleteSelected()
event.accepted = true
}
}
if (event.key === Qt.Key_F10) {
modal.showKeyboardHints = !modal.showKeyboardHints
event.accepted = true
}
}
}

View File

@@ -1,94 +0,0 @@
import QtQuick
import Quickshell.Io
QtObject {
id: clipboardProcesses
required property var modal
required property var clipboardModel
required property var filteredClipboardModel
// Load clipboard entries
property var loadProcess: Process {
id: loadProcess
command: ["cliphist", "list"]
running: false
stdout: StdioCollector {
onStreamFinished: {
clipboardModel.clear()
const lines = text.trim().split('\n')
for (const line of lines) {
if (line.trim().length > 0) {
clipboardModel.append({
"entry": line
})
}
}
modal.updateFilteredModel()
}
}
}
// Delete single entry
property var deleteProcess: Process {
id: deleteProcess
property string deletedEntry: ""
running: false
onExited: exitCode => {
if (exitCode === 0) {
for (var i = 0; i < clipboardModel.count; i++) {
if (clipboardModel.get(i).entry === deleteProcess.deletedEntry) {
clipboardModel.remove(i)
break
}
}
for (var j = 0; j < filteredClipboardModel.count; j++) {
if (filteredClipboardModel.get(j).entry === deleteProcess.deletedEntry) {
filteredClipboardModel.remove(j)
break
}
}
modal.totalCount = filteredClipboardModel.count
if (filteredClipboardModel.count === 0) {
modal.keyboardNavigationActive = false
modal.selectedIndex = 0
} else if (modal.selectedIndex >= filteredClipboardModel.count) {
modal.selectedIndex = filteredClipboardModel.count - 1
}
} else {
console.warn("Failed to delete clipboard entry")
}
}
}
// Clear all entries
property var clearProcess: Process {
id: clearProcess
command: ["cliphist", "wipe"]
running: false
onExited: exitCode => {
if (exitCode === 0) {
clipboardModel.clear()
filteredClipboardModel.clear()
modal.totalCount = 0
}
}
}
function refresh() {
loadProcess.running = true
}
function deleteEntry(entry) {
deleteProcess.deletedEntry = entry
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`]
deleteProcess.running = true
}
function clearAll() {
clearProcess.running = true
}
}

View File

@@ -1,327 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property string layerNamespace: "dms:modal"
WlrLayershell.namespace: layerNamespace
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property Item directContent: null
property real width: 400
property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080
readonly property real dpr: CompositorService.getScreenScale(screen)
property bool showBackground: true
property real backgroundOpacity: 0.5
property string positioning: "center"
property point customPosition: Qt.point(0, 0)
property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 1
property real cornerRadius: Theme.cornerRadius
property bool enableShadow: false
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
property bool shouldHaveFocus: shouldBeVisible
property bool allowFocusOverride: false
property bool allowStacking: false
property bool keepContentLoaded: false
signal opened
signal dialogClosed
signal backgroundClicked
function open() {
ModalManager.openModal(root)
closeTimer.stop()
shouldBeVisible = true
visible = true
shouldHaveFocus = false
Qt.callLater(() => {
shouldHaveFocus = Qt.binding(() => shouldBeVisible)
})
}
function close() {
shouldBeVisible = false
shouldHaveFocus = false
closeTimer.restart()
}
function toggle() {
if (shouldBeVisible) {
close()
} else {
open()
}
}
visible: shouldBeVisible
color: "transparent"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
return WlrLayershell.Bottom
case "overlay":
return WlrLayershell.Overlay
case "background":
return WlrLayershell.Background
default:
return WlrLayershell.Top
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
onVisibleChanged: {
if (root.visible) {
opened()
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide()
Qt.inputMethod.reset()
}
dialogClosed()
}
}
Connections {
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
close()
}
}
target: ModalManager
}
Timer {
id: closeTimer
interval: animationDuration + 120
onTriggered: {
visible = false
}
}
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => {
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
root.backgroundClicked()
}
}
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
Item {
id: modalContainer
width: Theme.px(root.width, dpr)
height: Theme.px(root.height, dpr)
x: {
if (positioning === "center") {
return Theme.snap((root.screenWidth - width) / 2, dpr)
} else if (positioning === "top-right") {
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
} else if (positioning === "custom") {
return Theme.snap(root.customPosition.x, dpr)
}
return 0
}
y: {
if (positioning === "center") {
return Theme.snap((root.screenHeight - height) / 2, dpr)
} else if (positioning === "top-right") {
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
} else if (positioning === "custom") {
return Theme.snap(root.customPosition.y, dpr)
}
return 0
}
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
property real animX: 0
property real animY: 0
property real scaleValue: root.animationScaleCollapsed
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr)
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr)
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
}
}
Behavior on animX {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item {
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
layer.enabled: true
layer.smooth: false
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(modalContainer.animY + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity {
NumberAnimation {
duration: animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
DankRectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
radius: root.cornerRadius
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false
Item {
id: directContentWrapper
anchors.fill: parent
visible: root.directContent !== null
focus: true
clip: false
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
}
}
Connections {
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
}
}
target: root
}
}
Loader {
id: contentLoader
anchors.fill: parent
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
}
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.shouldBeVisible || root.visible
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey && shouldHaveFocus) {
root.close()
event.accepted = true
}
}
}
}

View File

@@ -1,934 +0,0 @@
import Qt.labs.folderlistmodel
import QtCore
import QtQuick
import QtQuick.Controls
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Widgets
DankModal {
id: fileBrowserModal
layerNamespace: "dms:file-browser"
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation)
property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation)
property string currentPath: ""
property var fileExtensions: ["*.*"]
property alias filterExtensions: fileBrowserModal.fileExtensions
property string browserTitle: "Select File"
property string browserIcon: "folder_open"
property string browserType: "generic"
property bool showHiddenFiles: false
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property bool backButtonFocused: false
property bool saveMode: false
property string defaultFileName: ""
property int keyboardSelectionIndex: -1
property bool keyboardSelectionRequested: false
property bool showKeyboardHints: false
property bool showFileInfo: false
property string selectedFilePath: ""
property string selectedFileName: ""
property bool selectedFileIsDir: false
property bool showOverwriteConfirmation: false
property string pendingFilePath: ""
property var parentModal: null
property bool showSidebar: true
property string viewMode: "grid"
property string sortBy: "name"
property bool sortAscending: true
property int iconSizeIndex: 1
property var iconSizes: [80, 120, 160, 200]
property bool pathEditMode: false
property bool pathInputHasFocus: false
property int actualGridColumns: 5
property bool _initialized: false
signal fileSelected(string path)
function loadSettings() {
const type = browserType || "default"
const settings = CacheData.fileBrowserSettings[type]
const isImageBrowser = ["wallpaper", "profile"].includes(browserType)
if (settings) {
viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list")
sortBy = settings.sortBy || "name"
sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true
iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1
showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true
} else {
viewMode = isImageBrowser ? "grid" : "list"
}
}
function saveSettings() {
if (!_initialized)
return
const type = browserType || "default"
let settings = CacheData.fileBrowserSettings
if (!settings[type]) {
settings[type] = {}
}
settings[type].viewMode = viewMode
settings[type].sortBy = sortBy
settings[type].sortAscending = sortAscending
settings[type].iconSizeIndex = iconSizeIndex
settings[type].showSidebar = showSidebar
settings[type].lastPath = currentPath
CacheData.fileBrowserSettings = settings
if (browserType === "wallpaper") {
CacheData.wallpaperLastPath = currentPath
} else if (browserType === "profile") {
CacheData.profileLastPath = currentPath
}
CacheData.saveCache()
}
onViewModeChanged: saveSettings()
onSortByChanged: saveSettings()
onSortAscendingChanged: saveSettings()
onIconSizeIndexChanged: saveSettings()
onShowSidebarChanged: saveSettings()
function isImageFile(fileName) {
if (!fileName) {
return false
}
const ext = fileName.toLowerCase().split('.').pop()
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
}
function getLastPath() {
const type = browserType || "default"
const settings = CacheData.fileBrowserSettings[type]
const lastPath = settings?.lastPath || ""
return (lastPath && lastPath !== "") ? lastPath : homeDir
}
function saveLastPath(path) {
const type = browserType || "default"
let settings = CacheData.fileBrowserSettings
if (!settings[type]) {
settings[type] = {}
}
settings[type].lastPath = path
CacheData.fileBrowserSettings = settings
CacheData.saveCache()
if (browserType === "wallpaper") {
CacheData.wallpaperLastPath = path
} else if (browserType === "profile") {
CacheData.profileLastPath = path
}
}
function setSelectedFileData(path, name, isDir) {
selectedFilePath = path
selectedFileName = name
selectedFileIsDir = isDir
}
function navigateUp() {
const path = currentPath
if (path === homeDir)
return
const lastSlash = path.lastIndexOf('/')
if (lastSlash > 0) {
const newPath = path.substring(0, lastSlash)
if (newPath.length < homeDir.length) {
currentPath = homeDir
saveLastPath(homeDir)
} else {
currentPath = newPath
saveLastPath(newPath)
}
}
}
function navigateTo(path) {
currentPath = path
saveLastPath(path)
selectedIndex = -1
backButtonFocused = false
}
function keyboardFileSelection(index) {
if (index >= 0) {
keyboardSelectionTimer.targetIndex = index
keyboardSelectionTimer.start()
}
}
function executeKeyboardSelection(index) {
keyboardSelectionIndex = index
keyboardSelectionRequested = true
}
function handleSaveFile(filePath) {
var normalizedPath = filePath
if (!normalizedPath.startsWith("file://")) {
normalizedPath = "file://" + filePath
}
var exists = false
var fileName = filePath.split('/').pop()
for (var i = 0; i < folderModel.count; i++) {
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
exists = true
break
}
}
if (exists) {
pendingFilePath = normalizedPath
showOverwriteConfirmation = true
} else {
fileSelected(normalizedPath)
fileBrowserModal.close()
}
}
objectName: "fileBrowserModal"
allowStacking: true
closeOnEscapeKey: false
shouldHaveFocus: shouldBeVisible
Component.onCompleted: {
loadSettings()
currentPath = getLastPath()
_initialized = true
}
property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
property int currentPathIndex: 0
width: 800
height: 600
enableShadow: true
visible: false
onBackgroundClicked: close()
onOpened: {
if (parentModal) {
parentModal.shouldHaveFocus = false
parentModal.allowFocusOverride = true
}
Qt.callLater(() => {
if (contentLoader && contentLoader.item) {
contentLoader.item.forceActiveFocus()
}
})
}
onDialogClosed: {
if (parentModal) {
parentModal.allowFocusOverride = false
parentModal.shouldHaveFocus = Qt.binding(() => {
return parentModal.shouldBeVisible
})
}
}
onVisibleChanged: {
if (visible) {
currentPath = getLastPath()
selectedIndex = -1
keyboardNavigationActive = false
backButtonFocused = false
}
}
onCurrentPathChanged: {
selectedFilePath = ""
selectedFileName = ""
selectedFileIsDir = false
saveSettings()
}
onSelectedIndexChanged: {
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
selectedFilePath = ""
selectedFileName = ""
selectedFileIsDir = false
}
}
FolderListModel {
id: folderModel
showDirsFirst: true
showDotAndDotDot: false
showHidden: fileBrowserModal.showHiddenFiles
nameFilters: fileExtensions
showFiles: true
showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
sortField: {
switch (sortBy) {
case "name":
return FolderListModel.Name
case "size":
return FolderListModel.Size
case "modified":
return FolderListModel.Time
case "type":
return FolderListModel.Type
default:
return FolderListModel.Name
}
}
sortReversed: !sortAscending
}
property var quickAccessLocations: [{
"name": "Home",
"path": homeDir,
"icon": "home"
}, {
"name": "Documents",
"path": docsDir,
"icon": "description"
}, {
"name": "Downloads",
"path": downloadDir,
"icon": "download"
}, {
"name": "Pictures",
"path": picsDir,
"icon": "image"
}, {
"name": "Music",
"path": musicDir,
"icon": "music_note"
}, {
"name": "Videos",
"path": videosDir,
"icon": "movie"
}, {
"name": "Desktop",
"path": desktopDir,
"icon": "computer"
}]
QtObject {
id: keyboardController
property int totalItems: folderModel.count
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
function handleKey(event) {
if (event.key === Qt.Key_Escape) {
close()
event.accepted = true
return
}
if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints
event.accepted = true
return
}
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
showFileInfo = !showFileInfo
event.accepted = true
return
}
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
if (currentPath !== homeDir) {
navigateUp()
event.accepted = true
}
return
}
if (!keyboardNavigationActive) {
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key
=== Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier)
if (isInitKey) {
keyboardNavigationActive = true
if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
} else {
backButtonFocused = false
selectedIndex = 0
}
event.accepted = true
}
return
}
switch (event.key) {
case Qt.Key_Tab:
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
} else if (selectedIndex < totalItems - 1) {
selectedIndex++
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
} else {
selectedIndex = 0
}
event.accepted = true
break
case Qt.Key_Backtab:
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = totalItems - 1
} else if (selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
} else {
selectedIndex = totalItems - 1
}
event.accepted = true
break
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
} else if (selectedIndex < totalItems - 1) {
selectedIndex++
}
event.accepted = true
}
break
case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) {
if (selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
}
break
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
if (selectedIndex < totalItems - 1) {
selectedIndex++
}
event.accepted = true
}
break
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
if (selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
}
break
case Qt.Key_H:
if (event.modifiers & Qt.ControlModifier) {
if (!backButtonFocused && selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
}
break
case Qt.Key_L:
if (event.modifiers & Qt.ControlModifier) {
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
} else if (selectedIndex < totalItems - 1) {
selectedIndex++
}
event.accepted = true
}
break
case Qt.Key_Left:
if (pathInputHasFocus)
return
if (backButtonFocused)
return
if (selectedIndex > 0) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
break
case Qt.Key_Right:
if (pathInputHasFocus)
return
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
} else if (selectedIndex < totalItems - 1) {
selectedIndex++
}
event.accepted = true
break
case Qt.Key_Up:
if (backButtonFocused) {
backButtonFocused = false
if (gridColumns === 1) {
selectedIndex = 0
} else {
var col = selectedIndex % gridColumns
selectedIndex = Math.min(col, totalItems - 1)
}
} else if (selectedIndex >= gridColumns) {
selectedIndex -= gridColumns
} else if (selectedIndex > 0 && gridColumns === 1) {
selectedIndex--
} else if (currentPath !== homeDir) {
backButtonFocused = true
selectedIndex = -1
}
event.accepted = true
break
case Qt.Key_Down:
if (backButtonFocused) {
backButtonFocused = false
selectedIndex = 0
} else if (gridColumns === 1) {
if (selectedIndex < totalItems - 1) {
selectedIndex++
}
} else {
var newIndex = selectedIndex + gridColumns
if (newIndex < totalItems) {
selectedIndex = newIndex
} else {
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
var col = selectedIndex % gridColumns
var targetIndex = lastRowStart + col
if (targetIndex < totalItems && targetIndex > selectedIndex) {
selectedIndex = targetIndex
}
}
}
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
case Qt.Key_Space:
if (backButtonFocused)
navigateUp()
else if (selectedIndex >= 0 && selectedIndex < totalItems)
fileBrowserModal.keyboardFileSelection(selectedIndex)
event.accepted = true
break
}
}
}
Timer {
id: keyboardSelectionTimer
property int targetIndex: -1
interval: 1
onTriggered: {
executeKeyboardSelection(targetIndex)
}
}
content: Component {
Item {
anchors.fill: parent
Keys.onPressed: event => {
keyboardController.handleKey(event)
}
onVisibleChanged: {
if (visible) {
forceActiveFocus()
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 48
Row {
spacing: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
DankIcon {
name: browserIcon
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: browserTitle
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
circular: false
iconName: showHiddenFiles ? "visibility_off" : "visibility"
iconSize: Theme.iconSize - 4
iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
onClicked: showHiddenFiles = !showHiddenFiles
}
DankActionButton {
circular: false
iconName: viewMode === "grid" ? "view_list" : "grid_view"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
}
DankActionButton {
circular: false
iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
visible: viewMode === "grid"
onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
}
DankActionButton {
circular: false
iconName: "info"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: fileBrowserModal.close()
}
}
}
StyledRect {
width: parent.width
height: 1
color: Theme.outline
}
Item {
width: parent.width
height: parent.height - 49
Row {
anchors.fill: parent
spacing: 0
Row {
width: showSidebar ? 201 : 0
height: parent.height
spacing: 0
visible: showSidebar
FileBrowserSidebar {
height: parent.height
quickAccessLocations: fileBrowserModal.quickAccessLocations
currentPath: fileBrowserModal.currentPath
onLocationSelected: path => navigateTo(path)
}
StyledRect {
width: 1
height: parent.height
color: Theme.outline
}
}
Column {
width: parent.width - (showSidebar ? 201 : 0)
height: parent.height
spacing: 0
FileBrowserNavigation {
width: parent.width
currentPath: fileBrowserModal.currentPath
homeDir: fileBrowserModal.homeDir
backButtonFocused: fileBrowserModal.backButtonFocused
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
showSidebar: fileBrowserModal.showSidebar
pathEditMode: fileBrowserModal.pathEditMode
onNavigateUp: fileBrowserModal.navigateUp()
onNavigateTo: path => fileBrowserModal.navigateTo(path)
onPathInputFocusChanged: hasFocus => {
fileBrowserModal.pathInputHasFocus = hasFocus
if (hasFocus) {
fileBrowserModal.pathEditMode = true
}
}
}
StyledRect {
width: parent.width
height: 1
color: Theme.outline
}
Item {
id: gridContainer
width: parent.width
height: parent.height - 41
clip: true
property real gridCellWidth: iconSizes[iconSizeIndex] + 24
property real gridCellHeight: iconSizes[iconSizeIndex] + 56
property real availableGridWidth: width - Theme.spacingM * 2
property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
onGridColumnsChanged: {
fileBrowserModal.actualGridColumns = gridColumns
}
Component.onCompleted: {
fileBrowserModal.actualGridColumns = gridColumns
}
DankGridView {
id: fileGrid
anchors.fill: parent
anchors.leftMargin: gridContainer.gridLeftMargin
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: viewMode === "grid"
cellWidth: gridContainer.gridCellWidth
cellHeight: gridContainer.gridCellHeight
cacheBuffer: 260
model: folderModel
currentIndex: selectedIndex
onCurrentIndexChanged: {
if (keyboardNavigationActive && currentIndex >= 0)
positionViewAtIndex(currentIndex, GridView.Contain)
}
ScrollBar.vertical: DankScrollbar {
id: gridScrollbar
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: FileBrowserGridDelegate {
iconSizes: fileBrowserModal.iconSizes
iconSizeIndex: fileBrowserModal.iconSizeIndex
selectedIndex: fileBrowserModal.selectedIndex
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
onItemClicked: (index, path, name, isDir) => {
selectedIndex = index
setSelectedFileData(path, name, isDir)
if (isDir) {
navigateTo(path)
} else {
fileSelected(path)
fileBrowserModal.close()
}
}
onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir)
}
Connections {
function onKeyboardSelectionRequestedChanged() {
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
fileBrowserModal.keyboardSelectionRequested = false
selectedIndex = index
setSelectedFileData(filePath, fileName, fileIsDir)
if (fileIsDir) {
navigateTo(filePath)
} else {
fileSelected(filePath)
fileBrowserModal.close()
}
}
}
target: fileBrowserModal
}
}
}
DankListView {
id: fileList
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: viewMode === "list"
spacing: 2
model: folderModel
currentIndex: selectedIndex
onCurrentIndexChanged: {
if (keyboardNavigationActive && currentIndex >= 0)
positionViewAtIndex(currentIndex, ListView.Contain)
}
ScrollBar.vertical: DankScrollbar {
id: listScrollbar
}
delegate: FileBrowserListDelegate {
width: fileList.width
selectedIndex: fileBrowserModal.selectedIndex
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
onItemClicked: (index, path, name, isDir) => {
selectedIndex = index
setSelectedFileData(path, name, isDir)
if (isDir) {
navigateTo(path)
} else {
fileSelected(path)
fileBrowserModal.close()
}
}
onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir)
}
Connections {
function onKeyboardSelectionRequestedChanged() {
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
fileBrowserModal.keyboardSelectionRequested = false
selectedIndex = index
setSelectedFileData(filePath, fileName, fileIsDir)
if (fileIsDir) {
navigateTo(filePath)
} else {
fileSelected(filePath)
fileBrowserModal.close()
}
}
}
target: fileBrowserModal
}
}
}
}
}
}
FileBrowserSaveRow {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
saveMode: fileBrowserModal.saveMode
defaultFileName: fileBrowserModal.defaultFileName
currentPath: fileBrowserModal.currentPath
onSaveRequested: filePath => handleSaveFile(filePath)
}
KeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: fileBrowserModal.showKeyboardHints
}
FileInfo {
id: fileInfo
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingL
width: 300
showFileInfo: fileBrowserModal.showFileInfo
selectedIndex: fileBrowserModal.selectedIndex
sourceFolderModel: folderModel
currentPath: fileBrowserModal.currentPath
currentFileName: fileBrowserModal.selectedFileName
currentFileIsDir: fileBrowserModal.selectedFileIsDir
currentFileExtension: {
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName)
return ""
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
}
}
FileBrowserSortMenu {
id: sortMenu
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 120
anchors.rightMargin: Theme.spacingL
sortBy: fileBrowserModal.sortBy
sortAscending: fileBrowserModal.sortAscending
onSortBySelected: value => {
fileBrowserModal.sortBy = value
}
onSortOrderSelected: ascending => {
fileBrowserModal.sortAscending = ascending
}
}
}
}
FileBrowserOverwriteDialog {
anchors.fill: parent
showDialog: showOverwriteConfirmation
pendingFilePath: fileBrowserModal.pendingFilePath
onConfirmed: filePath => {
showOverwriteConfirmation = false
fileSelected(filePath)
pendingFilePath = ""
Qt.callLater(() => fileBrowserModal.close())
}
onCancelled: {
showOverwriteConfirmation = false
pendingFilePath = ""
}
}
}
}
}

View File

@@ -1,232 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
layerNamespace: "dms:keybinds"
property real scrollStep: 60
property var activeFlickable: null
property real _maxW: Math.min(Screen.width * 0.92, 1200)
property real _maxH: Math.min(Screen.height * 0.92, 900)
width: _maxW
height: _maxH
onBackgroundClicked: close()
function scrollDown() {
if (!root.activeFlickable) return
let newY = root.activeFlickable.contentY + scrollStep
newY = Math.min(newY, root.activeFlickable.contentHeight - root.activeFlickable.height)
root.activeFlickable.contentY = newY
}
function scrollUp() {
if (!root.activeFlickable) return
let newY = root.activeFlickable.contentY - root.scrollStep
newY = Math.max(0, newY)
root.activeFlickable.contentY = newY
}
Shortcut { sequence: "Ctrl+j"; onActivated: root.scrollDown() }
Shortcut { sequence: "Down"; onActivated: root.scrollDown() }
Shortcut { sequence: "Ctrl+k"; onActivated: root.scrollUp() }
Shortcut { sequence: "Up"; onActivated: root.scrollUp() }
Shortcut { sequence: "Esc"; onActivated: root.close() }
content: Component {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
StyledText {
text: KeybindsService.keybinds.title || "Keybinds"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.primary
}
DankFlickable {
id: mainFlickable
width: parent.width
height: parent.height - parent.spacing - 40
contentWidth: rowLayout.implicitWidth
contentHeight: rowLayout.implicitHeight
clip: true
Component.onCompleted: root.activeFlickable = mainFlickable
property var rawBinds: KeybindsService.keybinds.binds || {}
property var categories: {
const processed = {}
for (const cat in rawBinds) {
const binds = rawBinds[cat]
const subcats = {}
let hasSubcats = false
for (let i = 0; i < binds.length; i++) {
const bind = binds[i]
if (bind.subcat) {
hasSubcats = true
if (!subcats[bind.subcat]) {
subcats[bind.subcat] = []
}
subcats[bind.subcat].push(bind)
} else {
if (!subcats["_root"]) {
subcats["_root"] = []
}
subcats["_root"].push(bind)
}
}
processed[cat] = {
hasSubcats: hasSubcats,
subcats: subcats,
subcatKeys: Object.keys(subcats)
}
}
return processed
}
property var categoryKeys: Object.keys(categories)
function distributeCategories(cols) {
const columns = []
for (let i = 0; i < cols; i++) {
columns.push([])
}
for (let i = 0; i < categoryKeys.length; i++) {
columns[i % cols].push(categoryKeys[i])
}
return columns
}
Row {
id: rowLayout
width: mainFlickable.width
spacing: Theme.spacingM
property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
property var columnCategories: mainFlickable.distributeCategories(numColumns)
Repeater {
model: rowLayout.numColumns
Column {
id: masonryColumn
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
spacing: Theme.spacingM
Repeater {
model: rowLayout.columnCategories[index] || []
Column {
id: categoryColumn
width: parent.width
spacing: Theme.spacingXS
property string catName: modelData
property var catData: mainFlickable.categories[catName]
StyledText {
text: categoryColumn.catName
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.primary
}
Rectangle {
width: parent.width
height: 1
color: Theme.primary
opacity: 0.3
}
Item { width: 1; height: Theme.spacingXS }
Column {
width: parent.width
spacing: Theme.spacingM
Repeater {
model: categoryColumn.catData?.subcatKeys || []
Column {
width: parent.width
spacing: Theme.spacingXS
property string subcatName: modelData
property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
StyledText {
visible: parent.subcatName !== "_root"
text: parent.subcatName
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
color: Theme.primary
opacity: 0.7
}
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: parent.parent.subcatBinds
Row {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: Math.min(140, parent.width * 0.42)
height: 22
radius: 4
opacity: 0.9
StyledText {
anchors.centerIn: parent
anchors.margins: 2
width: parent.width - 4
color: Theme.secondary
text: modelData.key || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
isMonospace: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
StyledText {
width: parent.width - 150
text: modelData.desc || ""
font.pixelSize: Theme.fontSizeSmall
opacity: 0.9
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -1,358 +0,0 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
layerNamespace: "dms:polkit"
property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false
property real minHeight: 240
function show() {
passwordInput = ""
isLoading = false
open()
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus()
}
})
}
shouldBeVisible: false
width: 420
height: Math.max(minHeight, contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240)
Connections {
target: contentLoader.item
function onImplicitHeightChanged() {
if (shouldBeVisible && contentLoader.item) {
const newHeight = contentLoader.item.implicitHeight + Theme.spacingM * 2
if (newHeight > minHeight) {
minHeight = newHeight
}
}
}
}
onOpened: {
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus()
}
})
}
onClosed: {
passwordInput = ""
isLoading = false
}
onBackgroundClicked: () => {
if (currentFlow && !isLoading) {
currentFlow.cancelAuthenticationRequest()
}
}
Connections {
target: PolkitService.agent
enabled: PolkitService.polkitAvailable
function onAuthenticationRequestStarted() {
show()
}
function onIsActiveChanged() {
if (!(PolkitService.agent?.isActive ?? false)) {
close()
}
}
}
Connections {
target: currentFlow
enabled: currentFlow !== null
function onIsResponseRequiredChanged() {
if (currentFlow.isResponseRequired) {
isLoading = false
passwordInput = ""
if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus()
}
}
}
function onAuthenticationSucceeded() {
close()
}
function onAuthenticationFailed() {
isLoading = false
}
function onAuthenticationRequestCancelled() {
close()
}
}
content: Component {
FocusScope {
id: authContent
property alias passwordField: passwordField
anchors.fill: parent
focus: true
implicitHeight: headerRow.implicitHeight + mainColumn.implicitHeight + Theme.spacingM
Keys.onEscapePressed: event => {
if (currentFlow && !isLoading) {
currentFlow.cancelAuthenticationRequest()
}
event.accepted = true
}
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingM
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Authentication Required")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: currentFlow?.message ?? ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
wrapMode: Text.Wrap
}
StyledText {
visible: (currentFlow?.supplementaryMessage ?? "") !== ""
text: currentFlow?.supplementaryMessage ?? ""
font.pixelSize: Theme.fontSizeSmall
color: (currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium
width: parent.width
wrapMode: Text.Wrap
opacity: (currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8
}
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
enabled: !isLoading
opacity: enabled ? 1 : 0.5
onClicked: () => {
if (currentFlow) {
currentFlow.cancelAuthenticationRequest()
}
}
}
}
Column {
id: mainColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
spacing: Theme.spacingM
StyledText {
text: currentFlow?.inputPrompt ?? ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
visible: (currentFlow?.inputPrompt ?? "") !== ""
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordField.activeFocus ? 2 : 1
opacity: isLoading ? 0.5 : 1
MouseArea {
anchors.fill: parent
enabled: !isLoading
onClicked: () => {
passwordField.forceActiveFocus()
}
}
DankTextField {
id: passwordField
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: passwordInput
echoMode: (currentFlow?.responseVisible ?? false) ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
enabled: !isLoading
onTextEdited: () => {
passwordInput = text
}
onAccepted: () => {
if (passwordInput.length > 0 && currentFlow && !isLoading) {
isLoading = true
currentFlow.submit(passwordInput)
passwordInput = ""
}
}
}
}
Item {
width: parent.width
height: (currentFlow?.failed ?? false) ? failedText.implicitHeight : 0
visible: height > 0
StyledText {
id: failedText
text: I18n.tr("Authentication failed, please try again")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
width: parent.width
opacity: (currentFlow?.failed ?? false) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
enabled: !isLoading
opacity: enabled ? 1 : 0.5
StyledText {
id: cancelText
anchors.centerIn: parent
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: () => {
if (currentFlow) {
currentFlow.cancelAuthenticationRequest()
}
}
}
}
Rectangle {
width: Math.max(80, authText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: authArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: !isLoading && (passwordInput.length > 0 || !(currentFlow?.isResponseRequired ?? true))
opacity: enabled ? 1 : 0.5
StyledText {
id: authText
anchors.centerIn: parent
text: I18n.tr("Authenticate")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: authArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: () => {
if (currentFlow && !isLoading) {
isLoading = true
currentFlow.submit(passwordInput)
passwordInput = ""
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}

View File

@@ -1,375 +0,0 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
layerNamespace: "dms:power-menu"
property int selectedRow: 0
property int selectedCol: 0
property int selectedIndex: selectedRow * gridColumns + selectedCol
property rect parentBounds: Qt.rect(0, 0, 0, 0)
property var parentScreen: null
property var visibleActions: []
property int gridColumns: 3
property int gridRows: 2
signal powerActionRequested(string action, string title, string message)
signal lockRequested
function openCentered() {
parentBounds = Qt.rect(0, 0, 0, 0)
parentScreen = null
backgroundOpacity = 0.5
open()
}
function openFromControlCenter(bounds, targetScreen) {
parentBounds = bounds
parentScreen = targetScreen
backgroundOpacity = 0
open()
}
function updateVisibleActions() {
const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
visibleActions = allActions.filter(action => {
if (action === "hibernate" && !SessionService.hibernateSupported)
return false
return true
})
const count = visibleActions.length
switch (count) {
case 0:
gridColumns = 1
gridRows = 1
break
case 1:
case 2:
case 3:
gridColumns = 1
gridRows = count
break
case 4:
gridColumns = 2
gridRows = 2
break
default:
gridColumns = 3
gridRows = Math.ceil(count / 3)
break
}
}
function getDefaultActionIndex() {
const defaultAction = SettingsData.powerMenuDefaultAction || "logout"
const index = visibleActions.indexOf(defaultAction)
return index >= 0 ? index : 0
}
function getActionAtIndex(index) {
if (index < 0 || index >= visibleActions.length)
return ""
return visibleActions[index]
}
function getActionData(action) {
switch (action) {
case "reboot":
return {
"icon": "restart_alt",
"label": I18n.tr("Reboot"),
"key": "R"
}
case "logout":
return {
"icon": "logout",
"label": I18n.tr("Log Out"),
"key": "X"
}
case "poweroff":
return {
"icon": "power_settings_new",
"label": I18n.tr("Power Off"),
"key": "P"
}
case "lock":
return {
"icon": "lock",
"label": I18n.tr("Lock"),
"key": "L"
}
case "suspend":
return {
"icon": "bedtime",
"label": I18n.tr("Suspend"),
"key": "S"
}
case "hibernate":
return {
"icon": "ac_unit",
"label": I18n.tr("Hibernate"),
"key": "H"
}
case "restart":
return {
"icon": "refresh",
"label": I18n.tr("Restart DMS"),
"key": "D"
}
default:
return {
"icon": "help",
"label": action,
"key": "?"
}
}
}
function selectOption(action) {
if (action === "lock") {
close()
lockRequested()
return
}
if (action === "restart") {
close()
Quickshell.execDetached(["dms", "restart"])
return
}
close()
const actions = {
"logout": {
"title": I18n.tr("Log Out"),
"message": I18n.tr("Are you sure you want to log out?")
},
"suspend": {
"title": I18n.tr("Suspend"),
"message": I18n.tr("Are you sure you want to suspend the system?")
},
"hibernate": {
"title": I18n.tr("Hibernate"),
"message": I18n.tr("Are you sure you want to hibernate the system?")
},
"reboot": {
"title": I18n.tr("Reboot"),
"message": I18n.tr("Are you sure you want to reboot the system?")
},
"poweroff": {
"title": I18n.tr("Power Off"),
"message": I18n.tr("Are you sure you want to power off the system?")
}
}
const selected = actions[action]
if (selected) {
root.powerActionRequested(action, selected.title, selected.message)
}
}
shouldBeVisible: false
width: Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2)
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
enableShadow: true
screen: parentScreen
positioning: parentBounds.width > 0 ? "custom" : "center"
customPosition: {
if (parentBounds.width > 0) {
const centerX = parentBounds.x + (parentBounds.width - width) / 2
const centerY = parentBounds.y + (parentBounds.height - height) / 2
return Qt.point(centerX, centerY)
}
return Qt.point(0, 0)
}
onBackgroundClicked: () => close()
onOpened: () => {
updateVisibleActions()
const defaultIndex = getDefaultActionIndex()
selectedRow = Math.floor(defaultIndex / gridColumns)
selectedCol = defaultIndex % gridColumns
Qt.callLater(() => modalFocusScope.forceActiveFocus())
}
Component.onCompleted: updateVisibleActions()
modalFocusScope.Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Left:
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
event.accepted = true
break
case Qt.Key_Right:
selectedCol = (selectedCol + 1) % gridColumns
event.accepted = true
break
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedRow = (selectedRow - 1 + gridRows) % gridRows
event.accepted = true
break
case Qt.Key_Down:
case Qt.Key_Tab:
selectedRow = (selectedRow + 1) % gridRows
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex))
event.accepted = true
break
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedCol = (selectedCol + 1) % gridColumns
event.accepted = true
}
break
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
selectOption("poweroff")
event.accepted = true
} else {
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
event.accepted = true
}
break
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow + 1) % gridRows
event.accepted = true
}
break
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow - 1 + gridRows) % gridRows
event.accepted = true
}
break
case Qt.Key_R:
selectOption("reboot")
event.accepted = true
break
case Qt.Key_X:
selectOption("logout")
event.accepted = true
break
case Qt.Key_L:
selectOption("lock")
event.accepted = true
break
case Qt.Key_S:
selectOption("suspend")
event.accepted = true
break
case Qt.Key_H:
selectOption("hibernate")
event.accepted = true
break
case Qt.Key_D:
selectOption("restart")
event.accepted = true
break
}
}
content: Component {
Item {
anchors.fill: parent
implicitHeight: buttonGrid.implicitHeight + Theme.spacingL * 2
Grid {
id: buttonGrid
anchors.centerIn: parent
columns: root.gridColumns
columnSpacing: Theme.spacingS
rowSpacing: Theme.spacingS
Repeater {
model: root.visibleActions
Rectangle {
required property int index
required property string modelData
readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
width: (root.width - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
height: 100
radius: Theme.cornerRadius
color: {
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
if (mouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
border.color: isSelected ? Theme.primary : "transparent"
border.width: isSelected ? 2 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: parent.parent.actionData.icon
size: Theme.iconSize + 8
color: {
if (parent.parent.showWarning && mouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
}
return Theme.surfaceText
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: parent.parent.actionData.label
font.pixelSize: Theme.fontSizeMedium
color: {
if (parent.parent.showWarning && mouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
}
return Theme.surfaceText
}
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
width: 20
height: 16
radius: 4
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: parent.parent.parent.actionData.key
font.pixelSize: Theme.fontSizeSmall - 1
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
anchors.centerIn: parent
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedRow = Math.floor(index / root.gridColumns)
root.selectedCol = index % root.gridColumns
root.selectOption(modelData)
}
}
}
}
}
}
}
}

View File

@@ -1,358 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Modals.Common
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
DankModal {
id: processListModal
layerNamespace: "dms:process-list-modal"
property int currentTab: 0
property var tabNames: ["Processes", "Performance", "System"]
function show() {
if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available");
return ;
}
open();
UserInfoService.getUptime();
}
function hide() {
close();
if (processContextMenu.visible) {
processContextMenu.close();
}
}
function toggle() {
if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available");
return ;
}
if (shouldBeVisible) {
hide();
} else {
show();
}
}
width: 900
height: 680
visible: false
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
enableShadow: true
onBackgroundClicked: () => {
return hide();
}
Component {
id: processesTabComponent
ProcessesTab {
contextMenu: processContextMenu
}
}
Component {
id: performanceTabComponent
PerformanceTab {
}
}
Component {
id: systemTabComponent
SystemTab {
}
}
ProcessContextMenu {
id: processContextMenu
}
content: Component {
Item {
anchors.fill: parent
focus: true
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
processListModal.hide();
event.accepted = true;
} else if (event.key === Qt.Key_1) {
currentTab = 0;
event.accepted = true;
} else if (event.key === Qt.Key_2) {
currentTab = 1;
event.accepted = true;
} else if (event.key === Qt.Key_3) {
currentTab = 2;
event.accepted = true;
}
}
// Show error message when dgop is not available
Rectangle {
anchors.centerIn: parent
width: 400
height: 200
radius: Theme.cornerRadius
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
border.color: Theme.error
border.width: 2
visible: !DgopService.dgopAvailable
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: "error"
size: 48
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("System Monitor Unavailable")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
visible: DgopService.dgopAvailable
RowLayout {
Layout.fillWidth: true
height: 40
StyledText {
text: I18n.tr("System Monitor")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
return processListModal.hide();
}
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineLight
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 4
spacing: 2
Repeater {
model: tabNames
Rectangle {
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
height: 44
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
const tabIcons = ["list_alt", "analytics", "settings"];
return tabIcons[index] || "tab";
}
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
currentTab = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: 1
Loader {
id: processesTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: performanceTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: systemTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
}
}

View File

@@ -1,780 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: powerTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: lockScreenSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "lock"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Lock Screen")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Power Actions")
description: I18n.tr("Show power, restart, and logout buttons on the lock screen")
checked: SettingsData.lockScreenShowPowerActions
onToggled: checked => SettingsData.set("lockScreenShowPowerActions", checked)
}
StyledText {
text: I18n.tr("loginctl not available - lock integration requires DMS socket connection")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
visible: !SessionService.loginctlAvailable
width: parent.width
wrapMode: Text.Wrap
}
DankToggle {
width: parent.width
text: I18n.tr("Enable loginctl lock integration")
description: I18n.tr("Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen")
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
enabled: SessionService.loginctlAvailable
onToggled: checked => {
if (SessionService.loginctlAvailable) {
SettingsData.set("loginctlLockIntegration", checked)
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Lock before suspend")
description: I18n.tr("Automatically lock the screen when the system prepares to suspend")
checked: SettingsData.lockBeforeSuspend
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
onToggled: checked => SettingsData.set("lockBeforeSuspend", checked)
}
DankToggle {
width: parent.width
text: I18n.tr("Enable fingerprint authentication")
description: I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)")
checked: SettingsData.enableFprint
visible: SettingsData.fprintdAvailable
onToggled: checked => SettingsData.set("enableFprint", checked)
}
}
}
StyledRect {
width: parent.width
height: timeoutSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: timeoutSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Idle Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - parent.children[0].width - parent.children[1].width - powerCategory.width - Theme.spacingM * 3)
height: parent.height
}
DankButtonGroup {
id: powerCategory
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
model: ["AC Power", "Battery"]
currentIndex: 0
selectionMode: "single"
checkEnabled: false
}
}
DankToggle {
width: parent.width
text: I18n.tr("Prevent idle for media")
description: I18n.tr("Inhibit idle timeout when audio or video is playing")
checked: SettingsData.preventIdleForMedia
visible: IdleService.idleMonitorAvailable
onToggled: checked => SettingsData.set("preventIdleForMedia", checked)
}
DankDropdown {
id: lockDropdown
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
addHorizontalPadding: true
text: I18n.tr("Automatically lock after")
options: timeoutOptions
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout
const index = lockDropdown.timeoutValues.indexOf(currentTimeout)
lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never"
}
}
Component.onCompleted: {
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout
const index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
onValueChanged: value => {
const index = timeoutOptions.indexOf(value)
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SettingsData.set("acLockTimeout", timeout)
} else {
SettingsData.set("batteryLockTimeout", timeout)
}
}
}
}
DankDropdown {
id: monitorDropdown
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
addHorizontalPadding: true
text: I18n.tr("Turn off monitors after")
options: timeoutOptions
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout
const index = monitorDropdown.timeoutValues.indexOf(currentTimeout)
monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never"
}
}
Component.onCompleted: {
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout
const index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
onValueChanged: value => {
const index = timeoutOptions.indexOf(value)
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SettingsData.set("acMonitorTimeout", timeout)
} else {
SettingsData.set("batteryMonitorTimeout", timeout)
}
}
}
}
DankDropdown {
id: suspendDropdown
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
addHorizontalPadding: true
text: I18n.tr("Suspend system after")
options: timeoutOptions
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout
const index = suspendDropdown.timeoutValues.indexOf(currentTimeout)
suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never"
}
}
Component.onCompleted: {
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout
const index = timeoutValues.indexOf(currentTimeout)
currentValue = index >= 0 ? timeoutOptions[index] : "Never"
}
onValueChanged: value => {
const index = timeoutOptions.indexOf(value)
if (index >= 0) {
const timeout = timeoutValues[index]
if (powerCategory.currentIndex === 0) {
SettingsData.set("acSuspendTimeout", timeout)
} else {
SettingsData.set("batterySuspendTimeout", timeout)
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: SessionService.hibernateSupported
StyledText {
text: I18n.tr("Suspend behavior")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
leftPadding: Theme.spacingM
}
DankButtonGroup {
id: suspendBehaviorSelector
anchors.horizontalCenter: parent.horizontalCenter
model: ["Suspend", "Hibernate", "Suspend then Hibernate"]
selectionMode: "single"
checkEnabled: false
Connections {
target: powerCategory
function onCurrentIndexChanged() {
const behavior = powerCategory.currentIndex === 0 ? SettingsData.acSuspendBehavior : SettingsData.batterySuspendBehavior
suspendBehaviorSelector.currentIndex = behavior
}
}
Component.onCompleted: {
const behavior = powerCategory.currentIndex === 0 ? SettingsData.acSuspendBehavior : SettingsData.batterySuspendBehavior
currentIndex = behavior
}
onSelectionChanged: (index, selected) => {
if (selected) {
if (powerCategory.currentIndex === 0) {
SettingsData.set("acSuspendBehavior", index)
} else {
SettingsData.set("batterySuspendBehavior", index)
}
}
}
}
}
StyledText {
text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
visible: !IdleService.idleMonitorAvailable
}
}
}
StyledRect {
width: parent.width
height: powerMenuCustomSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: powerMenuCustomSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "tune"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Power Menu Customization")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("Customize which actions appear in the power menu")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
DankDropdown {
id: defaultActionDropdown
width: parent.width
addHorizontalPadding: true
text: I18n.tr("Default selected action")
options: ["Reboot", "Log Out", "Power Off", "Lock", "Suspend", "Restart DMS", "Hibernate"]
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"]
Component.onCompleted: {
const currentAction = SettingsData.powerMenuDefaultAction || "logout"
const index = actionValues.indexOf(currentAction)
currentValue = index >= 0 ? options[index] : "Log Out"
}
onValueChanged: value => {
const index = options.indexOf(value)
if (index >= 0) {
SettingsData.set("powerMenuDefaultAction", actionValues[index])
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
DankToggle {
width: parent.width
text: I18n.tr("Show Reboot")
checked: SettingsData.powerMenuActions.includes("reboot")
onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]
if (checked && !actions.includes("reboot")) {
actions.push("reboot")
} else if (!checked) {
actions = actions.filter(a => a !== "reboot")
}
SettingsData.set("powerMenuActions", actions)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Log Out")
checked: SettingsData.powerMenuActions.includes("logout")
onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]
if (checked && !actions.includes("logout")) {
actions.push("logout")
} else if (!checked) {
actions = actions.filter(a => a !== "logout")
}
SettingsData.set("powerMenuActions", actions)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Power Off")
checked: SettingsData.powerMenuActions.includes("poweroff")
onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]
if (checked && !actions.includes("poweroff")) {
actions.push("poweroff")
} else if (!checked) {
actions = actions.filter(a => a !== "poweroff")
}
SettingsData.set("powerMenuActions", actions)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Lock")
checked: SettingsData.powerMenuActions.includes("lock")
onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]
if (checked && !actions.includes("lock")) {
actions.push("lock")
} else if (!checked) {
actions = actions.filter(a => a !== "lock")
}
SettingsData.set("powerMenuActions", actions)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Suspend")
checked: SettingsData.powerMenuActions.includes("suspend")
onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]
if (checked && !actions.includes("suspend")) {
actions.push("suspend")
} else if (!checked) {
actions = actions.filter(a => a !== "suspend")
}
SettingsData.set("powerMenuActions", actions)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Restart DMS")
description: I18n.tr("Restart the DankMaterialShell")
checked: SettingsData.powerMenuActions.includes("restart")
onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]
if (checked && !actions.includes("restart")) {
actions.push("restart")
} else if (!checked) {
actions = actions.filter(a => a !== "restart")
}
SettingsData.set("powerMenuActions", actions)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Hibernate")
description: I18n.tr("Only visible if hibernate is supported by your system")
checked: SettingsData.powerMenuActions.includes("hibernate")
visible: SessionService.hibernateSupported
onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]
if (checked && !actions.includes("hibernate")) {
actions.push("hibernate")
} else if (!checked) {
actions = actions.filter(a => a !== "hibernate")
}
SettingsData.set("powerMenuActions", actions)
}
}
}
}
}
StyledRect {
width: parent.width
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: powerCommandConfirmSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "check_circle"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Power Action Confirmation")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Confirmation on Power Actions")
description: I18n.tr("Request confirmation on power off, restart, suspend, hibernate and logout actions")
checked: SettingsData.powerActionConfirm
onToggled: checked => SettingsData.set("powerActionConfirm", checked)
}
}
}
StyledRect {
width: parent.width
height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: powerCommandCustomization
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "developer_mode"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Custom Power Actions")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard lock procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customLockCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myLock.sh"
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionLock) {
text = SettingsData.customPowerActionLock
}
}
onTextEdited: {
SettingsData.set("customPowerActionLock", text.trim())
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard logout procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customLogoutCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myLogout.sh"
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionLogout) {
text = SettingsData.customPowerActionLogout
}
}
onTextEdited: {
SettingsData.set("customPowerActionLogout", text.trim())
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard suspend procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customSuspendCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/mySuspend.sh"
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionSuspend) {
text = SettingsData.customPowerActionSuspend
}
}
onTextEdited: {
SettingsData.set("customPowerActionSuspend", text.trim())
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard hibernate procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customHibernateCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myHibernate.sh"
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionHibernate) {
text = SettingsData.customPowerActionHibernate
}
}
onTextEdited: {
SettingsData.set("customPowerActionHibernate", text.trim())
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard reboot procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customRebootCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myReboot.sh"
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionReboot) {
text = SettingsData.customPowerActionReboot
}
}
onTextEdited: {
SettingsData.set("customPowerActionReboot", text.trim())
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
anchors.left: parent.left
StyledText {
text: I18n.tr("Command or script to run instead of the standard power off procedure")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: customPowerOffCommand
width: parent.width
height: 48
placeholderText: "/usr/bin/myPowerOff.sh"
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.customPowerActionPowerOff) {
text = SettingsData.customPowerActionPowerOff
}
}
onTextEdited: {
SettingsData.set("customPowerActionPowerOff", text.trim())
}
}
}
}
}
}
}
}

View File

@@ -1,241 +0,0 @@
import QtQuick
import qs.Common
import qs.Modules.Settings
FocusScope {
id: root
property int currentIndex: 0
property var parentModal: null
focus: true
Rectangle {
anchors.fill: parent
anchors.leftMargin: 0
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingM
anchors.topMargin: 0
color: "transparent"
Loader {
id: personalizationLoader
anchors.fill: parent
active: root.currentIndex === 0
visible: active
focus: active
sourceComponent: Component {
PersonalizationTab {
parentModal: root.parentModal
}
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: timeWeatherLoader
anchors.fill: parent
active: root.currentIndex === 1
visible: active
focus: active
sourceComponent: TimeWeatherTab {
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: topBarLoader
anchors.fill: parent
active: root.currentIndex === 2
visible: active
focus: active
sourceComponent: DankBarTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: widgetsLoader
anchors.fill: parent
active: root.currentIndex === 3
visible: active
focus: active
sourceComponent: WidgetTweaksTab {
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: dockLoader
anchors.fill: parent
active: root.currentIndex === 4
visible: active
focus: active
sourceComponent: Component {
DockTab {
}
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: displaysLoader
anchors.fill: parent
active: root.currentIndex === 5
visible: active
focus: active
sourceComponent: DisplaysTab {
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: launcherLoader
anchors.fill: parent
active: root.currentIndex === 6
visible: active
focus: active
sourceComponent: LauncherTab {
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: themeColorsLoader
anchors.fill: parent
active: root.currentIndex === 7
visible: active
focus: active
sourceComponent: ThemeColorsTab {
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: powerLoader
anchors.fill: parent
active: root.currentIndex === 8
visible: active
focus: active
sourceComponent: PowerSettings {
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: pluginsLoader
anchors.fill: parent
active: root.currentIndex === 9
visible: active
focus: active
sourceComponent: PluginsTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
Loader {
id: aboutLoader
anchors.fill: parent
active: root.currentIndex === 10
visible: active
focus: active
sourceComponent: AboutTab {
}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus())
}
}
}
}
}

View File

@@ -1,260 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Modules.Settings
import qs.Services
import qs.Widgets
DankModal {
id: settingsModal
layerNamespace: "dms:settings"
property Component settingsContent
property alias profileBrowser: profileBrowser
property int currentTabIndex: 0
signal closingModal()
function show() {
open();
}
function hide() {
close();
}
function toggle() {
if (shouldBeVisible) {
hide();
} else {
show();
}
}
objectName: "settingsModal"
width: Math.min(800, screenWidth * 0.9)
height: Math.min(800, screenHeight * 0.85)
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onBackgroundClicked: () => {
return hide();
}
content: settingsContent
onOpened: () => {
Qt.callLater(() => {
modalFocusScope.forceActiveFocus()
if (contentLoader.item) {
contentLoader.item.forceActiveFocus()
}
})
}
onVisibleChanged: {
if (visible && shouldBeVisible) {
Qt.callLater(() => {
modalFocusScope.forceActiveFocus()
if (contentLoader.item) {
contentLoader.item.forceActiveFocus()
}
})
}
}
modalFocusScope.Keys.onPressed: event => {
const tabCount = 11
if (event.key === Qt.Key_Down) {
currentTabIndex = (currentTabIndex + 1) % tabCount
event.accepted = true
} else if (event.key === Qt.Key_Up) {
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
event.accepted = true
} else if (event.key === Qt.Key_Tab && !event.modifiers) {
currentTabIndex = (currentTabIndex + 1) % tabCount
event.accepted = true
} else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
event.accepted = true
}
}
IpcHandler {
function open(): string {
settingsModal.show();
return "SETTINGS_OPEN_SUCCESS";
}
function close(): string {
settingsModal.hide();
return "SETTINGS_CLOSE_SUCCESS";
}
function toggle(): string {
settingsModal.toggle();
return "SETTINGS_TOGGLE_SUCCESS";
}
target: "settings"
}
IpcHandler {
function browse(type: string) {
if (type === "wallpaper") {
wallpaperBrowser.allowStacking = false;
wallpaperBrowser.open();
} else if (type === "profile") {
profileBrowser.allowStacking = false;
profileBrowser.open();
}
}
target: "file"
}
FileBrowserModal {
id: profileBrowser
allowStacking: true
parentModal: settingsModal
browserTitle: "Select Profile Image"
browserIcon: "person"
browserType: "profile"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => {
PortalService.setProfileImage(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
if (settingsModal.shouldBeVisible) {
Qt.callLater(() => {
settingsModal.modalFocusScope.forceActiveFocus()
if (settingsModal.contentLoader.item) {
settingsModal.contentLoader.item.forceActiveFocus()
}
})
}
}
}
FileBrowserModal {
id: wallpaperBrowser
allowStacking: true
parentModal: settingsModal
browserTitle: "Select Wallpaper"
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => {
SessionData.setWallpaper(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
if (settingsModal.shouldBeVisible) {
Qt.callLater(() => {
settingsModal.modalFocusScope.forceActiveFocus()
if (settingsModal.contentLoader.item) {
settingsModal.contentLoader.item.forceActiveFocus()
}
})
}
}
}
settingsContent: Component {
Item {
id: rootScope
anchors.fill: parent
Keys.onEscapePressed: event => {
settingsModal.hide()
event.accepted = true
}
Column {
anchors.fill: parent
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingL
spacing: 0
Item {
width: parent.width
height: 35
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "settings"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Settings")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankActionButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
return settingsModal.hide();
}
}
}
Row {
width: parent.width
height: parent.height - 35
spacing: 0
SettingsSidebar {
id: sidebar
parentModal: settingsModal
currentIndex: settingsModal.currentTabIndex
onCurrentIndexChanged: {
settingsModal.currentTabIndex = currentIndex
}
}
SettingsContent {
id: content
width: parent.width - sidebar.width
height: parent.height
parentModal: settingsModal
currentIndex: settingsModal.currentTabIndex
}
}
}
}
}
}

View File

@@ -1,158 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modals.Settings
import qs.Widgets
Rectangle {
id: sidebarContainer
property int currentIndex: 0
property var parentModal: null
readonly property var sidebarItems: [{
"text": I18n.tr("Personalization"),
"icon": "person"
}, {
"text": I18n.tr("Time & Weather"),
"icon": "schedule"
}, {
"text": I18n.tr("Dank Bar"),
"icon": "toolbar"
}, {
"text": I18n.tr("Widgets"),
"icon": "widgets"
}, {
"text": I18n.tr("Dock"),
"icon": "dock_to_bottom"
}, {
"text": I18n.tr("Displays"),
"icon": "monitor"
}, {
"text": I18n.tr("Launcher"),
"icon": "apps"
}, {
"text": I18n.tr("Theme & Colors"),
"icon": "palette"
}, {
"text": I18n.tr("Power & Security"),
"icon": "power"
}, {
"text": I18n.tr("Plugins"),
"icon": "extension"
}, {
"text": I18n.tr("About"),
"icon": "info"
}]
function navigateNext() {
currentIndex = (currentIndex + 1) % sidebarItems.length
}
function navigatePrevious() {
currentIndex = (currentIndex - 1 + sidebarItems.length) % sidebarItems.length
}
width: 270
height: parent.height
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: sidebarColumn.implicitHeight
Column {
id: sidebarColumn
width: parent.width
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
anchors.topMargin: Theme.spacingM + 2
spacing: Theme.spacingXS
ProfileSection {
parentModal: sidebarContainer.parentModal
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 1
color: Theme.outline
opacity: 0.2
}
Item {
width: parent.width
height: Theme.spacingL
}
Repeater {
id: sidebarRepeater
model: sidebarContainer.sidebarItems
delegate: Rectangle {
required property int index
required property var modelData
property bool isActive: sidebarContainer.currentIndex === index
width: sidebarColumn.width - Theme.spacingS * 2
height: 44
radius: Theme.cornerRadius
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: modelData.icon || ""
size: Theme.iconSize - 2
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.text || ""
font.pixelSize: Theme.fontSizeMedium
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
sidebarContainer.currentIndex = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}

View File

@@ -1,447 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modals.Spotlight
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
Item {
id: spotlightKeyHandler
property alias appLauncher: appLauncher
property alias searchField: searchField
property alias fileSearchController: fileSearchController
property var parentModal: null
property string searchMode: "apps"
function resetScroll() {
if (searchMode === "apps") {
resultsView.resetScroll()
} else {
fileSearchResults.resetScroll()
}
}
function updateSearchMode() {
if (searchField.text.startsWith("/")) {
if (searchMode !== "files") {
searchMode = "files"
}
const query = searchField.text.substring(1)
fileSearchController.searchQuery = query
} else {
if (searchMode !== "apps") {
searchMode = "apps"
fileSearchController.reset()
appLauncher.searchQuery = searchField.text
}
}
}
onSearchModeChanged: {
if (searchMode === "files") {
appLauncher.keyboardNavigationActive = false
} else {
fileSearchController.keyboardNavigationActive = false
}
}
anchors.fill: parent
focus: true
clip: false
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
if (parentModal)
parentModal.hide()
event.accepted = true
} else if (event.key === Qt.Key_Down) {
if (searchMode === "apps") {
appLauncher.selectNext()
} else {
fileSearchController.selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_Up) {
if (searchMode === "apps") {
appLauncher.selectPrevious()
} else {
fileSearchController.selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
event.accepted = true
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
event.accepted = true
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
appLauncher.selectNext()
} else {
fileSearchController.selectNext()
}
event.accepted = true
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
appLauncher.selectPrevious()
} else {
fileSearchController.selectPrevious()
}
event.accepted = true
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
event.accepted = true
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
event.accepted = true
} else if (event.key === Qt.Key_Tab) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
} else {
appLauncher.selectNext()
}
} else {
fileSearchController.selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_Backtab) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
} else {
appLauncher.selectPrevious()
}
} else {
fileSearchController.selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
} else {
appLauncher.selectNext()
}
} else {
fileSearchController.selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
} else {
appLauncher.selectPrevious()
}
} else {
fileSearchController.selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (searchMode === "apps") {
appLauncher.launchSelected()
} else if (searchMode === "files") {
fileSearchController.openSelected()
}
event.accepted = true
}
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.spotlightModalViewMode
gridColumns: 4
onAppLaunched: () => {
if (parentModal)
parentModal.hide()
}
onViewModeSelected: mode => {
SettingsData.set("spotlightModalViewMode", mode)
}
}
FileSearchController {
id: fileSearchController
onFileOpened: () => {
if (parentModal)
parentModal.hide()
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
clip: false
Row {
width: parent.width
spacing: Theme.spacingM
leftPadding: Theme.spacingS
topPadding: Theme.spacingS
DankTextField {
id: searchField
width: parent.width - 80 - Theme.spacingL
height: 56
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: searchMode === "files" ? "folder" : "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: parentModal ? parentModal.spotlightOpen : true
placeholderText: ""
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [spotlightKeyHandler]
onTextChanged: {
if (searchMode === "apps") {
appLauncher.searchQuery = text
}
}
onTextEdited: {
updateSearchMode()
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
if (parentModal)
parentModal.hide()
event.accepted = true
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (searchMode === "apps") {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected()
else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0))
} else if (searchMode === "files") {
if (fileSearchController.model.count > 0)
fileSearchController.openSelected()
}
event.accepted = true
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key
=== Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false
}
}
}
Row {
spacing: Theme.spacingXS
visible: searchMode === "apps" && appLauncher.model.count > 0
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "view_list"
size: 18
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: listViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
appLauncher.setViewMode("list")
}
}
}
Rectangle {
width: 36
height: 36
radius: Theme.cornerRadius
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "grid_view"
size: 18
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: gridViewArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
appLauncher.setViewMode("grid")
}
}
}
}
Row {
spacing: Theme.spacingXS
visible: searchMode === "files"
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: filenameFilterButton
width: 36
height: 36
radius: Theme.cornerRadius
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "title"
size: 18
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: filenameFilterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
fileSearchController.searchField = "filename"
}
onEntered: {
filenameTooltipLoader.active = true
Qt.callLater(() => {
if (filenameTooltipLoader.item) {
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null)
}
})
}
onExited: {
if (filenameTooltipLoader.item)
filenameTooltipLoader.item.hide()
filenameTooltipLoader.active = false
}
}
}
Rectangle {
id: contentFilterButton
width: 36
height: 36
radius: Theme.cornerRadius
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "description"
size: 18
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: contentFilterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
fileSearchController.searchField = "body"
}
onEntered: {
contentTooltipLoader.active = true
Qt.callLater(() => {
if (contentTooltipLoader.item) {
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null)
}
})
}
onExited: {
if (contentTooltipLoader.item)
contentTooltipLoader.item.hide()
contentTooltipLoader.active = false
}
}
}
}
}
Item {
width: parent.width
height: parent.height - y
SpotlightResults {
id: resultsView
anchors.fill: parent
appLauncher: spotlightKeyHandler.appLauncher
contextMenu: contextMenu
visible: searchMode === "apps"
}
FileSearchResults {
id: fileSearchResults
anchors.fill: parent
fileSearchController: spotlightKeyHandler.fileSearchController
visible: searchMode === "files"
}
}
}
SpotlightContextMenu {
id: contextMenu
appLauncher: spotlightKeyHandler.appLauncher
parentHandler: spotlightKeyHandler
}
MouseArea {
anchors.fill: parent
visible: contextMenu.visible
z: 999
onClicked: () => {
contextMenu.hide()
}
MouseArea {
x: contextMenu.x
y: contextMenu.y
width: contextMenu.width
height: contextMenu.height
onClicked: () => {}
}
}
Loader {
id: filenameTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Loader {
id: contentTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

View File

@@ -1,338 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Popup {
id: contextMenu
property var currentApp: null
property var appLauncher: null
property var parentHandler: null
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
function show(x, y, app) {
currentApp = app
contextMenu.x = x + 4
contextMenu.y = y + 4
contextMenu.open()
}
function hide() {
contextMenu.close()
}
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
closePolicy: Popup.CloseOnPressOutside
modal: false
dim: false
background: Rectangle {
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: pinRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: {
if (!desktopEntry)
return "push_pin"
const appId = desktopEntry.id || desktopEntry.execString || ""
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin"
}
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!desktopEntry)
return I18n.tr("Pin to Dock")
const appId = desktopEntry.id || desktopEntry.execString || ""
return SessionData.isPinnedApp(appId) ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (!desktopEntry)
return
const appId = desktopEntry.id || desktopEntry.execString || ""
if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId)
else
SessionData.addPinnedApp(appId)
contextMenu.hide()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Repeater {
model: desktopEntry && desktopEntry.actions ? desktopEntry.actions : []
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: actionRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize - 2
height: Theme.iconSize - 2
visible: modelData.icon && modelData.icon !== ""
IconImage {
anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
StyledText {
text: modelData.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: actionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && desktopEntry) {
SessionService.launchDesktopAction(desktopEntry, modelData)
if (appLauncher && contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
contextMenu.hide()
}
}
}
}
Rectangle {
visible: desktopEntry && desktopEntry.actions && desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: launchRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (contextMenu.currentApp && appLauncher)
appLauncher.launchApp(contextMenu.currentApp)
contextMenu.hide()
}
}
}
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width
height: 32
radius: Theme.cornerRadius
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: primeRunRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch on dGPU")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: primeRunMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (desktopEntry) {
SessionService.launchDesktopEntry(desktopEntry, true)
if (appLauncher && contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
contextMenu.hide()
}
}
}
}
}

View File

@@ -1,158 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modals.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankModal {
id: spotlightModal
layerNamespace: "dms:spotlight"
property bool spotlightOpen: false
property alias spotlightContent: spotlightContentInstance
function show() {
spotlightOpen = true
open()
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
function showWithQuery(query) {
if (spotlightContent) {
if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = query
}
if (spotlightContent.searchField) {
spotlightContent.searchField.text = query
}
}
spotlightOpen = true
open()
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
function hide() {
spotlightOpen = false
close()
}
onDialogClosed: {
if (spotlightContent) {
if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = ""
spotlightContent.appLauncher.selectedIndex = 0
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
}
if (spotlightContent.fileSearchController) {
spotlightContent.fileSearchController.reset()
}
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll()
}
if (spotlightContent.searchField) {
spotlightContent.searchField.text = ""
}
}
}
function toggle() {
if (spotlightOpen) {
hide()
} else {
show()
}
}
shouldBeVisible: spotlightOpen
width: 500
height: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
onVisibleChanged: () => {
if (visible && !spotlightOpen) {
show()
}
if (visible && spotlightContent) {
Qt.callLater(() => {
if (spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
}
onBackgroundClicked: () => {
return hide()
}
Connections {
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
spotlightOpen = false
}
}
target: ModalManager
}
IpcHandler {
function open(): string {
spotlightModal.show()
return "SPOTLIGHT_OPEN_SUCCESS"
}
function close(): string {
spotlightModal.hide()
return "SPOTLIGHT_CLOSE_SUCCESS"
}
function toggle(): string {
spotlightModal.toggle()
return "SPOTLIGHT_TOGGLE_SUCCESS"
}
function openQuery(query: string): string {
spotlightModal.showWithQuery(query)
return "SPOTLIGHT_OPEN_QUERY_SUCCESS"
}
function toggleQuery(query: string): string {
if (spotlightModal.spotlightOpen) {
spotlightModal.hide()
} else {
spotlightModal.showWithQuery(query)
}
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS"
}
target: "spotlight"
}
SpotlightContent {
id: spotlightContentInstance
parentModal: spotlightModal
}
directContent: spotlightContentInstance
}

View File

@@ -1,179 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Widgets
Rectangle {
id: resultsContainer
property var appLauncher: null
property var contextMenu: null
function resetScroll() {
resultsList.contentY = 0
resultsGrid.contentY = 0
}
radius: Theme.cornerRadius
color: "transparent"
clip: true
DankListView {
id: resultsList
property int itemHeight: 60
property int iconSize: 40
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
const itemY = index * (itemHeight + itemSpacing)
const itemBottom = itemY + itemHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher && appLauncher.viewMode === "list"
model: appLauncher ? appLauncher.model : null
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: (index, modelData) => {
if (appLauncher)
appLauncher.launchApp(modelData)
}
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
if (contextMenu)
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: () => {
if (appLauncher)
appLauncher.keyboardNavigationActive = false
}
delegate: AppLauncherListDelegate {
listView: resultsList
itemHeight: resultsList.itemHeight
iconSize: resultsList.iconSize
showDescription: resultsList.showDescription
hoverUpdatesSelection: resultsList.hoverUpdatesSelection
keyboardNavigationActive: resultsList.keyboardNavigationActive
isCurrentItem: ListView.isCurrentItem
iconMaterialSizeAdjustment: 0
iconUnicodeScale: 0.8
onItemClicked: (idx, modelData) => resultsList.itemClicked(idx, modelData)
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
resultsList.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
}
onKeyboardNavigationReset: resultsList.keyboardNavigationReset
}
}
DankGridView {
id: resultsGrid
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
property int columns: 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property int cellPadding: 8
property real iconSizeRatio: 0.55
property int maxIconSize: 48
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
const itemY = Math.floor(index / actualColumns) * cellHeight
const itemBottom = itemY + cellHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: appLauncher && appLauncher.viewMode === "grid"
model: appLauncher ? appLauncher.model : null
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
rightMargin: leftMargin
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: (index, modelData) => {
if (appLauncher)
appLauncher.launchApp(modelData)
}
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
if (contextMenu)
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: () => {
if (appLauncher)
appLauncher.keyboardNavigationActive = false
}
delegate: AppLauncherGridDelegate {
gridView: resultsGrid
cellWidth: resultsGrid.cellWidth
cellHeight: resultsGrid.cellHeight
cellPadding: resultsGrid.cellPadding
minIconSize: resultsGrid.minIconSize
maxIconSize: resultsGrid.maxIconSize
iconSizeRatio: resultsGrid.iconSizeRatio
hoverUpdatesSelection: resultsGrid.hoverUpdatesSelection
keyboardNavigationActive: resultsGrid.keyboardNavigationActive
currentIndex: resultsGrid.currentIndex
onItemClicked: (idx, modelData) => resultsGrid.itemClicked(idx, modelData)
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
resultsGrid.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
}
onKeyboardNavigationReset: resultsGrid.keyboardNavigationReset
}
}
}

View File

@@ -1,594 +0,0 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
layerNamespace: "dms:wifi-password"
property string wifiPasswordSSID: ""
property string wifiPasswordInput: ""
property string wifiUsernameInput: ""
property bool requiresEnterprise: false
property string wifiAnonymousIdentityInput: ""
property string wifiDomainInput: ""
property bool isPromptMode: false
property string promptToken: ""
property string promptReason: ""
property var promptFields: []
property string promptSetting: ""
property bool isVpnPrompt: false
property string connectionName: ""
property string vpnServiceType: ""
property string connectionType: ""
function show(ssid) {
wifiPasswordSSID = ssid
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
isPromptMode = false
promptToken = ""
promptReason = ""
promptFields = []
promptSetting = ""
isVpnPrompt = false
connectionName = ""
vpnServiceType = ""
connectionType = ""
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
requiresEnterprise = network?.enterprise || false
open()
Qt.callLater(() => {
if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
})
}
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
isPromptMode = true
promptToken = token
promptReason = reason
promptFields = fields || []
promptSetting = setting || "802-11-wireless-security"
connectionType = connType || "802-11-wireless"
connectionName = connName || ssid || ""
vpnServiceType = vpnService || ""
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard")
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid
requiresEnterprise = setting === "802-1x"
if (reason === "wrong-password") {
wifiPasswordInput = ""
wifiUsernameInput = ""
} else {
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
open()
Qt.callLater(() => {
if (contentLoader.item) {
if (reason === "wrong-password" && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.text = ""
contentLoader.item.passwordInput.forceActiveFocus()
} else if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
})
}
shouldBeVisible: false
width: 420
height: requiresEnterprise ? 430 : 230
onShouldBeVisibleChanged: () => {
if (!shouldBeVisible) {
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
}
onOpened: {
Qt.callLater(() => {
if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
})
}
onBackgroundClicked: () => {
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
Connections {
target: NetworkService
function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID
wifiPasswordInput = ""
open()
NetworkService.passwordDialogShouldReopen = false
}
}
}
content: Component {
FocusScope {
id: wifiContent
property alias usernameInput: usernameInput
property alias passwordInput: passwordInput
anchors.fill: parent
focus: true
Keys.onEscapePressed: event => {
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
event.accepted = true
}
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: {
if (isVpnPrompt) {
return I18n.tr("Connect to VPN")
}
return I18n.tr("Connect to Wi-Fi")
}
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: {
if (isVpnPrompt) {
return I18n.tr("Enter password for ") + wifiPasswordSSID
}
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ")
return prefix + wifiPasswordSSID
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
StyledText {
visible: isPromptMode && promptReason === "wrong-password"
text: I18n.tr("Incorrect password")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
width: parent.width
}
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: usernameInput.activeFocus ? 2 : 1
visible: requiresEnterprise && !isVpnPrompt
MouseArea {
anchors.fill: parent
onClicked: () => {
usernameInput.forceActiveFocus()
}
}
DankTextField {
id: usernameInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiUsernameInput
placeholderText: I18n.tr("Username")
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiUsernameInput = text
}
onAccepted: () => {
if (passwordInput) {
passwordInput.forceActiveFocus()
}
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: () => {
passwordInput.forceActiveFocus()
}
}
DankTextField {
id: passwordInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
backgroundColor: "transparent"
focus: !requiresEnterprise
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiPasswordInput = text
}
onAccepted: () => {
if (isPromptMode) {
const secrets = {}
if (isVpnPrompt) {
if (passwordInput.text) secrets["password"] = passwordInput.text
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text
} else if (promptSetting === "802-1x") {
if (usernameInput.text) secrets["identity"] = usernameInput.text
if (passwordInput.text) secrets["password"] = passwordInput.text
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
}
NetworkService.submitCredentials(promptToken, secrets, true)
} else {
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(
wifiPasswordSSID,
passwordInput.text,
username,
wifiAnonymousIdentityInput,
wifiDomainInput
)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}
Component.onCompleted: () => {
if (root.shouldBeVisible && !requiresEnterprise)
focusDelayTimer.start()
}
Timer {
id: focusDelayTimer
interval: 100
repeat: false
onTriggered: () => {
if (root.shouldBeVisible) {
if (requiresEnterprise && usernameInput) {
usernameInput.forceActiveFocus()
} else {
passwordInput.forceActiveFocus()
}
}
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible)
focusDelayTimer.start()
}
}
}
}
Rectangle {
visible: requiresEnterprise && !isVpnPrompt
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: anonInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: () => {
anonInput.forceActiveFocus()
}
}
DankTextField {
id: anonInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiAnonymousIdentityInput
placeholderText: I18n.tr("Anonymous Identity (optional)")
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiAnonymousIdentityInput = text
}
}
}
Rectangle {
visible: requiresEnterprise && !isVpnPrompt
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: domainMatchInput.activeFocus ? 2 : 1
MouseArea {
anchors.fill: parent
onClicked: () => {
domainMatchInput.forceActiveFocus()
}
}
DankTextField {
id: domainMatchInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiDomainInput
placeholderText: I18n.tr("Domain (optional)")
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiDomainInput = text
}
}
}
Row {
spacing: Theme.spacingS
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
}
}
StyledText {
text: I18n.tr("Show password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: cancelText
anchors.centerIn: parent
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
}
}
Rectangle {
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: {
if (isVpnPrompt) {
return passwordInput.text.length > 0
}
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0
}
opacity: enabled ? 1 : 0.5
StyledText {
id: connectText
anchors.centerIn: parent
text: I18n.tr("Connect")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: () => {
if (isPromptMode) {
const secrets = {}
if (isVpnPrompt) {
if (passwordInput.text) secrets["password"] = passwordInput.text
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text
} else if (promptSetting === "802-1x") {
if (usernameInput.text) secrets["identity"] = usernameInput.text
if (passwordInput.text) secrets["password"] = passwordInput.text
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
}
NetworkService.submitCredentials(promptToken, secrets, true)
} else {
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(
wifiPasswordSSID,
passwordInput.text,
username,
wifiAnonymousIdentityInput,
wifiDomainInput
)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}

View File

@@ -1,251 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
Ref {
service: DMSNetworkService
}
ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
ccWidgetPrimaryText: "VPN"
ccWidgetSecondaryText: {
if (!DMSNetworkService.connected)
return "Disconnected"
const names = DMSNetworkService.activeNames || []
if (names.length <= 1)
return names[0] || "Connected"
return names[0] + " +" + (names.length - 1)
}
ccWidgetIsActive: DMSNetworkService.connected
onCcWidgetToggled: {
DMSNetworkService.toggleVpn()
}
ccDetailContent: Component {
Rectangle {
id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
id: detailColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
spacing: Theme.spacingS
width: parent.width
StyledText {
text: {
if (!DMSNetworkService.connected)
return "Active: None"
const names = DMSNetworkService.activeNames || []
if (names.length <= 1)
return "Active: " + (names[0] || "VPN")
return "Active: " + names[0] + " +" + (names.length - 1)
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
Layout.fillWidth: true
Layout.maximumWidth: parent.width - 120
}
Rectangle {
height: 28
radius: 14
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: DMSNetworkService.connected
width: 110
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "link_off"
size: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Disconnect")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: discAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.disconnectAllActive()
}
}
}
Rectangle {
height: 1
width: parent.width
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankFlickable {
width: parent.width
height: 160
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: Theme.spacingXS
Item {
width: parent.width
height: DMSNetworkService.profiles.length === 0 ? 120 : 0
visible: height > 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "playlist_remove"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No VPN profiles found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Add a VPN in NetworkManager")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Repeater {
model: DMSNetworkService.profiles
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 300
height: 50
radius: Theme.cornerRadius
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
size: Theme.iconSize - 4
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Column {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: {
if (modelData.type === "wireguard")
return "WireGuard"
const svc = modelData.serviceType || ""
if (svc.indexOf("openvpn") !== -1)
return "OpenVPN"
if (svc.indexOf("wireguard") !== -1)
return "WireGuard (plugin)"
if (svc.indexOf("openconnect") !== -1)
return "OpenConnect"
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1)
return "Fortinet"
if (svc.indexOf("strongswan") !== -1)
return "IPsec (strongSwan)"
if (svc.indexOf("libreswan") !== -1)
return "IPsec (Libreswan)"
if (svc.indexOf("l2tp") !== -1)
return "L2TP/IPsec"
if (svc.indexOf("pptp") !== -1)
return "PPTP"
if (svc.indexOf("vpnc") !== -1)
return "Cisco (vpnc)"
if (svc.indexOf("sstp") !== -1)
return "SSTP"
if (svc)
return svc.split('.').pop()
return "VPN"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Item {
Layout.fillWidth: true
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(modelData.uuid)
}
}
}
}
}
}
}
}
}

View File

@@ -1,210 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
property bool hasVolumeSliderInCC: {
const widgets = SettingsData.controlCenterWidgets || []
return widgets.some(widget => widget.id === "volumeSlider")
}
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Audio Devices")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: volumeSlider
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerRow.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
height: 35
spacing: 0
visible: !hasVolumeSliderInCC
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!AudioService.sink || !AudioService.sink.audio) return "volume_off"
let muted = AudioService.sink.audio.muted
let volume = AudioService.sink.audio.volume
if (muted || volume === 0.0) return "volume_off"
if (volume <= 0.33) return "volume_down"
if (volume <= 0.66) return "volume_up"
return "volume_up"
}
size: Theme.iconSize
color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: AudioService.sink && AudioService.sink.audio
minimum: 0
maximum: 100
value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant
onSliderValueChanged: function(newValue) {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.volume = newValue / 100
if (newValue > 0 && AudioService.sink.audio.muted) {
AudioService.sink.audio.muted = false
}
AudioService.volumeChanged()
}
}
}
}
DankFlickable {
id: audioContent
anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
contentHeight: audioColumn.height
clip: true
Column {
id: audioColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream
})
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("hdmi"))
return "tv"
else if (modelData.name.includes("usb"))
return "headset"
else
return "speaker"
}
size: Theme.iconSize - 4
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData
}
}
}
}
}
}
}
}

View File

@@ -1,316 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Widgets
PanelWindow {
id: root
readonly property string powerOptionsText: I18n.tr("Power Options")
readonly property string logOutText: I18n.tr("Log Out")
readonly property string suspendText: I18n.tr("Suspend")
readonly property string rebootText: I18n.tr("Reboot")
readonly property string powerOffText: I18n.tr("Power Off")
property bool powerMenuVisible: false
signal powerActionRequested(string action, string title, string message)
visible: powerMenuVisible
implicitWidth: 400
implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: {
powerMenuVisible = false
}
}
Rectangle {
width: Math.min(320, parent.width - Theme.spacingL * 2)
height: 320 // Fixed height to prevent cropping
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
y: Theme.barHeight + Theme.spacingXS
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 0
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
MouseArea {
anchors.fill: parent
onClicked: {
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: root.powerOptionsText
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: {
powerMenuVisible = false
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.logOutText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"logout", "Log Out",
"Are you sure you want to log out?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.suspendText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"suspend", "Suspend",
"Are you sure you want to suspend the system?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
Theme.warning.g,
Theme.warning.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.rebootText
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"reboot", "Reboot",
"Are you sure you want to reboot the system?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.powerOffText
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"poweroff", "Power Off",
"Are you sure you want to power off the system?")
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}

View File

@@ -1,25 +0,0 @@
function setTriggerPosition(root, x, y, width, section, screen) {
root.triggerX = x
root.triggerY = y
root.triggerWidth = width
root.triggerSection = section
root.triggerScreen = screen
}
function openWithSection(root, section) {
if (root.shouldBeVisible) {
root.close()
} else {
root.expandedSection = section
root.open()
}
}
function toggleSection(root, section) {
if (root.expandedSection === section) {
root.expandedSection = ""
root.expandedWidgetIndex = -1
} else {
root.expandedSection = section
}
}

View File

@@ -1,179 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
Item {
id: root
required property var barWindow
required property var axis
required property var appDrawerLoader
required property var dankDashPopoutLoader
required property var processListPopoutLoader
required property var notificationCenterLoader
required property var batteryPopoutLoader
required property var vpnPopoutLoader
required property var controlCenterLoader
required property var clipboardHistoryModalPopup
required property var systemUpdateLoader
required property var notepadInstance
property alias reveal: core.reveal
property alias autoHide: core.autoHide
property alias backgroundTransparency: core.backgroundTransparency
property alias hasActivePopout: core.hasActivePopout
property alias mouseArea: topBarMouseArea
Item {
id: inputMask
readonly property int barThickness: barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing)
readonly property bool showing: SettingsData.dankBarVisible && (core.reveal
|| (CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview)
|| !core.autoHide)
readonly property int maskThickness: showing ? barThickness : 1
x: {
if (!axis.isVertical) {
return 0
} else {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Left: return 0
case SettingsData.Position.Right: return parent.width - maskThickness
default: return 0
}
}
}
y: {
if (axis.isVertical) {
return 0
} else {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Top: return 0
case SettingsData.Position.Bottom: return parent.height - maskThickness
default: return 0
}
}
}
width: axis.isVertical ? maskThickness : parent.width
height: axis.isVertical ? parent.height : maskThickness
}
Region {
id: mask
item: inputMask
}
property alias maskRegion: mask
QtObject {
id: core
property real backgroundTransparency: SettingsData.dankBarTransparency
property bool autoHide: SettingsData.dankBarAutoHide
property bool revealSticky: false
property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false
readonly property bool hasActivePopout: {
const loaders = [{
"loader": appDrawerLoader,
"prop": "shouldBeVisible"
}, {
"loader": dankDashPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": processListPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": notificationCenterLoader,
"prop": "shouldBeVisible"
}, {
"loader": batteryPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": vpnPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": controlCenterLoader,
"prop": "shouldBeVisible"
}, {
"loader": clipboardHistoryModalPopup,
"prop": "visible"
}, {
"loader": systemUpdateLoader,
"prop": "shouldBeVisible"
}]
return notepadInstanceVisible || loaders.some(item => {
if (item.loader) {
return item.loader?.item?.[item.prop]
}
return false
})
}
property bool reveal: {
if (CompositorService.isNiri && NiriService.inOverview) {
return SettingsData.dankBarOpenOnOverview
}
return SettingsData.dankBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky)
}
onHasActivePopoutChanged: {
if (!hasActivePopout && autoHide && !topBarMouseArea.containsMouse) {
revealSticky = true
revealHold.restart()
}
}
}
Timer {
id: revealHold
interval: 250
repeat: false
onTriggered: core.revealSticky = false
}
Connections {
function onDankBarTransparencyChanged() {
core.backgroundTransparency = SettingsData.dankBarTransparency
}
target: SettingsData
}
Connections {
target: topBarMouseArea
function onContainsMouseChanged() {
if (topBarMouseArea.containsMouse) {
core.revealSticky = true
revealHold.stop()
} else {
if (core.autoHide && !core.hasActivePopout) {
revealHold.restart()
}
}
}
}
MouseArea {
id: topBarMouseArea
y: !barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? parent.height - height : 0) : 0
x: barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.width - width : 0) : 0
height: !barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
width: barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
anchors {
left: !barWindow.isVertical ? parent.left : (SettingsData.dankBarPosition === SettingsData.Position.Left ? parent.left : undefined)
right: !barWindow.isVertical ? parent.right : (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.right : undefined)
top: barWindow.isVertical ? parent.top : undefined
bottom: barWindow.isVertical ? parent.bottom : undefined
}
hoverEnabled: SettingsData.dankBarAutoHide && !core.reveal
acceptedButtons: Qt.NoButton
enabled: SettingsData.dankBarAutoHide && !core.reveal
}
}

View File

@@ -1,410 +0,0 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Services
Item {
id: root
required property var barWindow
required property var axis
anchors.fill: parent
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
anchors.rightMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
readonly property real dpr: CompositorService.getScreenScale(barWindow.screen)
function requestRepaint() {
debounceTimer.restart()
}
Timer {
id: debounceTimer
interval: 50
repeat: false
onTriggered: {
barShape.requestPaint()
barTint.requestPaint()
barBorder.requestPaint()
}
}
Canvas {
id: barShape
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: barWindow
function on_BgColorChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceContainerChanged() { root.requestRepaint() }
}
Connections {
target: SettingsData
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
}
onPaint: {
const ctx = getContext("2d")
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
if (R > 0) {
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
} else {
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
}
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
drawTopPath()
ctx.restore()
ctx.fillStyle = barWindow._bgColor
ctx.fill()
}
}
Canvas {
id: barTint
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onAlphaTintChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: barWindow
function on_BgColorChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceChanged() { root.requestRepaint() }
}
Connections {
target: SettingsData
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
}
onPaint: {
const ctx = getContext("2d")
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
if (R > 0) {
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
} else {
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
}
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
drawTopPath()
ctx.restore()
ctx.fillStyle = Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, alphaTint)
ctx.fill()
}
}
Canvas {
id: barBorder
anchors.fill: parent
visible: SettingsData.dankBarBorderEnabled
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
property bool borderEnabled: SettingsData.dankBarBorderEnabled
antialiasing: rt > 0 || wing > 0
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onBorderEnabledChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceTextChanged() { root.requestRepaint() }
function onPrimaryChanged() { root.requestRepaint() }
function onSecondaryChanged() { root.requestRepaint() }
function onOutlineChanged() { root.requestRepaint() }
}
Connections {
target: SettingsData
function onDankBarBorderColorChanged() { root.requestRepaint() }
function onDankBarBorderOpacityChanged() { root.requestRepaint() }
function onDankBarBorderThicknessChanged() { root.requestRepaint() }
function onDankBarSpacingChanged() { root.requestRepaint() }
function onDankBarSquareCornersChanged() { root.requestRepaint() }
function onDankBarTransparencyChanged() { root.requestRepaint() }
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
}
onPaint: {
if (!borderEnabled) return
const ctx = getContext("2d")
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
const spacing = SettingsData.dankBarSpacing
const hasEdgeGap = spacing > 0 || RT > 0
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
const uiThickness = Math.max(1, SettingsData.dankBarBorderThickness ?? 1)
const devThickness = Math.max(1, Math.round(Theme.px(uiThickness, dpr)))
const key = SettingsData.dankBarBorderColor || "surfaceText"
const base = (key === "surfaceText") ? Theme.surfaceText
: (key === "primary") ? Theme.primary
: Theme.secondary
const color = Theme.withAlpha(base, SettingsData.dankBarBorderOpacity ?? 1.0)
ctx.globalCompositeOperation = "source-over"
ctx.fillStyle = color
function drawTopBorder() {
if (!hasEdgeGap) {
ctx.beginPath()
ctx.rect(0, H - devThickness, W, devThickness)
ctx.fill()
} else {
const thk = devThickness
const RTi = Math.max(0, RT - thk)
const Ri = Math.max(0, R - thk)
ctx.beginPath()
if (R > 0 && Ri > 0) {
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
ctx.moveTo(RT, thk)
ctx.arcTo(thk, thk, thk, RT, RTi)
ctx.lineTo(thk, H + R)
ctx.arc(R, H + R, Ri, -Math.PI, -Math.PI / 2, false)
ctx.lineTo(W - R, H + thk)
ctx.arc(W - R, H + R, Ri, -Math.PI / 2, 0, false)
ctx.lineTo(W - thk, H + R)
ctx.lineTo(W - thk, RT)
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
ctx.lineTo(RT, thk)
ctx.closePath()
} else {
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
ctx.moveTo(RT, thk)
ctx.arcTo(thk, thk, thk, RT, RTi)
ctx.lineTo(thk, H - RT)
ctx.arcTo(thk, H - thk, RT, H - thk, RTi)
ctx.lineTo(W - RT, H - thk)
ctx.arcTo(W - thk, H - thk, W - thk, H - RT, RTi)
ctx.lineTo(W - thk, RT)
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
ctx.lineTo(RT, thk)
ctx.closePath()
}
ctx.fill("evenodd")
}
}
drawTopBorder()
ctx.restore()
}
}
}

View File

@@ -1,499 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
property var widgetsModel: null
property var components: null
property bool noBackground: false
required property var axis
property string section: "center"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property bool overrideAxisLayout: false
property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
property var centerWidgets: []
property int totalWidgets: 0
property real totalSize: 0
function updateLayout() {
if ((isVertical ? height : width) <= 0 || !visible) {
return
}
centerWidgets = []
totalWidgets = 0
totalSize = 0
let configuredWidgets = 0
let configuredMiddleWidget = null
let configuredLeftWidget = null
let configuredRightWidget = null
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i)
if (item && getWidgetVisible(item.widgetId)) {
configuredWidgets++
}
}
const isOddConfigured = configuredWidgets % 2 === 1
const configuredMiddlePos = Math.floor(configuredWidgets / 2)
const configuredLeftPos = isOddConfigured ? -1 : ((configuredWidgets / 2) - 1)
const configuredRightPos = isOddConfigured ? -1 : (configuredWidgets / 2)
let currentConfigIndex = 0
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i)
if (item && getWidgetVisible(item.widgetId)) {
if (isOddConfigured && currentConfigIndex === configuredMiddlePos && item.active && item.item) {
configuredMiddleWidget = item.item
}
if (!isOddConfigured && currentConfigIndex === configuredLeftPos && item.active && item.item) {
configuredLeftWidget = item.item
}
if (!isOddConfigured && currentConfigIndex === configuredRightPos && item.active && item.item) {
configuredRightWidget = item.item
}
if (item.active && item.item) {
centerWidgets.push(item.item)
totalWidgets++
totalSize += isVertical ? item.item.height : item.item.width
}
currentConfigIndex++
}
}
if (totalWidgets === 0) {
return
}
if (totalWidgets > 1) {
totalSize += spacing * (totalWidgets - 1)
}
positionWidgets(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget)
}
function positionWidgets(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget) {
const parentCenter = (isVertical ? height : width) / 2
const isOddConfigured = configuredWidgets % 2 === 1
centerWidgets.forEach(widget => {
if (isVertical) {
widget.anchors.verticalCenter = undefined
} else {
widget.anchors.horizontalCenter = undefined
}
})
if (isOddConfigured && configuredMiddleWidget) {
const middleWidget = configuredMiddleWidget
const middleIndex = centerWidgets.indexOf(middleWidget)
const middleSize = isVertical ? middleWidget.height : middleWidget.width
if (isVertical) {
middleWidget.y = parentCenter - (middleSize / 2)
} else {
middleWidget.x = parentCenter - (middleSize / 2)
}
let currentPos = isVertical ? middleWidget.y : middleWidget.x
for (var i = middleIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
for (var i = middleIndex + 1; i < totalWidgets; i++) {
currentPos += spacing
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
}
} else {
if (totalWidgets === 1) {
const widget = centerWidgets[0]
const size = isVertical ? widget.height : widget.width
if (isVertical) {
widget.y = parentCenter - (size / 2)
} else {
widget.x = parentCenter - (size / 2)
}
return
}
if (!configuredLeftWidget || !configuredRightWidget) {
if (totalWidgets % 2 === 1) {
const middleIndex = Math.floor(totalWidgets / 2)
const middleWidget = centerWidgets[middleIndex]
if (!middleWidget) {
return
}
const middleSize = isVertical ? middleWidget.height : middleWidget.width
if (isVertical) {
middleWidget.y = parentCenter - (middleSize / 2)
} else {
middleWidget.x = parentCenter - (middleSize / 2)
}
let currentPos = isVertical ? middleWidget.y : middleWidget.x
for (var i = middleIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
for (var i = middleIndex + 1; i < totalWidgets; i++) {
currentPos += spacing
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
}
} else {
const leftIndex = (totalWidgets / 2) - 1
const rightIndex = totalWidgets / 2
const fallbackLeft = centerWidgets[leftIndex]
const fallbackRight = centerWidgets[rightIndex]
if (!fallbackLeft || !fallbackRight) {
return
}
const halfSpacing = spacing / 2
const leftSize = isVertical ? fallbackLeft.height : fallbackLeft.width
if (isVertical) {
fallbackLeft.y = parentCenter - halfSpacing - leftSize
fallbackRight.y = parentCenter + halfSpacing
} else {
fallbackLeft.x = parentCenter - halfSpacing - leftSize
fallbackRight.x = parentCenter + halfSpacing
}
let currentPos = isVertical ? fallbackLeft.y : fallbackLeft.x
for (var i = leftIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? fallbackRight.y + fallbackRight.height : fallbackRight.x + fallbackRight.width)
for (var i = rightIndex + 1; i < totalWidgets; i++) {
currentPos += spacing
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
}
}
return
}
const leftWidget = configuredLeftWidget
const rightWidget = configuredRightWidget
const leftIndex = centerWidgets.indexOf(leftWidget)
const rightIndex = centerWidgets.indexOf(rightWidget)
const halfSpacing = spacing / 2
const leftSize = isVertical ? leftWidget.height : leftWidget.width
if (isVertical) {
leftWidget.y = parentCenter - halfSpacing - leftSize
rightWidget.y = parentCenter + halfSpacing
} else {
leftWidget.x = parentCenter - halfSpacing - leftSize
rightWidget.x = parentCenter + halfSpacing
}
let currentPos = isVertical ? leftWidget.y : leftWidget.x
for (var i = leftIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
for (var i = rightIndex + 1; i < totalWidgets; i++) {
currentPos += spacing
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
}
}
}
function getWidgetVisible(widgetId) {
const widgetVisibility = {
"cpuUsage": DgopService.dgopAvailable,
"memUsage": DgopService.dgopAvailable,
"cpuTemp": DgopService.dgopAvailable,
"gpuTemp": DgopService.dgopAvailable,
"network_speed_monitor": DgopService.dgopAvailable
}
return widgetVisibility[widgetId] ?? true
}
function getWidgetComponent(widgetId) {
// Build dynamic component map including plugins
let baseMap = {
"launcherButton": "launcherButtonComponent",
"workspaceSwitcher": "workspaceSwitcherComponent",
"focusedWindow": "focusedWindowComponent",
"runningApps": "runningAppsComponent",
"clock": "clockComponent",
"music": "mediaComponent",
"weather": "weatherComponent",
"systemTray": "systemTrayComponent",
"privacyIndicator": "privacyIndicatorComponent",
"clipboard": "clipboardComponent",
"cpuUsage": "cpuUsageComponent",
"memUsage": "memUsageComponent",
"diskUsage": "diskUsageComponent",
"cpuTemp": "cpuTempComponent",
"gpuTemp": "gpuTempComponent",
"notificationButton": "notificationButtonComponent",
"battery": "batteryComponent",
"controlCenterButton": "controlCenterButtonComponent",
"idleInhibitor": "idleInhibitorComponent",
"spacer": "spacerComponent",
"separator": "separatorComponent",
"network_speed_monitor": "networkComponent",
"keyboard_layout_name": "keyboardLayoutNameComponent",
"vpn": "vpnComponent",
"notepadButton": "notepadButtonComponent",
"colorPicker": "colorPickerComponent",
"systemUpdate": "systemUpdateComponent"
}
// For built-in components, get from components property
const componentKey = baseMap[widgetId]
if (componentKey && root.components[componentKey]) {
return root.components[componentKey]
}
// For plugin components, get from PluginService
var parts = widgetId.split(":")
var pluginId = parts[0]
let pluginComponents = PluginService.getWidgetComponents()
return pluginComponents[pluginId] || null
}
height: parent.height
width: parent.width
anchors.centerIn: parent
Timer {
id: layoutTimer
interval: 0
repeat: false
onTriggered: root.updateLayout()
}
Component.onCompleted: {
layoutTimer.restart()
}
onWidthChanged: {
if (width > 0) {
layoutTimer.restart()
}
}
onHeightChanged: {
if (height > 0) {
layoutTimer.restart()
}
}
onVisibleChanged: {
if (visible && (isVertical ? height : width) > 0) {
layoutTimer.restart()
}
}
Repeater {
id: centerRepeater
model: root.widgetsModel
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined
anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined
active: root.getWidgetVisible(model.widgetId) && (model.widgetId !== "music" || MprisController.activePlayer !== null)
sourceComponent: root.getWidgetComponent(model.widgetId)
opacity: (model.enabled !== false) ? 1 : 0
asynchronous: false
onLoaded: {
if (!item) {
return
}
item.widthChanged.connect(() => layoutTimer.restart())
item.heightChanged.connect(() => layoutTimer.restart())
if (root.axis && "axis" in item) {
item.axis = Qt.binding(() => root.axis)
}
if (root.axis && "isVertical" in item) {
try {
item.isVertical = Qt.binding(() => root.axis.isVertical)
} catch (e) {
}
}
// Inject properties for plugin widgets
if ("section" in item) {
item.section = root.section
}
if ("parentScreen" in item) {
item.parentScreen = Qt.binding(() => root.parentScreen)
}
if ("widgetThickness" in item) {
item.widgetThickness = Qt.binding(() => root.widgetThickness)
}
if ("barThickness" in item) {
item.barThickness = Qt.binding(() => root.barThickness)
}
if ("sectionSpacing" in item) {
item.sectionSpacing = Qt.binding(() => root.spacing)
}
if ("isFirst" in item) {
item.isFirst = Qt.binding(() => {
for (var i = 0; i < centerRepeater.count; i++) {
const checkItem = centerRepeater.itemAt(i)
if (checkItem && checkItem.active && checkItem.item) {
return checkItem.item === item
}
}
return false
})
}
if ("isLast" in item) {
item.isLast = Qt.binding(() => {
for (var i = centerRepeater.count - 1; i >= 0; i--) {
const checkItem = centerRepeater.itemAt(i)
if (checkItem && checkItem.active && checkItem.item) {
return checkItem.item === item
}
}
return false
})
}
if ("isLeftBarEdge" in item) {
item.isLeftBarEdge = false
}
if ("isRightBarEdge" in item) {
item.isRightBarEdge = false
}
if ("isTopBarEdge" in item) {
item.isTopBarEdge = false
}
if ("isBottomBarEdge" in item) {
item.isBottomBarEdge = false
}
if (item.pluginService !== undefined) {
var parts = model.widgetId.split(":")
var pluginId = parts[0]
var variantId = parts.length > 1 ? parts[1] : null
if (item.pluginId !== undefined) {
item.pluginId = pluginId
}
if (item.variantId !== undefined) {
item.variantId = variantId
}
if (item.variantData !== undefined && variantId) {
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
}
item.pluginService = PluginService
}
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
layoutTimer.restart()
}
onActiveChanged: {
layoutTimer.restart()
}
}
}
Connections {
target: widgetsModel
function onCountChanged() {
layoutTimer.restart()
}
}
// Listen for plugin changes and refresh components
Connections {
target: PluginService
function onPluginLoaded(pluginId) {
// Force refresh of component lookups
for (var i = 0; i < centerRepeater.count; i++) {
var item = centerRepeater.itemAt(i)
if (item && item.widgetId.startsWith(pluginId)) {
item.sourceComponent = root.getWidgetComponent(item.widgetId)
}
}
}
function onPluginUnloaded(pluginId) {
// Force refresh of component lookups
for (var i = 0; i < centerRepeater.count; i++) {
var item = centerRepeater.itemAt(i)
if (item && item.widgetId.startsWith(pluginId)) {
item.sourceComponent = root.getWidgetComponent(item.widgetId)
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,439 +0,0 @@
// No external details import; content inlined for consistency
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
DankPopout {
id: root
layerNamespace: "dms:vpn"
Ref {
service: DMSNetworkService
}
property bool wasVisible: false
onShouldBeVisibleChanged: {
if (shouldBeVisible && !wasVisible) {
DMSNetworkService.getState()
}
wasVisible = shouldBeVisible
}
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x;
triggerY = y;
triggerWidth = width;
triggerSection = section;
triggerScreen = screen;
}
popupWidth: 360
popupHeight: Math.min(Screen.height - 100, contentLoader.item ? contentLoader.item.implicitHeight : 260)
triggerX: Screen.width - 380 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
triggerWidth: 70
positioning: ""
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
content: Component {
Rectangle {
id: content
implicitHeight: contentColumn.height + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
antialiasing: true
smooth: true
focus: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.close();
event.accepted = true;
}
}
// Outer subtle shadow rings to match BatteryPopout
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 0
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Theme.shadowMedium
border.width: 0
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineStrong
border.width: 0
radius: parent.radius
z: -1
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Item {
width: parent.width
height: 32
StyledText {
text: I18n.tr("VPN Connections")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
// Close button (matches BatteryPopout)
Rectangle {
width: 32
height: 32
radius: 16
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "close"
size: Theme.iconSize - 4
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: root.close()
}
}
}
// Inlined VPN details
Rectangle {
id: vpnDetail
width: parent.width
implicitHeight: detailsColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineStrong
border.width: 0
clip: true
Column {
id: detailsColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
spacing: Theme.spacingS
width: parent.width
StyledText {
text: {
if (!DMSNetworkService.connected) {
return "Active: None";
}
const names = DMSNetworkService.activeNames || [];
if (names.length <= 1) {
return "Active: " + (names[0] || "VPN");
}
return "Active: " + names[0] + " +" + (names.length - 1);
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
Layout.fillWidth: true
Layout.maximumWidth: parent.width - 140
}
// Removed Quick Connect for clarity
Item {
width: 1
height: 1
}
// Disconnect all (shown only when any active)
Rectangle {
height: 28
radius: 14
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: DMSNetworkService.connected
width: 130
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
border.width: 0
border.color: Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "link_off"
size: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Disconnect")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: discAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.disconnectAllActive()
}
}
}
Rectangle {
height: 1
width: parent.width
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankFlickable {
width: parent.width
height: 160
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: Theme.spacingXS
Item {
width: parent.width
height: DMSNetworkService.profiles.length === 0 ? 120 : 0
visible: height > 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "playlist_remove"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No VPN profiles found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Add a VPN in NetworkManager")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Repeater {
model: DMSNetworkService.profiles
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 300
height: 50
radius: Theme.cornerRadius
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
size: Theme.iconSize - 4
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Column {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: {
if (modelData.type === "wireguard") {
return "WireGuard";
}
const svc = modelData.serviceType || "";
if (svc.indexOf("openvpn") !== -1) {
return "OpenVPN";
}
if (svc.indexOf("wireguard") !== -1) {
return "WireGuard (plugin)";
}
if (svc.indexOf("openconnect") !== -1) {
return "OpenConnect";
}
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1) {
return "Fortinet";
}
if (svc.indexOf("strongswan") !== -1) {
return "IPsec (strongSwan)";
}
if (svc.indexOf("libreswan") !== -1) {
return "IPsec (Libreswan)";
}
if (svc.indexOf("l2tp") !== -1) {
return "L2TP/IPsec";
}
if (svc.indexOf("pptp") !== -1) {
return "PPTP";
}
if (svc.indexOf("vpnc") !== -1) {
return "Cisco (vpnc)";
}
if (svc.indexOf("sstp") !== -1) {
return "SSTP";
}
if (svc) {
const parts = svc.split('.');
return parts[parts.length - 1];
}
return "VPN";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Item {
Layout.fillWidth: true
height: 1
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(modelData.uuid)
}
}
}
Item {
height: 1
width: 1
}
}
}
}
}
}
}
}
}

View File

@@ -1,77 +0,0 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
Item {
id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool hasActiveMedia: activePlayer !== null
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
width: 20
height: Theme.iconSize
Loader {
active: isPlaying
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Timer {
id: fallbackTimer
running: !CavaService.cavaAvailable && isPlaying
interval: 256
repeat: true
onTriggered: {
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25];
}
}
Row {
anchors.centerIn: parent
spacing: 1.5
Repeater {
model: 6
Rectangle {
width: 2
height: {
if (root.isPlaying && CavaService.values.length > index) {
const rawLevel = CavaService.values[index] || 0;
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
const maxHeight = Theme.iconSize - 2;
const minHeight = 3;
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
}
return 3;
}
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
}
}
}

View File

@@ -1,255 +0,0 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool isActive: false
property var popoutTarget: null
property var widgetData: null
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : controlIndicators.implicitWidth
implicitHeight: root.isVerticalOrientation ? controlColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: controlColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
if (NetworkService.wifiToggling) {
return "sync"
}
const status = NetworkService.networkStatus
if (status === "ethernet") {
return "lan"
}
if (status === "vpn") {
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
}
return NetworkService.wifiSignalIcon
}
size: Theme.barIconSize(root.barThickness)
color: {
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon {
name: "bluetooth"
size: Theme.barIconSize(root.barThickness)
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
}
Rectangle {
width: audioIconV.implicitWidth + 4
height: audioIconV.implicitHeight + 4
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showAudioIcon
DankIcon {
id: audioIconV
name: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off"
} else if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down"
} else {
return "volume_up"
}
}
return "volume_up"
}
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
let newVolume
if (delta > 0) {
newVolume = Math.min(100, currentVolume + 5)
} else {
newVolume = Math.max(0, currentVolume - 5)
}
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newVolume / 100
}
wheelEvent.accepted = true
}
}
}
DankIcon {
name: "settings"
size: Theme.barIconSize(root.barThickness)
color: root.isActive ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
}
}
Row {
id: controlIndicators
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
id: networkIcon
name: {
if (NetworkService.wifiToggling) {
return "sync"
}
const status = NetworkService.networkStatus
if (status === "ethernet") {
return "lan"
}
if (status === "vpn") {
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
}
return NetworkService.wifiSignalIcon
}
size: Theme.barIconSize(root.barThickness)
color: {
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon {
id: bluetoothIcon
name: "bluetooth"
size: Theme.barIconSize(root.barThickness)
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
}
Rectangle {
width: audioIcon.implicitWidth + 4
height: audioIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showAudioIcon
DankIcon {
id: audioIcon
name: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off";
} else if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down";
} else {
return "volume_up";
}
}
return "volume_up";
}
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: audioWheelArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y;
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
let newVolume;
if (delta > 0) {
newVolume = Math.min(100, currentVolume + 5);
} else {
newVolume = Math.max(0, currentVolume - 5);
}
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100;
}
wheelEvent.accepted = true;
}
}
}
DankIcon {
name: "mic"
size: Theme.barIconSize(root.barThickness)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false
}
DankIcon {
name: "settings"
size: Theme.barIconSize(root.barThickness)
color: root.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
}
}
}
}
MouseArea {
x: -root.leftMargin
y: -root.topMargin
width: root.width + root.leftMargin + root.rightMargin
height: root.height + root.topMargin + root.bottomMargin
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clicked()
}
}
}

View File

@@ -1,148 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modules.Plugins
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool compactMode: SettingsData.keyboardLayoutNameCompactMode
property string currentLayout: CompositorService.isNiri ? NiriService.getCurrentKeyboardLayoutName() : ""
property string hyprlandKeyboard: ""
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth
implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: contentColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "keyboard"
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (!root.currentLayout) return ""
const parts = root.currentLayout.split(" ")
if (parts.length > 0) {
return parts[0].substring(0, 2).toUpperCase()
}
return root.currentLayout.substring(0, 2).toUpperCase()
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: contentRow
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingS
StyledText {
text: root.currentLayout
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
MouseArea {
z: 1
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CompositorService.isNiri) {
NiriService.cycleKeyboardLayout()
} else if (CompositorService.isHyprland) {
Quickshell.execDetached([
"hyprctl",
"switchxkblayout",
root.hyprlandKeyboard,
"next"
])
}
}
}
Connections {
target: CompositorService.isHyprland ? Hyprland : null
enabled: CompositorService.isHyprland
function onRawEvent(event) {
if (event.name === "activelayout") {
updateLayout()
}
}
}
Component.onCompleted: {
if (CompositorService.isHyprland) {
updateLayout()
}
}
function updateLayout() {
if (CompositorService.isHyprland) {
Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => {
if (exitCode !== 0) {
root.currentLayout = "Unknown"
return
}
try {
const data = JSON.parse(output)
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
root.hyprlandKeyboard = mainKeyboard.name
if (mainKeyboard) {
const layout = mainKeyboard.layout
const variant = mainKeyboard.variant
const index = mainKeyboard.active_layout_index
if (root.compactMode && layout && variant && index !== undefined) {
const layouts = mainKeyboard.layout.split(",")
const variants = mainKeyboard.variant.split(",")
const index = mainKeyboard.active_layout_index
if (layouts[index] && variants[index] !== undefined) {
if (variants[index] === "") {
root.currentLayout = layouts[index]
} else {
root.currentLayout = layouts[index] + "-" + variants[index]
}
} else {
root.currentLayout = "Unknown"
}
} else if (mainKeyboard && mainKeyboard.active_keymap) {
root.currentLayout = mainKeyboard.active_keymap
} else {
root.currentLayout = "Unknown"
}
} else {
root.currentLayout = "Unknown"
}
} catch (e) {
root.currentLayout = "Unknown"
}
})
}
}
}

View File

@@ -1,798 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "left"
property var parentScreen
property var hoveredItem: null
property var topBar: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
property int _desktopEntriesUpdateTrigger: 0
property int _toplevelsUpdateTrigger: 0
readonly property var sortedToplevels: {
_toplevelsUpdateTrigger
const toplevels = CompositorService.sortedToplevels
if (!toplevels || toplevels.length === 0) return []
if (SettingsData.runningAppsCurrentWorkspace) {
return CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || []
}
return toplevels
}
Connections {
target: CompositorService
function onToplevelsChanged() {
_toplevelsUpdateTrigger++
}
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
_desktopEntriesUpdateTrigger++
}
}
readonly property var groupedWindows: {
if (!SettingsData.runningAppsGroupByApp) {
return []
}
try {
if (!sortedToplevels || sortedToplevels.length === 0) {
return []
}
const appGroups = new Map()
sortedToplevels.forEach((toplevel, index) => {
if (!toplevel)
return
const appId = toplevel?.appId || "unknown"
if (!appGroups.has(appId)) {
appGroups.set(appId, {
"appId": appId,
"windows": []
})
}
appGroups.get(appId).windows.push({
"toplevel": toplevel,
"windowId": index,
"windowTitle": toplevel?.title || "(Unnamed)"
})
})
return Array.from(appGroups.values())
} catch (e) {
console.error("RunningApps: groupedWindows error:", e)
return []
}
}
readonly property int windowCount: SettingsData.runningAppsGroupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
readonly property int calculatedSize: {
if (windowCount === 0) {
return 0
}
if (SettingsData.runningAppsCompactMode) {
return windowCount * 24 + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
} else {
return windowCount * (24 + Theme.spacingXS + 120) + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
}
}
width: isVertical ? barThickness : calculatedSize
height: isVertical ? calculatedSize : barThickness
visible: windowCount > 0
Rectangle {
id: visualBackground
width: root.isVertical ? root.widgetThickness : root.calculatedSize
height: root.isVertical ? root.calculatedSize : root.widgetThickness
anchors.centerIn: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
clip: false
color: {
if (windowCount === 0) {
return "transparent"
}
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
const baseColor = Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
property real scrollAccumulator: 0
property real touchpadThreshold: 500
onWheel: wheel => {
const deltaY = wheel.angleDelta.y
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
const windows = root.sortedToplevels
if (windows.length < 2) {
return
}
if (isMouseWheel) {
// Direct mouse wheel action
let currentIndex = -1
for (var i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i
break
}
}
let nextIndex
if (deltaY < 0) {
if (currentIndex === -1) {
nextIndex = 0
} else {
nextIndex = (currentIndex + 1) % windows.length
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length - 1
} else {
nextIndex = (currentIndex - 1 + windows.length) % windows.length
}
}
const nextWindow = windows[nextIndex]
if (nextWindow) {
nextWindow.activate()
}
} else {
// Touchpad - accumulate small deltas
scrollAccumulator += deltaY
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
let currentIndex = -1
for (var i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i
break
}
}
let nextIndex
if (scrollAccumulator < 0) {
if (currentIndex === -1) {
nextIndex = 0
} else {
nextIndex = (currentIndex + 1) % windows.length
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length - 1
} else {
nextIndex = (currentIndex - 1 + windows.length) % windows.length
}
}
const nextWindow = windows[nextIndex]
if (nextWindow) {
nextWindow.activate()
}
scrollAccumulator = 0
}
}
wheel.accepted = true
}
}
Loader {
id: layoutLoader
anchors.centerIn: parent
sourceComponent: root.isVertical ? columnLayout : rowLayout
}
Component {
id: rowLayout
Row {
spacing: Theme.spacingXS
Repeater {
id: windowRepeater
model: ScriptModel {
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
}
delegate: Item {
id: delegateItem
property bool isGrouped: SettingsData.runningAppsGroupByApp
property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
property bool isFocused: toplevelData ? toplevelData.activated : false
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
property var toplevelObject: toplevelData
property int windowCount: isGrouped ? modelData.windows.length : 1
property string tooltipText: {
root._desktopEntriesUpdateTrigger
let appName = "Unknown"
if (appId) {
const desktopEntry = DesktopEntries.heuristicLookup(appId)
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
}
if (isGrouped && windowCount > 1) {
return appName + " (" + windowCount + " windows)"
}
return appName + (windowTitle ? " • " + windowTitle : "")
}
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
width: visualWidth
height: root.barThickness
Rectangle {
id: visualContent
width: delegateItem.visualWidth
height: 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: {
if (isFocused) {
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
} else {
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
}
}
// App icon
IconImage {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
source: {
root._desktopEntriesUpdateTrigger
const moddedId = Paths.moddedAppId(appId)
if (moddedId.toLowerCase().includes("steam_app")) {
return ""
}
return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
}
DankIcon {
anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"
color: Theme.surfaceText
visible: {
const moddedId = Paths.moddedAppId(appId)
return moddedId.toLowerCase().includes("steam_app")
}
}
// Fallback text if no icon found
Text {
anchors.centerIn: parent
visible: {
const moddedId = Paths.moddedAppId(appId)
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
return !iconImg.visible && !isSteamApp
}
text: {
root._desktopEntriesUpdateTrigger
if (!appId) {
return "?"
}
const desktopEntry = DesktopEntries.heuristicLookup(appId)
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
}
return appId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
}
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
anchors.bottomMargin: -2
width: 14
height: 14
radius: 7
color: Theme.primary
visible: isGrouped && windowCount > 1
z: 10
StyledText {
anchors.centerIn: parent
text: windowCount > 9 ? "9+" : windowCount
font.pixelSize: 9
color: Theme.surface
}
}
// Window title text (only visible in expanded mode)
StyledText {
anchors.left: iconImg.right
anchors.leftMargin: Theme.spacingXS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.runningAppsCompactMode
text: windowTitle
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (isGrouped && windowCount > 1) {
let currentIndex = -1
for (var i = 0; i < groupData.windows.length; i++) {
if (groupData.windows[i].toplevel.activated) {
currentIndex = i
break
}
}
const nextIndex = (currentIndex + 1) % groupData.windows.length
groupData.windows[nextIndex].toplevel.activate()
} else if (toplevelObject) {
toplevelObject.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
windowContextMenuLoader.active = true
if (windowContextMenuLoader.item) {
windowContextMenuLoader.item.currentWindow = toplevelObject
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const relativeX = globalPos.x - screenX
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
}
}
}
}
onEntered: {
root.hoveredItem = delegateItem
tooltipLoader.active = true
if (tooltipLoader.item) {
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
const isBottom = root.axis?.edge === "bottom"
const tooltipY = isBottom
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
}
}
}
onExited: {
if (root.hoveredItem === delegateItem) {
root.hoveredItem = null
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
}
}
}
}
Component {
id: columnLayout
Column {
spacing: Theme.spacingXS
Repeater {
id: windowRepeater
model: ScriptModel {
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
}
delegate: Item {
id: delegateItem
property bool isGrouped: SettingsData.runningAppsGroupByApp
property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
property bool isFocused: toplevelData ? toplevelData.activated : false
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
property var toplevelObject: toplevelData
property int windowCount: isGrouped ? modelData.windows.length : 1
property string tooltipText: {
root._desktopEntriesUpdateTrigger
let appName = "Unknown"
if (appId) {
const desktopEntry = DesktopEntries.heuristicLookup(appId)
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
}
if (isGrouped && windowCount > 1) {
return appName + " (" + windowCount + " windows)"
}
return appName + (windowTitle ? " • " + windowTitle : "")
}
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
width: root.barThickness
height: 24
Rectangle {
id: visualContent
width: delegateItem.visualWidth
height: 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: {
if (isFocused) {
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
} else {
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
}
}
IconImage {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
source: {
root._desktopEntriesUpdateTrigger
const moddedId = Paths.moddedAppId(appId)
if (moddedId.toLowerCase().includes("steam_app")) {
return ""
}
return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
}
DankIcon {
anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"
color: Theme.surfaceText
visible: {
const moddedId = Paths.moddedAppId(appId)
return moddedId.toLowerCase().includes("steam_app")
}
}
Text {
anchors.centerIn: parent
visible: {
const moddedId = Paths.moddedAppId(appId)
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
return !iconImg.visible && !isSteamApp
}
text: {
root._desktopEntriesUpdateTrigger
if (!appId) {
return "?"
}
const desktopEntry = DesktopEntries.heuristicLookup(appId)
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
}
return appId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
}
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
anchors.bottomMargin: -2
width: 14
height: 14
radius: 7
color: Theme.primary
visible: isGrouped && windowCount > 1
z: 10
StyledText {
anchors.centerIn: parent
text: windowCount > 9 ? "9+" : windowCount
font.pixelSize: 9
color: Theme.surface
}
}
StyledText {
anchors.left: iconImg.right
anchors.leftMargin: Theme.spacingXS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.runningAppsCompactMode
text: windowTitle
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (isGrouped && windowCount > 1) {
let currentIndex = -1
for (var i = 0; i < groupData.windows.length; i++) {
if (groupData.windows[i].toplevel.activated) {
currentIndex = i
break
}
}
const nextIndex = (currentIndex + 1) % groupData.windows.length
groupData.windows[nextIndex].toplevel.activate()
} else if (toplevelObject) {
toplevelObject.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
windowContextMenuLoader.active = true
if (windowContextMenuLoader.item) {
windowContextMenuLoader.item.currentWindow = toplevelObject
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const relativeX = globalPos.x - screenX
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
}
}
}
}
onEntered: {
root.hoveredItem = delegateItem
tooltipLoader.active = true
if (tooltipLoader.item) {
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
const isBottom = root.axis?.edge === "bottom"
const tooltipY = isBottom
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
}
}
}
onExited: {
if (root.hoveredItem === delegateItem) {
root.hoveredItem = null
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
}
}
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Loader {
id: windowContextMenuLoader
active: false
sourceComponent: PanelWindow {
id: contextMenuWindow
property var currentWindow: null
property bool isVisible: false
property point anchorPos: Qt.point(0, 0)
property bool isVertical: false
property string edge: "top"
function showAt(x, y, vertical, barEdge) {
screen = root.parentScreen
anchorPos = Qt.point(x, y)
isVertical = vertical ?? false
edge = barEdge ?? "top"
isVisible = true
visible = true
}
function close() {
isVisible = false
visible = false
windowContextMenuLoader.active = false
}
implicitWidth: 100
implicitHeight: 40
visible: false
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: contextMenuWindow.close()
}
Rectangle {
x: {
if (contextMenuWindow.isVertical) {
if (contextMenuWindow.edge === "left") {
return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x)
} else {
return Math.max(10, contextMenuWindow.anchorPos.x - width)
}
} else {
const left = 10
const right = contextMenuWindow.width - width - 10
const want = contextMenuWindow.anchorPos.x - width / 2
return Math.max(left, Math.min(right, want))
}
}
y: {
if (contextMenuWindow.isVertical) {
const top = 10
const bottom = contextMenuWindow.height - height - 10
const want = contextMenuWindow.anchorPos.y - height / 2
return Math.max(top, Math.min(bottom, want))
} else {
return contextMenuWindow.anchorPos.y
}
}
width: 100
height: 32
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
Rectangle {
anchors.fill: parent
radius: parent.radius
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
}
StyledText {
anchors.centerIn: parent
text: I18n.tr("Close")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenuWindow.currentWindow) {
contextMenuWindow.currentWindow.close()
}
contextMenuWindow.close()
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +0,0 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
Ref {
service: DMSNetworkService
}
property var popoutTarget: null
property bool isHovered: clickArea.containsMouse
signal toggleVpnPopup()
content: Component {
Item {
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
id: icon
name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
size: Theme.barIconSize(root.barThickness, -4)
color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceText
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
anchors.centerIn: parent
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.InOutQuad
}
}
}
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
MouseArea {
id: clickArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
enabled: !DMSNetworkService.isBusy
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.toggleVpnPopup();
}
onEntered: {
if (root.parentScreen && !(popoutTarget && popoutTarget.shouldBeVisible)) {
tooltipLoader.active = true
if (tooltipLoader.item) {
let tooltipText = ""
if (!DMSNetworkService.connected) {
tooltipText = "VPN Disconnected"
} else {
const names = DMSNetworkService.activeNames || []
if (names.length <= 1) {
const name = names[0] || ""
const maxLength = 25
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
tooltipText = "VPN Connected • " + displayName
} else {
const name = names[0]
const maxLength = 20
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
tooltipText = "VPN Connected • " + displayName + " +" + (names.length - 1)
}
}
if (root.isVerticalOrientation) {
const globalPos = mapToGlobal(width / 2, height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
} else {
const globalPos = mapToGlobal(width / 2, height)
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
}
}
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}

View File

@@ -1,950 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import Quickshell.I3
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string screenName: ""
property real widgetHeight: 30
property real barThickness: 48
property var hyprlandOverviewLoader: null
property var parentScreen: null
property int _desktopEntriesUpdateTrigger: 0
readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
}
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && ExtWorkspaceService.extWorkspaceAvailable)
Connections {
target: DesktopEntries
function onApplicationsChanged() {
_desktopEntriesUpdateTrigger++
}
}
property var currentWorkspace: {
if (useExtWorkspace) return getExtWorkspaceActiveWorkspace()
switch (CompositorService.compositor) {
case "niri":
return getNiriActiveWorkspace()
case "hyprland":
return getHyprlandActiveWorkspace()
case "dwl":
const activeTags = getDwlActiveTags()
return activeTags.length > 0 ? activeTags[0] : -1
case "sway":
return getSwayActiveWorkspace()
default:
return 1
}
}
property var dwlActiveTags: {
if (CompositorService.isDwl) {
return getDwlActiveTags()
}
return []
}
property var workspaceList: {
if (useExtWorkspace) {
const baseList = getExtWorkspaceWorkspaces()
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
}
let baseList
switch (CompositorService.compositor) {
case "niri":
baseList = getNiriWorkspaces()
break
case "hyprland":
baseList = getHyprlandWorkspaces()
break
case "dwl":
baseList = getDwlTags()
break
case "sway":
baseList = getSwayWorkspaces()
break
default:
return [1]
}
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
}
function getSwayWorkspaces() {
const workspaces = I3.workspaces?.values || []
if (workspaces.length === 0) return [{"num": 1}]
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return workspaces.slice().sort((a, b) => a.num - b.num)
}
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}]
}
function getSwayActiveWorkspace() {
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
return focusedWs ? focusedWs.num : 1
}
const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true)
return focusedWs ? focusedWs.num : 1
}
function getHyprlandWorkspaces() {
const workspaces = Hyprland.workspaces?.values || []
if (workspaces.length === 0) return [{id: 1}]
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return workspaces.slice().sort((a, b) => a.id - b.id)
}
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.id - b.id) : [{id: 1}]
}
function getHyprlandActiveWorkspace() {
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return Hyprland.focusedWorkspace?.id || 1
}
const monitor = Hyprland.monitors?.values?.find(m => m.name === root.screenName)
return monitor?.activeWorkspace?.id || 1
}
function getWorkspaceIcons(ws) {
_desktopEntriesUpdateTrigger
if (!SettingsData.showWorkspaceApps || !ws) {
return []
}
let targetWorkspaceId
if (CompositorService.isNiri) {
const wsNumber = typeof ws === "number" ? ws : -1
if (wsNumber <= 0) {
return []
}
const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.screenName)
if (!workspace) {
return []
}
targetWorkspaceId = workspace.id
} else if (CompositorService.isHyprland) {
targetWorkspaceId = ws.id !== undefined ? ws.id : ws
} else if (CompositorService.isDwl) {
if (typeof ws !== "object" || ws.tag === undefined) {
return []
}
targetWorkspaceId = ws.tag
} else if (CompositorService.isSway) {
targetWorkspaceId = ws.num !== undefined ? ws.num : ws
} else {
return []
}
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels
const byApp = {}
let isActiveWs = false
if (CompositorService.isNiri) {
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active)
} else if (CompositorService.isSway) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false
} else if (CompositorService.isDwl) {
const output = DwlService.getOutputState(root.screenName)
if (output && output.tags) {
const tag = output.tags.find(t => t.tag === targetWorkspaceId)
isActiveWs = tag ? (tag.state === 1) : false
}
} else {
isActiveWs = targetWorkspaceId === root.currentWorkspace
}
wins.forEach((w, i) => {
if (!w) {
return
}
let winWs = null
if (CompositorService.isNiri) {
winWs = w.workspace_id
} else if (CompositorService.isSway) {
winWs = w.workspace?.num
} else {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || [])
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w)
winWs = hyprToplevel?.workspace?.id
}
if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
return
}
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown")
const key = isActiveWs ? `${keyBase}_${i}` : keyBase
if (!byApp[key]) {
const moddedId = Paths.moddedAppId(keyBase)
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
const icon = isSteamApp ? "" : DesktopService.resolveIconPath(moddedId)
byApp[key] = {
"type": "icon",
"icon": icon,
"isSteamApp": isSteamApp,
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
"count": 1,
"windowId": w.address || w.id,
"fallbackText": w.appId || w.class || w.title || ""
}
} else {
byApp[key].count++
if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) {
byApp[key].active = true
}
}
})
return Object.values(byApp)
}
function padWorkspaces(list) {
const padded = list.slice()
let placeholder
if (useExtWorkspace) {
placeholder = {"id": "", "name": "", "active": false, "hidden": true}
} else if (CompositorService.isHyprland) {
placeholder = {"id": -1, "name": ""}
} else if (CompositorService.isDwl) {
placeholder = {"tag": -1}
} else if (CompositorService.isSway) {
placeholder = {"num": -1}
} else {
placeholder = -1
}
while (padded.length < 3) {
padded.push(placeholder)
}
return padded
}
function getNiriWorkspaces() {
if (NiriService.allWorkspaces.length === 0) {
return [1, 2]
}
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return NiriService.getCurrentOutputWorkspaceNumbers()
}
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1)
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
}
function getNiriActiveWorkspace() {
if (NiriService.allWorkspaces.length === 0) {
return 1
}
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return NiriService.getCurrentWorkspaceNumber()
}
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active)
return activeWs ? activeWs.idx + 1 : 1
}
function getDwlTags() {
if (!DwlService.dwlAvailable) {
return []
}
const output = DwlService.getOutputState(root.screenName)
if (!output || !output.tags || output.tags.length === 0) {
return []
}
if (SettingsData.dwlShowAllTags) {
return output.tags.map(tag => ({"tag": tag.tag, "state": tag.state, "clients": tag.clients, "focused": tag.focused}))
}
const visibleTagIndices = DwlService.getVisibleTags(root.screenName)
return visibleTagIndices.map(tagIndex => {
const tagData = output.tags.find(t => t.tag === tagIndex)
return {
"tag": tagIndex,
"state": tagData?.state ?? 0,
"clients": tagData?.clients ?? 0,
"focused": tagData?.focused ?? false
}
})
}
function getDwlActiveTags() {
if (!DwlService.dwlAvailable) {
return []
}
const activeTags = DwlService.getActiveTags(root.screenName)
return activeTags
}
function getExtWorkspaceWorkspaces() {
const groups = ExtWorkspaceService.groups
if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) {
return [{"id": "1", "name": "1", "active": false}]
}
const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName))
if (!group || !group.workspaces) {
return [{"id": "1", "name": "1", "active": false}]
}
const visible = group.workspaces.filter(ws => !ws.hidden).sort((a, b) => {
const coordsA = a.coordinates || [0, 0]
const coordsB = b.coordinates || [0, 0]
if (coordsA[0] !== coordsB[0]) return coordsA[0] - coordsB[0]
return coordsA[1] - coordsB[1]
}).map(ws => ({
id: ws.id,
name: ws.name,
coordinates: ws.coordinates,
state: ws.state,
active: ws.active,
urgent: ws.urgent,
hidden: ws.hidden,
groupID: group.id
}))
return visible.length > 0 ? visible : [{"id": "1", "name": "1", "active": false}]
}
function getExtWorkspaceActiveWorkspace() {
if (!ExtWorkspaceService.extWorkspaceAvailable) {
return 1
}
const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName)
return activeWs ? (activeWs.id || activeWs.name || "1") : "1"
}
readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
function getRealWorkspaces() {
return root.workspaceList.filter(ws => {
if (useExtWorkspace) return ws && ws.id !== "" && !ws.hidden
if (CompositorService.isHyprland) return ws && ws.id !== -1
if (CompositorService.isDwl) return ws && ws.tag !== -1
if (CompositorService.isSway) return ws && ws.num !== -1
return ws !== -1
})
}
function switchWorkspace(direction) {
if (useExtWorkspace) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
const nextWorkspace = realWorkspaces[nextIndex]
ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || "")
} else if (CompositorService.isNiri) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1)
} else if (CompositorService.isHyprland) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws.id === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`)
} else if (CompositorService.isDwl) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag)
} else if (CompositorService.isSway) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){}
}
}
width: isVertical ? barThickness : visualWidth
height: isVertical ? visualHeight : barThickness
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || useExtWorkspace
Rectangle {
id: visualBackground
width: root.visualWidth
height: root.visualHeight
anchors.centerIn: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground)
return "transparent"
const baseColor = Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) {
NiriService.toggleOverview()
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
}
}
}
}
Flow {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
Repeater {
model: ScriptModel {
values: root.workspaceList
}
Item {
id: delegateRoot
property bool isActive: {
if (root.useExtWorkspace) return (modelData?.id || modelData?.name) === root.currentWorkspace
if (CompositorService.isHyprland) return !!(modelData && modelData.id === root.currentWorkspace)
if (CompositorService.isDwl) return !!(modelData && root.dwlActiveTags.includes(modelData.tag))
if (CompositorService.isSway) return !!(modelData && modelData.num === root.currentWorkspace)
return modelData === root.currentWorkspace
}
property bool isPlaceholder: {
if (root.useExtWorkspace) return !!(modelData && modelData.hidden)
if (CompositorService.isHyprland) return !!(modelData && modelData.id === -1)
if (CompositorService.isDwl) return !!(modelData && modelData.tag === -1)
if (CompositorService.isSway) return !!(modelData && modelData.num === -1)
return modelData === -1
}
property bool isHovered: mouseArea.containsMouse
property var loadedWorkspaceData: null
property bool loadedIsUrgent: false
property bool isUrgent: {
if (root.useExtWorkspace) return modelData?.urgent ?? false
if (CompositorService.isHyprland) return modelData?.urgent ?? false
if (CompositorService.isNiri) return loadedIsUrgent
if (CompositorService.isDwl) return modelData?.state === 2
if (CompositorService.isSway) return loadedIsUrgent
return false
}
property var loadedIconData: null
property bool loadedHasIcon: false
property var loadedIcons: []
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
readonly property real iconsExtraWidth: {
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
}
return 0
}
readonly property real iconsExtraHeight: {
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
}
return 0
}
readonly property real visualWidth: baseWidth + iconsExtraWidth
readonly property real visualHeight: baseHeight + iconsExtraHeight
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: !isPlaceholder
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isPlaceholder
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (isPlaceholder) return
const isRightClick = mouse.button === Qt.RightButton
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "")
} else if (CompositorService.isNiri) {
if (isRightClick) {
NiriService.toggleOverview()
} else {
NiriService.switchToWorkspace(modelData - 1)
}
} else if (CompositorService.isHyprland && modelData?.id) {
if (isRightClick && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
} else {
Hyprland.dispatch(`workspace ${modelData.id}`)
}
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
console.log("DWL click - tag:", modelData.tag, "rightClick:", isRightClick)
if (isRightClick) {
console.log("Calling toggleTag")
DwlService.toggleTag(root.screenName, modelData.tag)
} else {
console.log("Calling switchToTag")
DwlService.switchToTag(root.screenName, modelData.tag)
}
} else if (CompositorService.isSway && modelData?.num) {
try { I3.dispatch(`workspace number ${modelData.num}`) } catch(_){}
}
}
}
Timer {
id: dataUpdateTimer
interval: 50
onTriggered: {
if (isPlaceholder) {
delegateRoot.loadedWorkspaceData = null
delegateRoot.loadedIconData = null
delegateRoot.loadedHasIcon = false
delegateRoot.loadedIcons = []
delegateRoot.loadedIsUrgent = false
return
}
var wsData = null;
if (root.useExtWorkspace) {
wsData = modelData;
} else if (CompositorService.isNiri) {
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null;
} else if (CompositorService.isHyprland) {
wsData = modelData;
} else if (CompositorService.isDwl) {
wsData = modelData;
} else if (CompositorService.isSway) {
wsData = modelData;
}
delegateRoot.loadedWorkspaceData = wsData;
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
var icData = null;
if (wsData?.name) {
icData = SettingsData.getWorkspaceNameIcon(wsData.name);
}
delegateRoot.loadedIconData = icData;
delegateRoot.loadedHasIcon = icData !== null;
if (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else {
delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
}
} else {
delegateRoot.loadedIcons = [];
}
}
}
function updateAllData() {
dataUpdateTimer.restart()
}
width: root.isVertical ? root.barThickness : visualWidth
height: root.isVertical ? visualHeight : root.barThickness
Rectangle {
id: visualContent
width: delegateRoot.visualWidth
height: delegateRoot.visualHeight
anchors.centerIn: parent
radius: Theme.cornerRadius
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
border.width: isUrgent && !isActive ? 2 : 0
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0)
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Loader {
id: appIconsLoader
anchors.fill: parent
active: SettingsData.showWorkspaceApps
sourceComponent: Item {
Loader {
id: contentRow
anchors.centerIn: parent
sourceComponent: root.isVertical ? columnLayout : rowLayout
}
Component {
id: rowLayout
Row {
spacing: 4
visible: loadedIcons.length > 0
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
}
delegate: Item {
width: 18
height: 18
IconImage {
id: appIcon
property var windowId: modelData.windowId
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
visible: !modelData.isSteamApp
}
DankIcon {
anchors.centerIn: parent
size: 18
name: "sports_esports"
color: Theme.surfaceText
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp
}
MouseArea {
id: appMouseArea
anchors.fill: parent
enabled: isActive
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!appIcon.windowId) return
if (CompositorService.isHyprland) {
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
} else if (CompositorService.isNiri) {
NiriService.focusWindow(appIcon.windowId)
}
}
}
Rectangle {
visible: modelData.count > 1 && !isActive
width: 12
height: 12
radius: 6
color: "black"
border.color: "white"
border.width: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
z: 2
Text {
anchors.centerIn: parent
text: modelData.count
font.pixelSize: 8
color: "white"
}
}
}
}
}
}
Component {
id: columnLayout
Column {
spacing: 4
visible: loadedIcons.length > 0
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
}
delegate: Item {
width: 18
height: 18
IconImage {
id: appIcon
property var windowId: modelData.windowId
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
visible: !modelData.isSteamApp
}
DankIcon {
anchors.centerIn: parent
size: 18
name: "sports_esports"
color: Theme.surfaceText
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp
}
MouseArea {
id: appMouseArea
anchors.fill: parent
enabled: isActive
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!appIcon.windowId) return
if (CompositorService.isHyprland) {
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
} else if (CompositorService.isNiri) {
NiriService.focusWindow(appIcon.windowId)
}
}
}
Rectangle {
visible: modelData.count > 1 && !isActive
width: 12
height: 12
radius: 6
color: "black"
border.color: "white"
border.width: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
z: 2
Text {
anchors.centerIn: parent
text: modelData.count
font.pixelSize: 8
color: "white"
}
}
}
}
}
}
}
}
// Loader for Custom Name Icon
Loader {
id: customIconLoader
anchors.fill: parent
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "icon" && !SettingsData.showWorkspaceApps
sourceComponent: Item {
DankIcon {
anchors.centerIn: parent
name: loadedIconData ? loadedIconData.value : "" // NULL CHECK
size: Theme.fontSizeSmall
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
weight: isActive && !isPlaceholder ? 500 : 400
}
}
}
// Loader for Custom Name Text
Loader {
id: customTextLoader
anchors.fill: parent
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "text" && !SettingsData.showWorkspaceApps
sourceComponent: Item {
StyledText {
anchors.centerIn: parent
text: loadedIconData ? loadedIconData.value : "" // NULL CHECK
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
}
}
// Loader for Workspace Index
Loader {
id: indexLoader
anchors.fill: parent
active: !isPlaceholder && SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
sourceComponent: Item {
StyledText {
anchors.centerIn: parent
text: {
let isPlaceholder
if (root.useExtWorkspace) {
isPlaceholder = modelData?.hidden === true
} else if (CompositorService.isHyprland) {
isPlaceholder = modelData?.id === -1
} else if (CompositorService.isDwl) {
isPlaceholder = modelData?.tag === -1
} else if (CompositorService.isSway) {
isPlaceholder = modelData?.num === -1
} else {
isPlaceholder = modelData === -1
}
if (isPlaceholder) return index + 1
if (root.useExtWorkspace) return modelData?.name || modelData?.id || ""
if (CompositorService.isHyprland) return modelData?.id || ""
if (CompositorService.isDwl) return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""
if (CompositorService.isSway) return modelData?.num || ""
return modelData - 1
}
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
}
}
}
Component.onCompleted: updateAllData()
Connections {
target: CompositorService
function onSortedToplevelsChanged() { delegateRoot.updateAllData() }
}
Connections {
target: NiriService
enabled: CompositorService.isNiri
function onAllWorkspacesChanged() { delegateRoot.updateAllData() }
function onWindowUrgentChanged() { delegateRoot.updateAllData() }
function onWindowsChanged() { delegateRoot.updateAllData() }
}
Connections {
target: SettingsData
function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() }
function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() }
}
Connections {
target: DwlService
enabled: CompositorService.isDwl
function onStateChanged() { delegateRoot.updateAllData() }
}
Connections {
target: Hyprland.workspaces
enabled: CompositorService.isHyprland
function onValuesChanged() { delegateRoot.updateAllData() }
}
Connections {
target: I3.workspaces
enabled: CompositorService.isSway
function onValuesChanged() { delegateRoot.updateAllData() }
}
Connections {
target: ExtWorkspaceService
enabled: root.useExtWorkspace
function onStateChanged() { delegateRoot.updateAllData() }
}
}
}
}
Component.onCompleted: {
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
DMSService.addSubscription("extworkspace")
}
}
}

View File

@@ -1,329 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import qs.Common
import qs.Widgets
import qs.Modules.DankDash
DankPopout {
id: root
layerNamespace: "dms:dash"
property bool dashVisible: false
property var triggerScreen: null
property int currentTabIndex: 0
keyboardFocusMode: WlrKeyboardFocus.Exclusive
function setTriggerPosition(x, y, width, section, screen) {
triggerSection = section
triggerScreen = screen
triggerY = y
if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)) {
const screenWidth = screen ? screen.width : Screen.width
triggerX = (screenWidth - popupWidth) / 2
triggerWidth = popupWidth
} else if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)) {
const screenHeight = screen ? screen.height : Screen.height
triggerX = (screenHeight - popupHeight) / 2
triggerWidth = popupHeight
} else {
triggerX = x
triggerWidth = width
}
}
popupWidth: 700
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerX: Screen.width - 620 - Theme.spacingL
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
triggerWidth: 80
shouldBeVisible: dashVisible
visible: shouldBeVisible
property bool __focusArmed: false
property bool __contentReady: false
function __tryFocusOnce() {
if (!__focusArmed)
return
const win = root.window
if (!win || !win.visible)
return
if (!contentLoader.item)
return
if (win.requestActivate)
win.requestActivate()
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
if (contentLoader.item.activeFocus)
__focusArmed = false
}
onDashVisibleChanged: {
if (dashVisible) {
__focusArmed = true
__contentReady = !!contentLoader.item
open()
__tryFocusOnce()
} else {
__focusArmed = false
__contentReady = false
close()
}
}
Connections {
target: contentLoader
function onLoaded() {
__contentReady = true
if (__focusArmed)
__tryFocusOnce()
}
}
Connections {
target: root.window ? root.window : null
enabled: !!root.window
function onVisibleChanged() {
if (__focusArmed)
__tryFocusOnce()
}
}
onBackgroundClicked: {
dashVisible = false
}
content: Component {
Rectangle {
id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
focus: true
Component.onCompleted: {
if (root.shouldBeVisible) {
mainContainer.forceActiveFocus()
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function () {
mainContainer.forceActiveFocus()
})
}
}
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.dashVisible = false
event.accepted = true
return
}
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
let nextIndex = root.currentTabIndex + 1
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
nextIndex++
}
if (nextIndex >= tabBar.model.length) {
nextIndex = 0
}
root.currentTabIndex = nextIndex
event.accepted = true
return
}
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
let prevIndex = root.currentTabIndex - 1
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
}
if (prevIndex < 0) {
prevIndex = tabBar.model.length - 1
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
}
}
if (prevIndex >= 0) {
root.currentTabIndex = prevIndex
}
event.accepted = true
return
}
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
if (wallpaperTab.handleKeyEvent(event)) {
event.accepted = true
return
}
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: root.shouldBeVisible
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankTabBar {
id: tabBar
width: parent.width
height: 48
currentIndex: root.currentTabIndex
spacing: Theme.spacingS
equalWidthTabs: true
enableArrowNavigation: false
focus: false
activeFocusOnTab: false
nextFocusTarget: {
const item = pages.currentItem
if (!item)
return null
if (item.focusTarget)
return item.focusTarget
return item
}
model: {
let tabs = [{
"icon": "dashboard",
"text": I18n.tr("Overview")
}, {
"icon": "music_note",
"text": I18n.tr("Media")
}, {
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}]
if (SettingsData.weatherEnabled) {
tabs.push({
"icon": "wb_sunny",
"text": I18n.tr("Weather")
})
}
tabs.push({
"icon": "settings",
"text": I18n.tr("Settings"),
"isAction": true
})
return tabs
}
onTabClicked: function (index) {
root.currentTabIndex = index
}
onActionTriggered: function (index) {
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
if (index === settingsIndex) {
dashVisible = false
settingsModal.show()
}
}
}
Item {
width: parent.width
height: Theme.spacingXS
}
StackLayout {
id: pages
width: parent.width
implicitHeight: {
if (currentIndex === 0)
return overviewTab.implicitHeight
if (currentIndex === 1)
return mediaTab.implicitHeight
if (currentIndex === 2)
return wallpaperTab.implicitHeight
if (SettingsData.weatherEnabled && currentIndex === 3)
return weatherTab.implicitHeight
return overviewTab.implicitHeight
}
currentIndex: root.currentTabIndex
OverviewTab {
id: overviewTab
onCloseDash: {
root.dashVisible = false
}
onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 3
tabBar.tabClicked(3)
}
}
onSwitchToMediaTab: {
tabBar.currentIndex = 1
tabBar.tabClicked(1)
}
}
MediaPlayerTab {
id: mediaTab
}
WallpaperTab {
id: wallpaperTab
active: root.currentTabIndex === 2
tabBarItem: tabBar
keyForwardTarget: mainContainer
targetScreen: root.triggerScreen
}
WeatherTab {
id: weatherTab
visible: SettingsData.weatherEnabled && root.currentTabIndex === 3
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,642 +0,0 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
implicitWidth: 700
implicitHeight: 410
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
DankIcon {
name: "cloud_off"
size: Theme.iconSize * 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No Weather Data Available")
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
Item {
width: parent.width
height: 70
DankIcon {
id: refreshButton
name: "refresh"
size: Theme.iconSize - 4
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
anchors.right: parent.right
anchors.top: parent.top
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
onClicked: {
refreshButton.isRefreshing = true
WeatherService.forceRefresh()
refreshTimer.restart()
}
enabled: parent.enabled
}
Timer {
id: refreshTimer
interval: 2000
onTriggered: refreshButton.isRefreshing = false
}
NumberAnimation on rotation {
running: refreshButton.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2
height: 70
DankIcon {
id: weatherIcon
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize * 1.5
color: Theme.primary
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.2)
shadowOpacity: 0.2
}
}
Column {
id: tempColumn
spacing: Theme.spacingXS
anchors.left: weatherIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
Item {
width: tempText.width + unitText.width + Theme.spacingXS
height: tempText.height
StyledText {
id: tempText
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
font.pixelSize: Theme.fontSizeLarge + 4
color: Theme.surfaceText
font.weight: Font.Light
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: unitText
text: SettingsData.useFahrenheit ? "F" : "C"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.left: tempText.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (WeatherService.weather.available) {
SettingsData.set("temperatureUnit", !SettingsData.useFahrenheit)
}
}
enabled: WeatherService.weather.available
}
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
Column {
id: sunriseColumn
spacing: Theme.spacingXS
anchors.left: tempColumn.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: WeatherService.weather.sunrise && WeatherService.weather.sunset
Item {
width: sunriseIcon.width + sunriseText.width + Theme.spacingXS
height: sunriseIcon.height
DankIcon {
id: sunriseIcon
name: "wb_twilight"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: sunriseText
text: WeatherService.weather.sunrise || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: sunriseIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: sunsetIcon.width + sunsetText.width + Theme.spacingXS
height: sunsetIcon.height
DankIcon {
id: sunsetIcon
name: "bedtime"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: sunsetText
text: WeatherService.weather.sunset || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: sunsetIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
GridLayout {
width: parent.width
height: 95
columns: 6
columnSpacing: Theme.spacingS
rowSpacing: 0
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "device_thermostat"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Feels Like")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "humidity_low"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Humidity")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "air"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Wind")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "speed"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Pressure")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.pressure ? WeatherService.weather.pressure + " hPa" : "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "rainy"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Rain Chance")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "wb_sunny"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Visibility")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Good")
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
Column {
width: parent.width
height: parent.height - 70 - 95 - Theme.spacingM * 3 - 2
spacing: Theme.spacingS
StyledText {
text: I18n.tr("7-Day Forecast")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
height: parent.height - Theme.fontSizeMedium - Theme.spacingS - Theme.spacingL
spacing: Theme.spacingXS
Repeater {
model: 7
Rectangle {
width: (parent.width - Theme.spacingXS * 6) / 7
height: parent.height
radius: Theme.cornerRadius
property var dayDate: {
const date = new Date()
date.setDate(date.getDate() + index)
return date
}
property bool isToday: index === 0
property var forecastData: {
if (WeatherService.weather.forecast && WeatherService.weather.forecast.length > index) {
return WeatherService.weather.forecast[index]
}
return null
}
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
border.width: isToday ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: Qt.locale().dayName(dayDate.getDay(), Locale.ShortFormat)
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : Theme.surfaceText
font.weight: isToday ? Font.Medium : Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
DankIcon {
name: forecastData ? WeatherService.getWeatherIcon(forecastData.wCode || 0) : "cloud"
size: Theme.iconSize
color: isToday ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
anchors.horizontalCenter: parent.horizontalCenter
}
Column {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: forecastData ? (SettingsData.useFahrenheit ? (forecastData.tempMaxF || forecastData.tempMax) : (forecastData.tempMax || 0)) + "°/" + (SettingsData.useFahrenheit ? (forecastData.tempMinF || forecastData.tempMin) : (forecastData.tempMin || 0)) + "°" : "--/--"
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Column {
spacing: 1
anchors.horizontalCenter: parent.horizontalCenter
visible: forecastData && forecastData.sunrise && forecastData.sunset
Row {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "wb_twilight"
size: 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: forecastData ? forecastData.sunrise : ""
font.pixelSize: Theme.fontSizeSmall - 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "bedtime"
size: 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: forecastData ? forecastData.sunset : ""
font.pixelSize: Theme.fontSizeSmall - 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -1,397 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
pragma ComponentBehavior: Bound
Variants {
id: dockVariants
model: SettingsData.getFilteredScreens("dock")
property var contextMenu
delegate: PanelWindow {
id: dock
WlrLayershell.namespace: "dms:dock"
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
anchors {
top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
}
property var modelData: item
property bool autoHide: SettingsData.dockAutoHide
property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
readonly property real widgetHeight: SettingsData.dockIconSize
readonly property real effectiveBarHeight: widgetHeight + SettingsData.dockSpacing * 2 + 10
readonly property real barSpacing: {
const barIsHorizontal = (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)
const barIsVertical = (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)
const samePosition = (SettingsData.dockPosition === SettingsData.dankBarPosition)
const dockIsHorizontal = !isVertical
const dockIsVertical = isVertical
if (!SettingsData.dankBarVisible) return 0
if (dockIsHorizontal && barIsHorizontal && samePosition) {
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
}
if (dockIsVertical && barIsVertical && samePosition) {
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
}
return 0
}
readonly property real dockMargin: SettingsData.dockSpacing
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
function px(v) { return Math.round(v * _dpr) / _dpr }
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool revealSticky: false
Timer {
id: revealHold
interval: 250
repeat: false
onTriggered: dock.revealSticky = false
}
property bool reveal: {
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
return true
}
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
}
onContextMenuOpenChanged: {
if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) {
revealSticky = true
revealHold.restart()
}
}
Connections {
target: SettingsData
function onDockTransparencyChanged() {
dock.backgroundTransparency = SettingsData.dockTransparency
}
}
screen: modelData
visible: {
if (CompositorService.isNiri && NiriService.inOverview) {
return SettingsData.dockOpenOnOverview
}
return SettingsData.showDock
}
color: "transparent"
exclusiveZone: {
if (!SettingsData.showDock || autoHide) return -1
if (barSpacing > 0) return -1
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin)
}
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
Item {
id: maskItem
parent: dock.contentItem
visible: false
x: {
const baseX = dockCore.x + dockMouseArea.x
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) {
return baseX - animationHeadroom
}
return baseX
}
y: {
const baseY = dockCore.y + dockMouseArea.y
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) {
return baseY - animationHeadroom
}
return baseY
}
width: dockMouseArea.width + (isVertical ? animationHeadroom : 0)
height: dockMouseArea.height + (!isVertical ? animationHeadroom : 0)
}
mask: Region {
item: maskItem
}
property var hoveredButton: {
if (!dockApps.children[0]) {
return null
}
const layoutItem = dockApps.children[0]
const flowLayout = layoutItem.children[0]
let repeater = null
for (var i = 0; i < flowLayout.children.length; i++) {
const child = flowLayout.children[i]
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
repeater = child
break
}
}
if (!repeater || !repeater.itemAt) {
return null
}
for (var i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i)
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton
}
}
return null
}
DankTooltip {
id: dockTooltip
targetScreen: dock.screen
}
Timer {
id: tooltipRevealDelay
interval: 250
repeat: false
onTriggered: dock.showTooltipForHoveredButton()
}
function showTooltipForHoveredButton() {
dockTooltip.hide()
if (dock.hoveredButton && dock.reveal && !slideXAnimation.running && !slideYAnimation.running) {
const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0)
const tooltipText = dock.hoveredButton.tooltipText || ""
if (tooltipText) {
const screenX = dock.screen ? (dock.screen.x || 0) : 0
const screenY = dock.screen ? (dock.screen.y || 0) : 0
const screenHeight = dock.screen ? dock.screen.height : 0
if (!dock.isVertical) {
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2
const screenRelativeY = isBottom
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - SettingsData.dockMargin - 35)
: (buttonGlobalPos.y - screenY + dock.hoveredButton.height + Theme.spacingS)
dockTooltip.show(tooltipText,
globalX,
screenRelativeY,
dock.screen,
false, false)
} else {
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + Theme.spacingXS
const tooltipX = isLeft ? tooltipOffset : (dock.screen.width - tooltipOffset)
const screenRelativeY = buttonGlobalPos.y - screenY + dock.hoveredButton.height / 2
dockTooltip.show(tooltipText,
screenX + tooltipX,
screenRelativeY,
dock.screen,
isLeft,
!isLeft)
}
}
}
}
Connections {
target: dock
function onRevealChanged() {
if (!dock.reveal) {
tooltipRevealDelay.stop()
dockTooltip.hide()
} else {
tooltipRevealDelay.restart()
}
}
function onHoveredButtonChanged() {
dock.showTooltipForHoveredButton()
}
}
Item {
id: dockCore
anchors.fill: parent
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
Connections {
target: dockMouseArea
function onContainsMouseChanged() {
if (dockMouseArea.containsMouse) {
dock.revealSticky = true
revealHold.stop()
} else {
if (dock.autoHide && !dock.contextMenuOpen) {
revealHold.restart()
}
}
}
}
MouseArea {
id: dockMouseArea
property real currentScreen: modelData ? modelData : dock.screen
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080
property real maxDockWidth: screenWidth * 0.98
property real maxDockHeight: screenHeight * 0.98
height: {
if (dock.isVertical) {
return dock.reveal ? Math.min(dockBackground.implicitHeight + 4, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
} else {
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
}
}
width: {
if (dock.isVertical) {
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
} else {
return dock.reveal ? Math.min(dockBackground.implicitWidth + 4, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
}
}
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
hoverEnabled: true
acceptedButtons: Qt.NoButton
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Item {
id: dockContainer
anchors.fill: parent
clip: false
transform: Translate {
id: dockSlide
x: {
if (!dock.isVertical) return 0
if (dock.reveal) return 0
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance
} else {
return -hideDistance
}
}
y: {
if (dock.isVertical) return 0
if (dock.reveal) return 0
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance
} else {
return -hideDistance
}
}
Behavior on x {
NumberAnimation {
id: slideXAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on y {
NumberAnimation {
id: slideYAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
}
Item {
id: dockBackground
objectName: "dockBackground"
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 : 0
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
width: implicitWidth
height: implicitHeight
layer.enabled: true
clip: false
DankRectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
overlayColor: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
}
}
DockApps {
id: dockApps
anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
contextMenu: dockVariants.contextMenu
groupByApp: dock.groupByApp
isVertical: dock.isVertical
dockScreen: dock.screen
iconSize: dock.widgetHeight
}
}
}
}
}
}

View File

@@ -1,251 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var contextMenu: null
property bool requestDockShow: false
property int pinnedAppCount: 0
property bool groupByApp: false
property bool isVertical: false
property var dockScreen: null
property real iconSize: 40
clip: false
implicitWidth: isVertical ? appLayout.height : appLayout.width
implicitHeight: isVertical ? appLayout.width : appLayout.height
function movePinnedApp(fromIndex, toIndex) {
if (fromIndex === toIndex) {
return
}
const currentPinned = [...(SessionData.pinnedApps || [])]
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
return
}
const movedApp = currentPinned.splice(fromIndex, 1)[0]
currentPinned.splice(toIndex, 0, movedApp)
SessionData.setPinnedApps(currentPinned)
}
Item {
id: appLayout
width: layoutFlow.width
height: layoutFlow.height
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
anchors.top: root.isVertical ? undefined : parent.top
Flow {
id: layoutFlow
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
Repeater {
id: repeater
property var dockItems: []
model: ScriptModel {
values: repeater.dockItems
objectProp: "uniqueKey"
}
Component.onCompleted: updateModel()
function updateModel() {
const items = []
const pinnedApps = [...(SessionData.pinnedApps || [])]
const sortedToplevels = CompositorService.sortedToplevels
if (root.groupByApp) {
const appGroups = new Map()
pinnedApps.forEach(appId => {
appGroups.set(appId, {
appId: appId,
isPinned: true,
windows: []
})
})
sortedToplevels.forEach((toplevel, index) => {
const appId = toplevel.appId || "unknown"
if (!appGroups.has(appId)) {
appGroups.set(appId, {
appId: appId,
isPinned: false,
windows: []
})
}
appGroups.get(appId).windows.push({
toplevel: toplevel,
index: index
})
})
const pinnedGroups = []
const unpinnedGroups = []
Array.from(appGroups.entries()).forEach(([appId, group]) => {
const firstWindow = group.windows.length > 0 ? group.windows[0] : null
const item = {
uniqueKey: "grouped_" + appId,
type: "grouped",
appId: appId,
toplevel: firstWindow ? firstWindow.toplevel : null,
isPinned: group.isPinned,
isRunning: group.windows.length > 0,
windowCount: group.windows.length,
allWindows: group.windows
}
if (group.isPinned) {
pinnedGroups.push(item)
} else {
unpinnedGroups.push(item)
}
})
pinnedGroups.forEach(item => items.push(item))
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
items.push({
uniqueKey: "separator_grouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
})
}
unpinnedGroups.forEach(item => items.push(item))
root.pinnedAppCount = pinnedGroups.length
} else {
pinnedApps.forEach(appId => {
items.push({
uniqueKey: "pinned_" + appId,
type: "pinned",
appId: appId,
toplevel: null,
isPinned: true,
isRunning: false
})
})
root.pinnedAppCount = pinnedApps.length
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
items.push({
uniqueKey: "separator_ungrouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
})
}
sortedToplevels.forEach((toplevel, index) => {
let uniqueKey = "window_" + index
if (CompositorService.isHyprland && Hyprland.toplevels) {
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
for (let i = 0; i < hyprlandToplevels.length; i++) {
if (hyprlandToplevels[i].wayland === toplevel) {
uniqueKey = "window_" + hyprlandToplevels[i].address
break
}
}
}
items.push({
uniqueKey: uniqueKey,
type: "window",
appId: toplevel.appId,
toplevel: toplevel,
isPinned: false,
isRunning: true
})
})
}
dockItems = items
}
delegate: Item {
id: delegateItem
property alias dockButton: button
property var itemData: modelData
clip: false
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
Rectangle {
visible: itemData.type === "separator"
width: root.isVertical ? root.iconSize * 0.5 : 2
height: root.isVertical ? 2 : root.iconSize * 0.5
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
radius: 1
anchors.centerIn: parent
}
DockAppButton {
id: button
visible: itemData.type !== "separator"
anchors.centerIn: parent
width: delegateItem.width
height: delegateItem.height
actualIconSize: root.iconSize
appData: itemData
contextMenu: root.contextMenu
dockApps: root
index: model.index
parentDockScreen: root.dockScreen
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
windowTitle: {
const title = itemData?.toplevel?.title || "(Unnamed)"
return title.length > 50 ? title.substring(0, 47) + "..." : title
}
}
}
}
}
}
Connections {
target: CompositorService
function onToplevelsChanged() {
repeater.updateModel()
}
}
Connections {
target: SessionData
function onPinnedAppsChanged() {
repeater.updateModel()
}
}
onGroupByAppChanged: {
repeater.updateModel()
}
}

View File

@@ -1,459 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool isVisible: false
property bool showLogout: true
property int selectedIndex: 0
property int optionCount: {
let count = 0
if (showLogout) count++
count++
if (SessionService.hibernateSupported) count++
count += 2
return count
}
signal closed()
function show() {
isVisible = true
selectedIndex = 0
Qt.callLater(() => {
if (powerMenuFocusScope && powerMenuFocusScope.forceActiveFocus) {
powerMenuFocusScope.forceActiveFocus()
}
})
}
function hide() {
isVisible = false
closed()
}
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
visible: isVisible
z: 1000
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
FocusScope {
id: powerMenuFocusScope
anchors.fill: parent
focus: root.isVisible
onVisibleChanged: {
if (visible) {
Qt.callLater(() => forceActiveFocus())
}
}
Keys.onEscapePressed: {
root.hide()
}
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
break
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
const actions = []
if (showLogout) actions.push("logout")
actions.push("suspend")
if (SessionService.hibernateSupported) actions.push("hibernate")
actions.push("reboot", "poweroff")
if (selectedIndex < actions.length) {
const action = actions[selectedIndex]
hide()
switch (action) {
case "logout":
SessionService.logout()
break
case "suspend":
SessionService.suspend()
break
case "hibernate":
SessionService.hibernate()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}
event.accepted = true
break
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
}
break
case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
}
break
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % optionCount
event.accepted = true
}
break
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
event.accepted = true
}
break
}
}
Rectangle {
anchors.centerIn: parent
width: 320
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
height: implicitHeight
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outlineMedium
border.width: 1
Column {
id: mainColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: I18n.tr("Power Options")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hide()
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
visible: showLogout
color: {
if (selectedIndex === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (logoutArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
border.width: selectedIndex === 0 ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Log Out")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.logout()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
const suspendIdx = showLogout ? 1 : 0
if (selectedIndex === suspendIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (suspendArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === (showLogout ? 1 : 0) ? Theme.primary : "transparent"
border.width: selectedIndex === (showLogout ? 1 : 0) ? 1 : 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Suspend")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.suspend()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
const hibernateIdx = showLogout ? 2 : 1
if (selectedIndex === hibernateIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (hibernateArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: selectedIndex === (showLogout ? 2 : 1) ? Theme.primary : "transparent"
border.width: selectedIndex === (showLogout ? 2 : 1) ? 1 : 0
visible: SessionService.hibernateSupported
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "ac_unit"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Hibernate")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: hibernateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.hibernate()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
if (selectedIndex === rebootIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (rebootArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
return selectedIndex === rebootIdx ? Theme.primary : "transparent"
}
border.width: {
let rebootIdx = showLogout ? 3 : 2
if (!SessionService.hibernateSupported) rebootIdx--
return selectedIndex === rebootIdx ? 1 : 0
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Reboot")
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.reboot()
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
if (selectedIndex === powerOffIdx) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (powerOffArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
}
border.color: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
return selectedIndex === powerOffIdx ? Theme.primary : "transparent"
}
border.width: {
let powerOffIdx = showLogout ? 4 : 3
if (!SessionService.hibernateSupported) powerOffIdx--
return selectedIndex === powerOffIdx ? 1 : 0
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Power Off")
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hide()
SessionService.poweroff()
}
}
}
}
Item {
height: Theme.spacingS
}
}
}
}
}

View File

@@ -1,137 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankListView {
id: listView
property var keyboardController: null
property bool keyboardActive: false
property bool autoScrollDisabled: false
property alias count: listView.count
property alias listContentHeight: listView.contentHeight
clip: true
model: NotificationService.groupedNotifications
spacing: Theme.spacingL
onIsUserScrollingChanged: {
if (isUserScrolling && keyboardController && keyboardController.keyboardNavigationActive) {
autoScrollDisabled = true
}
}
function enableAutoScroll() {
autoScrollDisabled = false
}
Timer {
id: positionPreservationTimer
interval: 200
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled
repeat: true
onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
keyboardController.ensureVisible()
}
}
}
NotificationEmptyState {
visible: listView.count === 0
anchors.centerIn: parent
}
onModelChanged: {
if (!keyboardController || !keyboardController.keyboardNavigationActive) {
return
}
keyboardController.rebuildFlatNavigation()
Qt.callLater(() => {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
delegate: Item {
required property var modelData
required property int index
readonly property bool isExpanded: (NotificationService.expandedGroups[modelData && modelData.key] || false)
width: ListView.view.width
height: notificationCard.height
NotificationCard {
id: notificationCard
width: parent.width
notificationGroup: modelData
keyboardNavigationActive: listView.keyboardActive
isGroupSelected: {
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) {
return false
}
keyboardController.selectionVersion
const selection = keyboardController.getCurrentSelection()
return selection.type === "group" && selection.groupIndex === index
}
selectedNotificationIndex: {
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) {
return -1
}
keyboardController.selectionVersion
const selection = keyboardController.getCurrentSelection()
return (selection.type === "notification" && selection.groupIndex === index) ? selection.notificationIndex : -1
}
}
}
Connections {
target: NotificationService
function onGroupedNotificationsChanged() {
if (!keyboardController) {
return
}
if (keyboardController.isTogglingGroup) {
keyboardController.rebuildFlatNavigation()
return
}
keyboardController.rebuildFlatNavigation()
if (keyboardController.keyboardNavigationActive) {
Qt.callLater(() => {
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
function onExpandedGroupsChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
Qt.callLater(() => {
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
function onExpandedMessagesChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
Qt.callLater(() => {
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
}
}

View File

@@ -1,271 +0,0 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
QtObject {
id: manager
property var modelData
property int topMargin: 0
property int baseNotificationHeight: 120
property int maxTargetNotifications: 4
property var popupWindows: [] // strong refs to windows (live until exitFinished)
property var destroyingWindows: new Set()
property Component popupComponent
popupComponent: Component {
NotificationPopup {
onEntered: manager._onPopupEntered(this)
onExitFinished: manager._onPopupExitFinished(this)
}
}
property Connections notificationConnections
notificationConnections: Connections {
function onVisibleNotificationsChanged() {
manager._sync(NotificationService.visibleNotifications)
}
target: NotificationService
}
property Timer sweeper
sweeper: Timer {
interval: 500
running: false
repeat: true
onTriggered: {
const toRemove = []
for (const p of popupWindows) {
if (!p) {
toRemove.push(p)
continue
}
const isZombie = p.status === Component.Null || (!p.visible && !p.exiting) || (!p.notificationData && !p._isDestroying) || (!p.hasValidData && !p._isDestroying)
if (isZombie) {
toRemove.push(p)
if (p.forceExit) {
p.forceExit()
} else if (p.destroy) {
try {
p.destroy()
} catch (e) {
}
}
}
}
if (toRemove.length) {
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1)
const survivors = _active().sort((a, b) => a.screenY - b.screenY)
for (let k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight
}
}
if (popupWindows.length === 0) {
sweeper.stop()
}
}
}
function _hasWindowFor(w) {
return popupWindows.some(p => p && p.notificationData === w && !p._isDestroying && p.status !== Component.Null)
}
function _isValidWindow(p) {
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData
}
function _canMakeRoomFor(wrapper) {
const activeWindows = _active()
if (activeWindows.length < maxTargetNotifications) {
return true
}
if (!wrapper || !wrapper.notification) {
return false
}
const incomingUrgency = wrapper.notification.urgency || 0
for (const p of activeWindows) {
if (!p.notificationData || !p.notificationData.notification) {
continue
}
const existingUrgency = p.notificationData.notification.urgency || 0
if (existingUrgency < incomingUrgency) {
return true
}
if (existingUrgency === incomingUrgency) {
const timer = p.notificationData.timer
if (timer && !timer.running) {
return true
}
}
}
return false
}
function _makeRoomForNew(wrapper) {
const activeWindows = _active()
if (activeWindows.length < maxTargetNotifications) {
return
}
const toRemove = _selectPopupToRemove(activeWindows, wrapper)
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.notificationData.popup = false
if (toRemove.notificationData.timer) {
toRemove.notificationData.timer.stop()
}
}
}
function _selectPopupToRemove(activeWindows, incomingWrapper) {
const incomingUrgency = (incomingWrapper && incomingWrapper.notification) ? incomingWrapper.notification.urgency || 0 : 0
const sortedWindows = activeWindows.slice().sort((a, b) => {
const aUrgency = (a.notificationData && a.notificationData.notification) ? a.notificationData.notification.urgency || 0 : 0
const bUrgency = (b.notificationData && b.notificationData.notification) ? b.notificationData.notification.urgency || 0 : 0
if (aUrgency !== bUrgency) {
return aUrgency - bUrgency
}
const aTimer = a.notificationData && a.notificationData.timer
const bTimer = b.notificationData && b.notificationData.timer
const aRunning = aTimer && aTimer.running
const bRunning = bTimer && bTimer.running
if (aRunning !== bRunning) {
return aRunning ? 1 : -1
}
return b.screenY - a.screenY
})
return sortedWindows[0]
}
function _sync(newWrappers) {
for (const w of newWrappers) {
if (w && !_hasWindowFor(w)) {
insertNewestAtTop(w)
}
}
for (const p of popupWindows.slice()) {
if (!_isValidWindow(p)) {
continue
}
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) {
p.notificationData.removedByLimit = true
p.notificationData.popup = false
}
}
}
function insertNewestAtTop(wrapper) {
if (!wrapper) {
return
}
for (const p of popupWindows) {
if (!_isValidWindow(p)) {
continue
}
if (p.exiting) {
continue
}
p.screenY = p.screenY + baseNotificationHeight
}
const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : ""
const win = popupComponent.createObject(null, {
"notificationData": wrapper,
"notificationId": notificationId,
"screenY": topMargin,
"screen": manager.modelData
})
if (!win) {
return
}
if (!win.hasValidData) {
win.destroy()
return
}
popupWindows.push(win)
if (!sweeper.running) {
sweeper.start()
}
}
function _active() {
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting)
}
function _bottom() {
let b = null
let maxY = -1
for (const p of _active()) {
if (p.screenY > maxY) {
maxY = p.screenY
b = p
}
}
return b
}
function _onPopupEntered(p) {}
function _onPopupExitFinished(p) {
if (!p) {
return
}
const windowId = p.toString()
if (destroyingWindows.has(windowId)) {
return
}
destroyingWindows.add(windowId)
const i = popupWindows.indexOf(p)
if (i !== -1) {
popupWindows.splice(i, 1)
popupWindows = popupWindows.slice()
}
if (NotificationService.releaseWrapper && p.notificationData) {
NotificationService.releaseWrapper(p.notificationData)
}
Qt.callLater(() => {
if (p && p.destroy) {
try {
p.destroy()
} catch (e) {
}
}
Qt.callLater(() => destroyingWindows.delete(windowId))
})
const survivors = _active().sort((a, b) => a.screenY - b.screenY)
for (let k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight
}
}
function cleanupAllWindows() {
sweeper.stop()
for (const p of popupWindows.slice()) {
if (p) {
try {
if (p.forceExit) {
p.forceExit()
} else if (p.destroy) {
p.destroy()
}
} catch (e) {
}
}
}
popupWindows = []
destroyingWindows.clear()
}
onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running) {
sweeper.start()
} else if (popupWindows.length === 0 && sweeper.running) {
sweeper.stop()
}
}
}

View File

@@ -1,138 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankOSD {
id: root
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
osdHeight: 40 + Theme.spacingS * 2
autoHideInterval: 3000
enableMouseInteraction: true
Connections {
target: DisplayService
function onBrightnessChanged(showOsd) {
if (showOsd) {
root.show()
}
}
}
content: Item {
anchors.fill: parent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: {
const deviceInfo = DisplayService.getCurrentDeviceInfo()
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
return "brightness_medium"
} else if (deviceInfo.name.includes("kbd")) {
return "keyboard"
} else {
return "lightbulb"
}
}
size: Theme.iconSize
color: Theme.primary
}
}
DankSlider {
id: brightnessSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: {
const deviceInfo = DisplayService.getCurrentDeviceInfo()
if (!deviceInfo) return 1
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
if (isExponential) {
return 1
}
return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0
}
maximum: {
const deviceInfo = DisplayService.getCurrentDeviceInfo()
if (!deviceInfo) return 100
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
if (isExponential) {
return 100
}
return deviceInfo.displayMax || 100
}
enabled: DisplayService.brightnessAvailable
showValue: true
unit: {
const deviceInfo = DisplayService.getCurrentDeviceInfo()
if (!deviceInfo) return "%"
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
if (isExponential) {
return "%"
}
return deviceInfo.class === "ddc" ? "" : "%"
}
thumbOutlineColor: Theme.surfaceContainer
alwaysShowValue: SettingsData.osdAlwaysShowValue
Component.onCompleted: {
if (DisplayService.brightnessAvailable) {
value = DisplayService.brightnessLevel
}
}
onSliderValueChanged: newValue => {
if (DisplayService.brightnessAvailable) {
DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true)
resetHideTimer()
}
}
onContainsMouseChanged: {
setChildHovered(containsMouse)
}
onSliderDragFinished: finalValue => {
if (DisplayService.brightnessAvailable) {
DisplayService.setBrightness(finalValue, DisplayService.lastIpcDevice, true)
}
}
Connections {
target: DisplayService
function onBrightnessChanged(showOsd) {
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
brightnessSlider.value = DisplayService.brightnessLevel
}
}
function onDeviceSwitched() {
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
brightnessSlider.value = DisplayService.brightnessLevel
}
}
}
}
}
}
}

View File

@@ -1,139 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankOSD {
id: root
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
osdHeight: 40 + Theme.spacingS * 2
autoHideInterval: 3000
enableMouseInteraction: true
Connections {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() {
if (!AudioService.suppressOSD) {
root.show()
}
}
function onMutedChanged() {
if (!AudioService.suppressOSD) {
root.show()
}
}
}
Connections {
target: AudioService
function onSinkChanged() {
if (root.shouldBeVisible) {
root.show()
}
}
}
content: Item {
anchors.fill: parent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: muteButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
AudioService.toggleMute()
}
onContainsMouseChanged: {
setChildHovered(containsMouse || volumeSlider.containsMouse)
}
}
}
DankSlider {
id: volumeSlider
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
readonly property real displayPercent: actualVolumePercent
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 100
enabled: AudioService.sink && AudioService.sink.audio
showValue: true
unit: "%"
thumbOutlineColor: Theme.surfaceContainer
valueOverride: displayPercent
alwaysShowValue: SettingsData.osdAlwaysShowValue
Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio) {
value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
}
}
onSliderValueChanged: newValue => {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.suppressOSD = true
AudioService.sink.audio.volume = newValue / 100
AudioService.suppressOSD = false
resetHideTimer()
}
}
onContainsMouseChanged: {
setChildHovered(containsMouse || muteButton.containsMouse)
}
Connections {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() {
if (volumeSlider && !volumeSlider.pressed) {
volumeSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
}
}
}
}
}
}
onOsdShown: {
if (AudioService.sink && AudioService.sink.audio && contentLoader.item) {
const slider = contentLoader.item.children[0].children[1]
if (slider) {
slider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
}
}
}
}

View File

@@ -1,88 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var axis: null
property string section: "center"
property var popoutTarget: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property alias content: contentLoader.sourceComponent
property bool isVerticalOrientation: axis?.isVertical ?? false
property bool isFirst: false
property bool isLast: false
property real sectionSpacing: 0
property bool isLeftBarEdge: false
property bool isRightBarEdge: false
property bool isTopBarEdge: false
property bool isBottomBarEdge: false
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
readonly property real visualWidth: isVerticalOrientation ? widgetThickness : (contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0)
readonly property real visualHeight: isVerticalOrientation ? (contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0) : widgetThickness
readonly property alias visualContent: visualContent
readonly property real barEdgeExtension: 1000
readonly property real gapExtension: sectionSpacing
readonly property real leftMargin: !isVerticalOrientation ? (isLeftBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
readonly property real rightMargin: !isVerticalOrientation ? (isRightBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
readonly property real topMargin: isVerticalOrientation ? (isTopBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
readonly property real bottomMargin: isVerticalOrientation ? (isBottomBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
signal clicked()
signal rightClicked()
width: isVerticalOrientation ? barThickness : visualWidth
height: isVerticalOrientation ? visualHeight : barThickness
Rectangle {
id: visualContent
width: root.visualWidth
height: root.visualHeight
anchors.centerIn: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
const isHovered = mouseArea.containsMouse || (root.isHovered ?? false)
const baseColor = isHovered ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
Loader {
id: contentLoader
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: mouseArea
z: -1
x: -root.leftMargin
y: -root.topMargin
width: root.width + root.leftMargin + root.rightMargin
height: root.height + root.topMargin + root.bottomMargin
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: function (mouse) {
if (mouse.button === Qt.RightButton) {
root.rightClicked()
return
}
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clicked()
}
}
}

View File

@@ -1,184 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
required property string pluginId
property var pluginService: null
default property list<QtObject> content
signal settingChanged()
property var variants: []
property alias variantsModel: variantsListModel
implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
height: implicitHeight
readonly property bool hasPermission: {
if (!pluginService || !pluginId) return true
const allPlugins = pluginService.availablePlugins
const plugin = allPlugins[pluginId]
if (!plugin) return true
const permissions = Array.isArray(plugin.permissions) ? plugin.permissions : []
return permissions.indexOf("settings_write") !== -1
}
Component.onCompleted: {
loadVariants()
}
onPluginServiceChanged: {
if (pluginService) {
loadVariants()
for (let i = 0; i < content.length; i++) {
const child = content[i]
if (child.loadValue) {
child.loadValue()
}
}
}
}
onContentChanged: {
for (let i = 0; i < content.length; i++) {
const item = content[i]
if (item instanceof Item) {
item.parent = settingsColumn
}
}
}
Connections {
target: pluginService
function onPluginDataChanged(changedPluginId) {
if (changedPluginId === pluginId) {
loadVariants()
}
}
}
function loadVariants() {
if (!pluginService || !pluginId) {
variants = []
return
}
variants = pluginService.getPluginVariants(pluginId)
syncVariantsToModel()
}
function syncVariantsToModel() {
variantsListModel.clear()
for (let i = 0; i < variants.length; i++) {
variantsListModel.append(variants[i])
}
}
onVariantsChanged: {
syncVariantsToModel()
}
ListModel {
id: variantsListModel
}
function createVariant(variantName, variantConfig) {
if (!pluginService || !pluginId) {
return null
}
return pluginService.createPluginVariant(pluginId, variantName, variantConfig)
}
function removeVariant(variantId) {
if (!pluginService || !pluginId) {
return
}
pluginService.removePluginVariant(pluginId, variantId)
}
function updateVariant(variantId, variantConfig) {
if (!pluginService || !pluginId) {
return
}
pluginService.updatePluginVariant(pluginId, variantId, variantConfig)
}
function saveValue(key, value) {
if (!pluginService) {
return
}
if (!hasPermission) {
console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission")
return
}
if (pluginService.savePluginData) {
pluginService.savePluginData(pluginId, key, value)
settingChanged()
}
}
function loadValue(key, defaultValue) {
if (pluginService && pluginService.loadPluginData) {
return pluginService.loadPluginData(pluginId, key, defaultValue)
}
return defaultValue
}
function findFlickable(item) {
var current = item?.parent
while (current) {
if (current.contentY !== undefined && current.contentHeight !== undefined) {
return current
}
current = current.parent
}
return null
}
function ensureItemVisible(item) {
if (!item) return
var flickable = findFlickable(root)
if (!flickable) return
var itemGlobalY = item.mapToItem(null, 0, 0).y
var itemHeight = item.height
var flickableGlobalY = flickable.mapToItem(null, 0, 0).y
var viewportHeight = flickable.height
var itemRelativeY = itemGlobalY - flickableGlobalY
var viewportTop = 0
var viewportBottom = viewportHeight
if (itemRelativeY < viewportTop) {
flickable.contentY = Math.max(0, flickable.contentY - (viewportTop - itemRelativeY) - Theme.spacingL)
} else if (itemRelativeY + itemHeight > viewportBottom) {
flickable.contentY = Math.min(
flickable.contentHeight - viewportHeight,
flickable.contentY + (itemRelativeY + itemHeight - viewportBottom) + Theme.spacingL
)
}
}
StyledText {
id: errorText
visible: pluginService && !root.hasPermission
anchors.fill: parent
text: I18n.tr("This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json")
color: Theme.error
font.pixelSize: Theme.fontSizeMedium
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Column {
id: settingsColumn
visible: root.hasPermission
width: parent.width
spacing: Theme.spacingM
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,622 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: dockTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Dock Position
StyledRect {
width: parent.width
height: dockPositionSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: dockPositionSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "swap_vert"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: positionText
text: I18n.tr("Dock Position")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - Theme.iconSize - Theme.spacingM - positionText.width - positionButtonGroup.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
}
DankButtonGroup {
id: positionButtonGroup
anchors.verticalCenter: parent.verticalCenter
model: ["Top", "Bottom", "Left", "Right"]
currentIndex: {
switch (SettingsData.dockPosition) {
case SettingsData.Position.Top: return 0
case SettingsData.Position.Bottom: return 1
case SettingsData.Position.Left: return 2
case SettingsData.Position.Right: return 3
default: return 1
}
}
onSelectionChanged: (index, selected) => {
if (selected) {
switch (index) {
case 0: SettingsData.setDockPosition(SettingsData.Position.Top); break
case 1: SettingsData.setDockPosition(SettingsData.Position.Bottom); break
case 2: SettingsData.setDockPosition(SettingsData.Position.Left); break
case 3: SettingsData.setDockPosition(SettingsData.Position.Right); break
}
}
}
}
}
}
}
// Dock Visibility Section
StyledRect {
width: parent.width
height: dockVisibilitySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: dockVisibilitySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "dock_to_bottom"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- enableToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Show Dock")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Display a dock with pinned and running applications that can be positioned at the top, bottom, left, or right edge of the screen")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: enableToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.showDock
onToggled: checked => {
SettingsData.setShowDock(checked)
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: SettingsData.showDock
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: SettingsData.showDock
opacity: visible ? 1 : 0
DankIcon {
name: "visibility_off"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- autoHideToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Auto-hide Dock")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Hide the dock when not in use and reveal it when hovering near the dock area")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: autoHideToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.dockAutoHide
onToggled: checked => {
SettingsData.set("dockAutoHide", checked)
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: CompositorService.isNiri
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: CompositorService.isNiri
DankIcon {
name: "fullscreen"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- overviewToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Show on Overview")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Always show the dock when niri's overview is open")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: overviewToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.dockOpenOnOverview
onToggled: checked => {
SettingsData.set("dockOpenOnOverview", checked)
}
}
}
}
}
// Group by App
StyledRect {
width: parent.width
height: groupByAppSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: groupByAppSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "apps"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- groupByAppToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Group by App")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Group multiple windows of the same app together with a window count indicator")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: groupByAppToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.dockGroupByApp
onToggled: checked => {
SettingsData.set("dockGroupByApp", checked)
}
}
}
}
}
// Indicator Style Section
StyledRect {
width: parent.width
height: indicatorStyleSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: indicatorStyleSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "fiber_manual_record"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: indicatorStyleText
text: I18n.tr("Indicator Style")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - Theme.iconSize - Theme.spacingM - indicatorStyleText.width - indicatorStyleButtonGroup.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
}
DankButtonGroup {
id: indicatorStyleButtonGroup
anchors.verticalCenter: parent.verticalCenter
model: ["Circle", "Line"]
currentIndex: SettingsData.dockIndicatorStyle === "circle" ? 0 : 1
onSelectionChanged: (index, selected) => {
if (selected) {
SettingsData.set("dockIndicatorStyle", index === 0 ? "circle" : "line")
}
}
}
}
}
}
// Icon Size Section
StyledRect {
width: parent.width
height: iconSizeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: iconSizeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "photo_size_select_large"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Icon Size")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankSlider {
width: parent.width
height: 24
value: SettingsData.dockIconSize
minimum: 24
maximum: 96
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: newValue => {
SettingsData.set("dockIconSize", newValue)
}
}
}
}
// Dock Spacing Section
StyledRect {
width: parent.width
height: dockSpacingSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: dockSpacingSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "space_bar"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Spacing")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Padding")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: SettingsData.dockSpacing
minimum: 0
maximum: 32
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: newValue => {
SettingsData.set("dockSpacing",
newValue)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Height to Edge Gap (Exclusive Zone)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: SettingsData.dockBottomGap
minimum: -100
maximum: 100
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: newValue => {
SettingsData.set("dockBottomGap",
newValue)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Margin")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: SettingsData.dockMargin
minimum: 0
maximum: 100
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: newValue => {
SettingsData.set("dockMargin", newValue)
}
}
}
}
}
// Dock Transparency Section
StyledRect {
width: parent.width
height: transparencySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: transparencySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "opacity"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Dock Transparency")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankSlider {
width: parent.width
height: 32
value: Math.round(SettingsData.dockTransparency * 100)
minimum: 0
maximum: 100
unit: "%"
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: newValue => {
SettingsData.set("dockTransparency",
newValue / 100)
}
}
}
}
}
}
}

View File

@@ -1,754 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modals
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
Item {
id: recentAppsTab
FileBrowserModal {
id: logoFileBrowser
browserTitle: I18n.tr("Select Launcher Logo")
browserIcon: "image"
browserType: "generic"
filterExtensions: ["*.svg", "*.png", "*.jpg", "*.jpeg", "*.webp"]
onFileSelected: path => {
SettingsData.set("launcherLogoCustomPath", path.replace("file://", ""))
}
}
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: launcherLogoSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: launcherLogoSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "apps"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launcher Button Logo")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: I18n.tr("Choose the logo displayed on the launcher button in DankBar")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Item {
width: parent.width
height: logoModeGroup.height
DankButtonGroup {
id: logoModeGroup
anchors.horizontalCenter: parent.horizontalCenter
model: {
const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo"), I18n.tr("Dank")]
if (CompositorService.isNiri) {
modes.push("niri")
} else if (CompositorService.isHyprland) {
modes.push("Hyprland")
} else if (CompositorService.isDwl) {
modes.push("mango")
} else if (CompositorService.isSway) {
modes.push("Sway")
} else {
modes.push(I18n.tr("Compositor"))
}
modes.push(I18n.tr("Custom"))
return modes
}
currentIndex: {
if (SettingsData.launcherLogoMode === "apps") return 0
if (SettingsData.launcherLogoMode === "os") return 1
if (SettingsData.launcherLogoMode === "dank") return 2
if (SettingsData.launcherLogoMode === "compositor") return 3
if (SettingsData.launcherLogoMode === "custom") return 4
return 0
}
onSelectionChanged: (index, selected) => {
if (!selected) return
if (index === 0) {
SettingsData.set("launcherLogoMode", "apps")
} else if (index === 1) {
SettingsData.set("launcherLogoMode", "os")
} else if (index === 2) {
SettingsData.set("launcherLogoMode", "dank")
} else if (index === 3) {
SettingsData.set("launcherLogoMode", "compositor")
} else if (index === 4) {
SettingsData.set("launcherLogoMode", "custom")
}
}
}
}
Row {
width: parent.width
visible: SettingsData.launcherLogoMode === "custom"
opacity: visible ? 1 : 0
spacing: Theme.spacingM
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
StyledRect {
width: parent.width - selectButton.width - Theme.spacingM
height: 36
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
border.color: Theme.outlineStrong
border.width: 1
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: SettingsData.launcherLogoCustomPath || I18n.tr("Select an image file...")
font.pixelSize: Theme.fontSizeMedium
color: SettingsData.launcherLogoCustomPath ? Theme.surfaceText : Theme.outlineButton
width: parent.width - Theme.spacingM * 2
elide: Text.ElideMiddle
}
}
DankActionButton {
id: selectButton
iconName: "folder_open"
width: 36
height: 36
onClicked: logoFileBrowser.open()
}
}
Column {
width: parent.width
spacing: Theme.spacingL
visible: SettingsData.launcherLogoMode !== "apps"
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Color Override")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
DankButtonGroup {
id: colorModeGroup
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
currentIndex: {
const override = SettingsData.launcherLogoColorOverride
if (override === "") return 0
if (override === "primary") return 1
if (override === "surface") return 2
return 3
}
onSelectionChanged: (index, selected) => {
if (!selected) return
if (index === 0) {
SettingsData.set("launcherLogoColorOverride", "")
} else if (index === 1) {
SettingsData.set("launcherLogoColorOverride", "primary")
} else if (index === 2) {
SettingsData.set("launcherLogoColorOverride", "surface")
} else if (index === 3) {
const currentOverride = SettingsData.launcherLogoColorOverride
const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface"
if (isPreset) {
SettingsData.set("launcherLogoColorOverride", "#ffffff")
}
}
}
}
Rectangle {
visible: {
const override = SettingsData.launcherLogoColorOverride
return override !== "" && override !== "primary" && override !== "surface"
}
width: 36
height: 36
radius: 18
color: {
const override = SettingsData.launcherLogoColorOverride
if (override !== "" && override !== "primary" && override !== "surface") {
return override
}
return "#ffffff"
}
border.color: Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (PopoutService.colorPickerModal) {
PopoutService.colorPickerModal.selectedColor = SettingsData.launcherLogoColorOverride
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Launcher Logo Color")
PopoutService.colorPickerModal.onColorSelectedCallback = function(selectedColor) {
SettingsData.set("launcherLogoColorOverride", selectedColor)
}
PopoutService.colorPickerModal.show()
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Column {
width: 120
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: I18n.tr("Size Offset")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
DankSlider {
width: 100
height: 20
minimum: -12
maximum: 12
value: SettingsData.launcherLogoSizeOffset
unit: ""
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
anchors.horizontalCenter: parent.horizontalCenter
onSliderValueChanged: newValue => {
SettingsData.set("launcherLogoSizeOffset", newValue)
}
}
}
}
Item {
width: parent.width
height: customControlsFlow.height
visible: {
const override = SettingsData.launcherLogoColorOverride
return override !== "" && override !== "primary" && override !== "surface"
}
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Flow {
id: customControlsFlow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingS
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Brightness")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 100
value: Math.round(SettingsData.launcherLogoBrightness * 100)
unit: "%"
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
anchors.horizontalCenter: parent.horizontalCenter
onSliderValueChanged: newValue => {
SettingsData.set("launcherLogoBrightness", newValue / 100)
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Contrast")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 200
value: Math.round(SettingsData.launcherLogoContrast * 100)
unit: "%"
showValue: true
wheelEnabled: false
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
anchors.horizontalCenter: parent.horizontalCenter
onSliderValueChanged: newValue => {
SettingsData.set("launcherLogoContrast", newValue / 100)
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Invert on mode change")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
DankToggle {
width: 32
height: 18
checked: SettingsData.launcherLogoColorInvertOnMode
anchors.horizontalCenter: parent.horizontalCenter
onToggled: checked => {
SettingsData.set("launcherLogoColorInvertOnMode", checked)
}
}
}
}
}
}
}
}
StyledRect {
width: parent.width
height: launchPrefixSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: launchPrefixSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "terminal"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch Prefix")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: I18n.tr("Add a custom prefix to all application launches. This can be used for things like 'uwsm-app', 'systemd-run', or other command wrappers.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankTextField {
width: parent.width
text: SettingsData.launchPrefix
placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')")
onTextEdited: {
SettingsData.set("launchPrefix", text)
}
}
}
}
StyledRect {
width: parent.width
height: sortingSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: sortingSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "sort_by_alpha"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Sort Alphabetically")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - parent.children[0].width
- parent.children[1].width
- sortToggle.width - Theme.spacingM * 3
height: 1
}
DankToggle {
id: sortToggle
width: 32
height: 18
checked: SettingsData.sortAppsAlphabetically
anchors.verticalCenter: parent.verticalCenter
onToggled: checked => {
SettingsData.set("sortAppsAlphabetically", checked)
}
}
}
StyledText {
width: parent.width
text: I18n.tr("When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
}
StyledRect {
width: parent.width
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: recentlyUsedSection
property var rankedAppsModel: {
var apps = []
for (var appId in (AppUsageHistoryData.appUsageRanking
|| {})) {
var appData = (AppUsageHistoryData.appUsageRanking
|| {})[appId]
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
})
}
apps.sort(function (a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount
return a.name.localeCompare(b.name)
})
return apps.slice(0, 20)
}
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "history"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Recently Used Apps")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - parent.children[0].width
- parent.children[1].width
- clearAllButton.width - Theme.spacingM * 3
height: 1
}
DankActionButton {
id: clearAllButton
iconName: "delete_sweep"
iconSize: Theme.iconSize - 2
iconColor: Theme.error
anchors.verticalCenter: parent.verticalCenter
onClicked: {
AppUsageHistoryData.appUsageRanking = {}
AppUsageHistoryData.saveSettings()
}
}
}
StyledText {
width: parent.width
text: I18n.tr("Apps are ordered by usage frequency, then last used, then alphabetically.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Column {
id: rankedAppsList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: recentlyUsedSection.rankedAppsModel
delegate: Rectangle {
width: rankedAppsList.width
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.3)
border.color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText {
text: (index + 1).toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
width: 20
anchors.verticalCenter: parent.verticalCenter
}
Image {
width: 24
height: 24
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable"
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name
|| "Unknown App"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: {
if (!modelData.lastUsed)
return "Never used"
var date = new Date(modelData.lastUsed)
var now = new Date()
var diffMs = now - date
var diffMins = Math.floor(
diffMs / (1000 * 60))
var diffHours = Math.floor(
diffMs / (1000 * 60 * 60))
var diffDays = Math.floor(
diffMs / (1000 * 60 * 60 * 24))
if (diffMins < 1)
return I18n.tr("Last launched just now")
if (diffMins < 60)
return I18n.tr("Last launched %1 minute%2 ago")
.arg(diffMins)
.arg(diffMins === 1 ? "" : "s")
if (diffHours < 24)
return I18n.tr("Last launched %1 hour%2 ago")
.arg(diffHours)
.arg(diffHours === 1 ? "" : "s")
if (diffDays < 7)
return I18n.tr("Last launched %1 day%2 ago")
.arg(diffDays)
.arg(diffDays === 1 ? "" : "s")
return I18n.tr("Last launched %1")
.arg(date.toLocaleDateString())
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
circular: true
iconName: "close"
iconSize: 16
iconColor: Theme.error
onClicked: {
var currentRanking = Object.assign(
{},
AppUsageHistoryData.appUsageRanking
|| {})
delete currentRanking[modelData.id]
AppUsageHistoryData.appUsageRanking = currentRanking
AppUsageHistoryData.saveSettings()
}
}
}
}
StyledText {
width: parent.width
text: recentlyUsedSection.rankedAppsModel.length
=== 0 ? "No apps have been launched yet." : ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: recentlyUsedSection.rankedAppsModel.length === 0
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,669 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property var allPlugins: []
property string searchQuery: ""
property var filteredPlugins: []
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property bool isLoading: false
property var parentModal: null
width: 600
height: 650
allowStacking: true
backgroundOpacity: 0
closeOnEscapeKey: false
function updateFilteredPlugins() {
var filtered = []
var query = searchQuery ? searchQuery.toLowerCase() : ""
for (var i = 0; i < allPlugins.length; i++) {
var plugin = allPlugins[i]
var isFirstParty = plugin.firstParty || false
if (!SessionData.showThirdPartyPlugins && !isFirstParty) {
continue
}
if (query.length > 0) {
var name = plugin.name ? plugin.name.toLowerCase() : ""
var description = plugin.description ? plugin.description.toLowerCase() : ""
var author = plugin.author ? plugin.author.toLowerCase() : ""
if (name.indexOf(query) !== -1 ||
description.indexOf(query) !== -1 ||
author.indexOf(query) !== -1) {
filtered.push(plugin)
}
} else {
filtered.push(plugin)
}
}
filteredPlugins = filtered
selectedIndex = -1
keyboardNavigationActive = false
}
function selectNext() {
if (filteredPlugins.length === 0) return
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, filteredPlugins.length - 1)
}
function selectPrevious() {
if (filteredPlugins.length === 0) return
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, -1)
if (selectedIndex === -1) {
keyboardNavigationActive = false
}
}
function installPlugin(pluginName) {
ToastService.showInfo("Installing plugin: " + pluginName)
DMSService.install(pluginName, response => {
if (response.error) {
ToastService.showError("Install failed: " + response.error)
} else {
ToastService.showInfo("Plugin installed: " + pluginName)
PluginService.scanPlugins()
refreshPlugins()
}
})
}
function refreshPlugins() {
isLoading = true
DMSService.listPlugins()
if (DMSService.apiVersion >= 8) {
DMSService.listInstalled()
}
}
function show() {
if (parentModal) {
parentModal.shouldHaveFocus = false
}
open()
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
}
function hide() {
close()
if (parentModal) {
parentModal.shouldHaveFocus = Qt.binding(() => {
return parentModal.shouldBeVisible
})
Qt.callLater(() => {
if (parentModal.modalFocusScope) {
parentModal.modalFocusScope.forceActiveFocus()
}
})
}
}
onOpened: {
refreshPlugins()
}
Connections {
target: contentLoader
function onLoaded() {
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
}
}
onDialogClosed: () => {
allPlugins = []
searchQuery = ""
filteredPlugins = []
selectedIndex = -1
keyboardNavigationActive = false
isLoading = false
}
onBackgroundClicked: () => {
hide()
}
content: Component {
FocusScope {
id: browserKeyHandler
property alias searchField: browserSearchField
anchors.fill: parent
focus: true
Component.onCompleted: {
browserSearchField.forceActiveFocus()
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
} else if (event.key === Qt.Key_Down) {
root.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
root.selectPrevious()
event.accepted = true
}
}
Item {
id: browserContent
anchors.fill: parent
anchors.margins: Theme.spacingL
Item {
id: headerArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: Math.max(headerIcon.height, headerText.height, refreshButton.height, closeButton.height)
DankIcon {
id: headerIcon
name: "store"
size: Theme.iconSize
color: Theme.primary
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: headerText
text: I18n.tr("Browse Plugins")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.left: headerIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankButton {
id: thirdPartyButton
text: SessionData.showThirdPartyPlugins ? "Hide 3rd Party" : "Show 3rd Party"
iconName: SessionData.showThirdPartyPlugins ? "visibility_off" : "visibility"
height: 28
onClicked: {
if (SessionData.showThirdPartyPlugins) {
SessionData.setShowThirdPartyPlugins(false)
root.updateFilteredPlugins()
} else {
thirdPartyConfirmModal.open()
}
}
}
DankActionButton {
id: refreshButton
iconName: "refresh"
iconSize: 18
iconColor: Theme.primary
visible: !root.isLoading
onClicked: root.refreshPlugins()
}
DankActionButton {
id: closeButton
iconName: "close"
iconSize: Theme.iconSize - 2
iconColor: Theme.outline
onClicked: root.close()
}
}
}
StyledText {
id: descriptionText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerArea.bottom
anchors.topMargin: Theme.spacingM
text: I18n.tr("Install plugins from the DMS plugin registry")
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
wrapMode: Text.WordWrap
}
DankTextField {
id: browserSearchField
anchors.left: parent.left
anchors.right: parent.right
anchors.top: descriptionText.bottom
anchors.topMargin: Theme.spacingM
height: 48
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search plugins...")
text: root.searchQuery
focus: true
ignoreLeftRightKeys: true
keyForwardTargets: [browserKeyHandler]
onTextEdited: {
root.searchQuery = text
root.updateFilteredPlugins()
}
}
Item {
id: listArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: browserSearchField.bottom
anchors.topMargin: Theme.spacingM
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
Item {
anchors.fill: parent
visible: root.isLoading
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: "sync"
size: 48
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
RotationAnimator on rotation {
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
running: root.isLoading
}
}
StyledText {
text: I18n.tr("Loading plugins...")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
DankListView {
id: pluginBrowserList
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
spacing: Theme.spacingS
model: ScriptModel {
values: root.filteredPlugins
}
clip: true
visible: !root.isLoading
ScrollBar.vertical: DankScrollbar {
id: browserScrollbar
}
delegate: Rectangle {
width: pluginBrowserList.width
height: pluginDelegateColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
property bool isInstalled: modelData.installed || false
property bool isFirstParty: modelData.firstParty || false
color: isSelected ? Theme.primarySelected :
Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.3)
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: isSelected ? 2 : 1
Column {
id: pluginDelegateColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: modelData.icon || "extension"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - installButton.width - Theme.spacingM
spacing: 2
Row {
spacing: Theme.spacingXS
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
height: 16
width: firstPartyText.implicitWidth + Theme.spacingXS * 2
radius: 8
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4)
border.width: 1
visible: isFirstParty
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: firstPartyText
anchors.centerIn: parent
text: I18n.tr("official")
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.primary
font.weight: Font.Medium
}
}
Rectangle {
height: 16
width: thirdPartyText.implicitWidth + Theme.spacingXS * 2
radius: 8
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.15)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.4)
border.width: 1
visible: !isFirstParty
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: thirdPartyText
anchors.centerIn: parent
text: I18n.tr("3rd party")
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.warning
font.weight: Font.Medium
}
}
}
StyledText {
text: {
const author = "by " + (modelData.author || "Unknown")
const source = modelData.repo ? ` <a href="${modelData.repo}" style="text-decoration:none; color:${Theme.primary};">source</a>` : ""
return author + source
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
linkColor: Theme.primary
textFormat: Text.RichText
elide: Text.ElideRight
width: parent.width
onLinkActivated: url => Qt.openUrlExternally(url)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
}
Rectangle {
id: installButton
width: 80
height: 32
radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter
color: isInstalled ? Theme.surfaceVariant : Theme.primary
opacity: isInstalled ? 1 : (installMouseArea.containsMouse ? 0.9 : 1)
border.width: isInstalled ? 1 : 0
border.color: Theme.outline
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: isInstalled ? "check" : "download"
size: 14
color: isInstalled ? Theme.surfaceText : Theme.surface
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: isInstalled ? "Installed" : "Install"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: isInstalled ? Theme.surfaceText : Theme.surface
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: installMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: isInstalled ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isInstalled
onClicked: {
if (!isInstalled) {
root.installPlugin(modelData.name)
}
}
}
}
}
StyledText {
text: modelData.description || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width
wrapMode: Text.WordWrap
visible: modelData.description && modelData.description.length > 0
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: modelData.capabilities && modelData.capabilities.length > 0
Repeater {
model: modelData.capabilities || []
Rectangle {
height: 18
width: capabilityText.implicitWidth + Theme.spacingXS * 2
radius: 9
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 1
StyledText {
id: capabilityText
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.primary
}
}
}
}
}
}
}
StyledText {
anchors.centerIn: listArea
text: I18n.tr("No plugins found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: !root.isLoading && root.filteredPlugins.length === 0
}
}
}
}
}
DankModal {
id: thirdPartyConfirmModal
width: 500
height: 300
allowStacking: true
backgroundOpacity: 0.4
closeOnEscapeKey: true
content: Component {
FocusScope {
anchors.fill: parent
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
thirdPartyConfirmModal.close()
event.accepted = true
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "warning"
size: Theme.iconSize
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Third-Party Plugin Warning")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: I18n.tr("Third-party plugins are created by the community and are not officially supported by DankMaterialShell.\n\nThese plugins may pose security and privacy risks - install at your own risk.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: Text.WordWrap
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("• Plugins may contain bugs or security issues")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• Review code before installation when possible")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("• Install only from trusted sources")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: parent.width
height: parent.height - parent.spacing * 3 - y
}
Row {
anchors.right: parent.right
spacing: Theme.spacingM
DankButton {
text: I18n.tr("Cancel")
iconName: "close"
onClicked: thirdPartyConfirmModal.close()
}
DankButton {
text: I18n.tr("I Understand")
iconName: "check"
onClicked: {
SessionData.setShowThirdPartyPlugins(true)
root.updateFilteredPlugins()
thirdPartyConfirmModal.close()
}
}
}
}
}
}
}
}

View File

@@ -1,106 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string title: ""
property string iconName: ""
property alias content: contentLoader.sourceComponent
property bool expanded: false
property bool collapsible: true
property bool lazyLoad: true
width: parent.width
spacing: expanded ? Theme.spacingM : 0
Component.onCompleted: {
if (!collapsible)
expanded = true
}
MouseArea {
width: parent.width
height: headerRow.height
enabled: collapsible
hoverEnabled: collapsible
onClicked: {
if (collapsible)
expanded = !expanded
}
Rectangle {
anchors.fill: parent
color: parent.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : "transparent"
radius: Theme.radiusS
}
Row {
id: headerRow
width: parent.width
spacing: Theme.spacingS
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
DankIcon {
name: root.collapsible ? (root.expanded ? "expand_less" : "expand_more") : root.iconName
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on rotation {
NumberAnimation {
duration: Appearance.anim.durations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
DankIcon {
name: root.iconName
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.collapsible
}
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: expanded || !collapsible
}
Loader {
id: contentLoader
width: parent.width
active: lazyLoad ? expanded || !collapsible : true
visible: expanded || !collapsible
asynchronous: true
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,346 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modals.Common
import qs.Widgets
DankModal {
id: root
property var allWidgets: []
property string targetSection: ""
property string searchQuery: ""
property var filteredWidgets: []
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property Component widgetSelectionContent
property var parentModal: null
signal widgetSelected(string widgetId, string targetSection)
function updateFilteredWidgets() {
if (!searchQuery || searchQuery.length === 0) {
filteredWidgets = allWidgets.slice()
return
}
var filtered = []
var query = searchQuery.toLowerCase()
for (var i = 0; i < allWidgets.length; i++) {
var widget = allWidgets[i]
var text = widget.text ? widget.text.toLowerCase() : ""
var description = widget.description ? widget.description.toLowerCase() : ""
var id = widget.id ? widget.id.toLowerCase() : ""
if (text.indexOf(query) !== -1 ||
description.indexOf(query) !== -1 ||
id.indexOf(query) !== -1) {
filtered.push(widget)
}
}
filteredWidgets = filtered
selectedIndex = -1
keyboardNavigationActive = false
}
onAllWidgetsChanged: {
updateFilteredWidgets()
}
function selectNext() {
if (filteredWidgets.length === 0) return
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, filteredWidgets.length - 1)
}
function selectPrevious() {
if (filteredWidgets.length === 0) return
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, -1)
if (selectedIndex === -1) {
keyboardNavigationActive = false
}
}
function selectWidget() {
if (selectedIndex >= 0 && selectedIndex < filteredWidgets.length) {
var widget = filteredWidgets[selectedIndex]
root.widgetSelected(widget.id, root.targetSection)
root.close()
}
}
function show() {
if (parentModal) {
parentModal.shouldHaveFocus = false
}
open()
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.forceActiveFocus()
}
})
}
function hide() {
close()
if (parentModal) {
parentModal.shouldHaveFocus = Qt.binding(() => {
return parentModal.shouldBeVisible
})
Qt.callLater(() => {
if (parentModal && parentModal.modalFocusScope) {
parentModal.modalFocusScope.forceActiveFocus()
}
})
}
}
width: 500
height: 550
allowStacking: true
backgroundOpacity: 0
closeOnEscapeKey: false
onDialogClosed: () => {
allWidgets = []
targetSection = ""
searchQuery = ""
filteredWidgets = []
selectedIndex = -1
keyboardNavigationActive = false
if (parentModal) {
parentModal.shouldHaveFocus = Qt.binding(() => {
return parentModal.shouldBeVisible
})
Qt.callLater(() => {
if (parentModal && parentModal.modalFocusScope) {
parentModal.modalFocusScope.forceActiveFocus()
}
})
}
}
onBackgroundClicked: () => {
return hide()
}
content: widgetSelectionContent
widgetSelectionContent: Component {
FocusScope {
id: widgetKeyHandler
property alias searchField: searchField
anchors.fill: parent
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
} else if (event.key === Qt.Key_Down) {
root.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
root.selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
root.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
root.selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
root.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
root.selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (root.keyboardNavigationActive) {
root.selectWidget()
} else if (root.filteredWidgets.length > 0) {
var firstWidget = root.filteredWidgets[0]
root.widgetSelected(firstWidget.id, root.targetSection)
root.close()
}
event.accepted = true
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 2
iconColor: Theme.outline
anchors.top: parent.top
anchors.topMargin: Theme.spacingM
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
onClicked: root.close()
}
Column {
id: contentColumn
spacing: Theme.spacingM
anchors.fill: parent
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingL + 30 // Space for close button
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "add_circle"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Add Widget to ") + root.targetSection + " Section"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("Select a widget to add to the ") + root.targetSection.toLowerCase(
) + " section of the top bar. You can add multiple instances of the same widget if needed."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width
wrapMode: Text.WordWrap
}
DankTextField {
id: searchField
width: parent.width
height: 48
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
placeholderText: ""
text: root.searchQuery
focus: true
ignoreLeftRightKeys: true
keyForwardTargets: [widgetKeyHandler]
onTextEdited: {
root.searchQuery = text
updateFilteredWidgets()
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up ||
((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false
}
}
}
DankListView {
id: widgetList
width: parent.width
height: parent.height - y
spacing: Theme.spacingS
model: root.filteredWidgets
clip: true
delegate: Rectangle {
width: widgetList.width
height: 60
radius: Theme.cornerRadius
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
color: isSelected ? Theme.primarySelected :
widgetArea.containsMouse ? Theme.primaryHover : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.3)
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: isSelected ? 2 : 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - Theme.iconSize - Theme.spacingM * 3
StyledText {
text: modelData.text
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
elide: Text.ElideRight
width: parent.width
wrapMode: Text.WordWrap
}
}
DankIcon {
name: "add"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: widgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.widgetSelected(modelData.id,
root.targetSection)
root.close()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}

View File

@@ -1,705 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: widgetTweaksTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: workspaceSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: workspaceSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "view_module"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Workspace Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Workspace Index Numbers")
description: I18n.tr("Show workspace index numbers in the top bar workspace switcher")
checked: SettingsData.showWorkspaceIndex
onToggled: checked => {
return SettingsData.set("showWorkspaceIndex",
checked)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Workspace Padding")
description: I18n.tr("Always show a minimum of 3 workspaces, even if fewer are available")
checked: SettingsData.showWorkspacePadding
onToggled: checked => {
return SettingsData.set("showWorkspacePadding",
checked)
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show Workspace Apps")
description: I18n.tr("Display application icons in workspace indicators")
checked: SettingsData.showWorkspaceApps
visible: CompositorService.isNiri || CompositorService.isHyprland
onToggled: checked => {
return SettingsData.set("showWorkspaceApps",
checked)
}
}
Row {
width: parent.width - Theme.spacingL
spacing: Theme.spacingL
visible: SettingsData.showWorkspaceApps
opacity: visible ? 1 : 0
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Max apps to show")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
width: 100
height: 28
placeholderText: "#ffffff"
text: SettingsData.maxWorkspaceIcons
maximumLength: 7
font.pixelSize: Theme.fontSizeSmall
topPadding: Theme.spacingXS
bottomPadding: Theme.spacingXS
onEditingFinished: {
SettingsData.set("maxWorkspaceIcons", parseInt(text, 10))
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Per-Monitor Workspaces")
description: I18n.tr("Show only workspaces belonging to each specific monitor.")
checked: SettingsData.workspacesPerMonitor
onToggled: checked => {
return SettingsData.set("workspacesPerMonitor", checked);
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show All Tags")
description: I18n.tr("Show all 9 tags instead of only occupied tags (DWL only)")
checked: SettingsData.dwlShowAllTags
visible: CompositorService.isDwl
onToggled: checked => {
return SettingsData.set("dwlShowAllTags", checked);
}
}
}
}
StyledRect {
width: parent.width
height: mediaSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: mediaSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "music_note"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Media Player Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Wave Progress Bars")
description: I18n.tr("Use animated wave progress bars for media playback")
checked: SettingsData.waveProgressEnabled
onToggled: checked => {
return SettingsData.set("waveProgressEnabled", checked);
}
}
}
}
StyledRect {
width: parent.width
height: updaterSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: updaterSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "refresh"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("System Updater")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Use Custom Command")
description: I18n.tr("Use custom command for update your system")
checked: SettingsData.updaterUseCustomCommand
onToggled: checked => {
if (!checked) {
updaterCustomCommand.text = "";
updaterTerminalCustomClass.text = "";
SettingsData.set("updaterCustomCommand", "");
SettingsData.set("updaterTerminalAdditionalParams", "");
}
return SettingsData.set("updaterUseCustomCommand", checked);
}
}
FocusScope {
width: parent.width - Theme.spacingM * 2
height: customCommandColumn.implicitHeight
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
Column {
id: customCommandColumn
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("System update custom command")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updaterCustomCommand
width: parent.width
height: 48
placeholderText: "myPkgMngr --sysupdate"
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.updaterCustomCommand) {
text = SettingsData.updaterCustomCommand;
}
}
onTextEdited: {
SettingsData.set("updaterCustomCommand", text.trim());
}
MouseArea {
anchors.fill: parent
onPressed: mouse => {
updaterCustomCommand.forceActiveFocus()
mouse.accepted = false
}
}
}
}
}
FocusScope {
width: parent.width - Theme.spacingM * 2
height: terminalParamsColumn.implicitHeight
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
Column {
id: terminalParamsColumn
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Terminal custom additional parameters")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: updaterTerminalCustomClass
width: parent.width
height: 48
placeholderText: "-T udpClass"
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.updaterTerminalAdditionalParams) {
text = SettingsData.updaterTerminalAdditionalParams;
}
}
onTextEdited: {
SettingsData.set("updaterTerminalAdditionalParams", text.trim());
}
MouseArea {
anchors.fill: parent
onPressed: mouse => {
updaterTerminalCustomClass.forceActiveFocus()
mouse.accepted = false
}
}
}
}
}
}
}
StyledRect {
width: parent.width
height: runningAppsSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
visible: CompositorService.isNiri || CompositorService.isHyprland
Column {
id: runningAppsSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "apps"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Running Apps Settings")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: I18n.tr("Running Apps Only In Current Workspace")
description: I18n.tr("Show only apps running in current workspace")
checked: SettingsData.runningAppsCurrentWorkspace
onToggled: checked => {
return SettingsData.set("runningAppsCurrentWorkspace",
checked)
}
}
}
}
StyledRect {
width: parent.width
height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
visible: SettingsData.hasNamedWorkspaces()
Column {
id: workspaceIconsSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "label"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Named Workspace Icons")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: I18n.tr("Configure icons for named workspaces. Icons take priority over numbers when both are enabled.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
wrapMode: Text.WordWrap
}
Repeater {
model: SettingsData.getNamedWorkspaces()
Rectangle {
width: parent.width
height: workspaceIconRow.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5)
border.color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
border.width: 0
Row {
id: workspaceIconRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
StyledText {
text: "\"" + modelData + "\""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: 150
elide: Text.ElideRight
}
DankIconPicker {
id: iconPicker
anchors.verticalCenter: parent.verticalCenter
Component.onCompleted: {
var iconData = SettingsData.getWorkspaceNameIcon(
modelData)
if (iconData) {
setIcon(iconData.value,
iconData.type)
}
}
onIconSelected: (iconName, iconType) => {
SettingsData.setWorkspaceNameIcon(
modelData, {
"type": iconType,
"value": iconName
})
setIcon(iconName,
iconType)
}
Connections {
target: SettingsData
function onWorkspaceIconsUpdated() {
var iconData = SettingsData.getWorkspaceNameIcon(
modelData)
if (iconData) {
iconPicker.setIcon(
iconData.value,
iconData.type)
} else {
iconPicker.setIcon("", "icon")
}
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer
border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
border.width: 0
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "close"
size: 16
color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
MouseArea {
id: clearMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SettingsData.removeWorkspaceNameIcon(
modelData)
}
}
}
Item {
width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4
height: 1
}
}
}
}
}
}
StyledRect {
width: parent.width
height: notificationSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Column {
id: notificationSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "notifications"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Notification Popups")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: 0
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
DankDropdown {
width: parent.width - parent.leftPadding - parent.rightPadding
text: I18n.tr("Popup Position")
description: I18n.tr("Choose where notification popups appear on screen")
currentValue: {
if (SettingsData.notificationPopupPosition === -1) {
return "Top Center"
}
switch (SettingsData.notificationPopupPosition) {
case SettingsData.Position.Top:
return "Top Right"
case SettingsData.Position.Bottom:
return "Bottom Left"
case SettingsData.Position.Left:
return "Top Left"
case SettingsData.Position.Right:
return "Bottom Right"
default:
return "Top Right"
}
}
options: ["Top Right", "Top Left", "Top Center", "Bottom Right", "Bottom Left"]
onValueChanged: value => {
switch (value) {
case "Top Right":
SettingsData.set("notificationPopupPosition", SettingsData.Position.Top)
break
case "Top Left":
SettingsData.set("notificationPopupPosition", SettingsData.Position.Left)
break
case "Top Center":
SettingsData.set("notificationPopupPosition", -1)
break
case "Bottom Right":
SettingsData.set("notificationPopupPosition", SettingsData.Position.Right)
break
case "Bottom Left":
SettingsData.set("notificationPopupPosition", SettingsData.Position.Bottom)
break
}
SettingsData.sendTestNotifications()
}
}
}
}
}
StyledRect {
width: parent.width
height: osdRow.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 0
Row {
id: osdRow
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DankIcon {
name: "tune"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - osdToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Always Show OSD Percentage")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Display volume and brightness percentage values by default in OSD popups")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: osdToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.osdAlwaysShowValue
onToggleCompleted: checked => {
SettingsData.set("osdAlwaysShowValue", checked)
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

224
README.md
View File

@@ -1,34 +1,47 @@
# DankMaterialShell (dms)
# DankMaterialShell
<div align="center">
<a href="https://danklinux.com">
<img src="assets/danklogo2.svg" alt="DankMaterialShell Logo" width="200">
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
</a>
### A modern Wayland desktop shell
### 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://hypr.land), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop - all in one cohesive package with a gorgeous interface.
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.
## Components
## Repository Structure
DankMaterialShell combines two main components:
This is a monorepo containing both the shell interface and the core backend services:
- **[QML/UI Layer](https://github.com/AvengeMedia/DankMaterialShell)** (this repo) - All the visual components, widgets, and shell interface built with Quickshell
- **[Go Backend](https://github.com/AvengeMedia/danklinux)** - System integration, IPC, process management, and core services
---
```
DankMaterialShell/
├── quickshell/ # QML-based shell interface
│ ├── Modules/ # UI components (panels, widgets, overlays)
│ ├── Services/ # System integration (audio, network, bluetooth)
│ ├── Widgets/ # Reusable UI controls
│ └── Common/ # Shared resources and themes
├── core/ # Go backend and CLI
│ ├── cmd/ # dms CLI and dankinstall binaries
│ ├── internal/ # System integration, IPC, distro support
│ └── pkg/ # Shared packages
├── distro/ # Distribution packaging
│ ├── fedora/ # Fedora RPM specs
│ ├── debian/ # Debian packaging
│ └── nix/ # NixOS/home-manager modules
└── flake.nix # Nix flake for declarative installation
```
## See it in Action
@@ -54,166 +67,129 @@ https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
</details>
---
## Quick Install
## Installation
```bash
curl -fsSL https://install.danklinux.com | sh
```
That's it. One command installs dms and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
**[Manual Installation Guide](https://danklinux.com/docs/dankmaterialshell/installation)**
**[Manual installation guide](https://danklinux.com/docs/dankmaterialshell/installation)**
---
## What You Get
## Features
**Dynamic Theming**
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (like vscode, vscodium), and more with [matugen](https://github.com/InioX/matugen) and [dank16](https://github.com/AvengeMedia/danklinux/blob/master/internal/dank16/dank16.go).
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
**System Monitoring**
Real-time CPU, RAM, GPU metrics and temps with [dgop](https://github.com/AvengeMedia/dgop). Full process list with search and management.
Real-time CPU, RAM, GPU metrics and temperatures with [dgop](https://github.com/AvengeMedia/dgop). Process list with search and management.
**Powerful Launcher**
Spotlight-style search for apps, files (via [dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, commands - extensible with plugins.
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
**Control Center**
Network, Bluetooth, audio devices, display settings, night mode - all in one clean interface.
Unified interface for network, Bluetooth, audio devices, display settings, and night mode.
**Smart Notifications**
Notification center with grouping, rich text support, and keyboard navigation.
**Media Integration**
MPRIS player controls, calendar sync, weather widgets, clipboard history with image previews.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Complete Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, greeter support.
**Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
**Plugin System**
Endless customization with the [plugin registry](https://plugins.danklinux.com).
**TL;DR** - One shell replaces waybar, swaylock, swayidle, mako, fuzzel, polkit and everything else you normally piece together to create a linux desktop.
---
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors
DankMaterialShell works best with **[niri](https://github.com/YaLTeR/niri)**, **[Hyprland](https://hyprland.org/)**, **[sway](https://swaywm.org/)**, and **[dwl/MangoWC](https://github.com/DreamMaoMao/mangowc)**. - with full workspace switching, overview integration, and monitor management.
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.
Other Wayland compositors work too, just with a reduced feature set.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
**[Compositor configuration guide →](https://danklinux.com/docs/dankmaterialshell/compositors)**
## Command Line Interface
---
## Keybinds & IPC
Control everything from the command line or keybinds:
Control the shell from the command line or keybinds:
```bash
dms run # Start the shell
dms ipc call spotlight toggle
dms ipc call audio setvolume 50
dms ipc call wallpaper set /path/to/image.jpg
dms ipc call theme toggle
dms brightness list # List available displays
dms plugins search # Browse plugin registry
```
**[Full keybind and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)**
---
## Theming
DankMaterialShell automatically generates color schemes from your wallpaper or theme and applies them to GTK, Qt, terminals, and more.
DMS is not opinionated or forcing these themes - they are created as optional themes you can enable. You can refer to the documentation if you want to use them:
**Application theming:** [GTK, Qt, Firefox, terminals, vscode+vscodium →](https://danklinux.com/docs/dankmaterialshell/application-themes)
**Custom themes:** [Create your own color schemes →](https://danklinux.com/docs/dankmaterialshell/custom-themes)
---
## Plugins
Extend dms with the plugin system. Browse community plugins at [plugins.danklinux.com](https://plugins.danklinux.com).
**[Plugin development guide →](https://danklinux.com/docs/dankmaterialshell/plugins-overview)**
---
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
## Documentation
**Website:** [danklinux.com](https://danklinux.com)
- **Website:** [danklinux.com](https://danklinux.com)
- **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)
**Docs:** [danklinux.com/docs](https://danklinux.com/docs)
## Development
**Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
See component-specific documentation:
---
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
- **[core/](core/)** - Go backend, CLI tools, and system integration
- **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
### Building from Source
**Core + Dankinstall:**
```bash
cd core
make # Build dms CLI
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.dank-material-shell ];
}
```
## Contributing
Contributions welcome! Bug fixes, new widgets, theme improvements, or docs - it all helps.
Contributions welcome. Bug fixes, widgets, features, documentation, and plugins all help.
**Contributing Code:**
1. Fork the repository
2. Set up the development environment
3. Make your changes
2. Make your changes
3. Test thoroughly
4. Open a pull request
**Contributing Documentation:**
1. Fork the [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs) repository
2. Update files in the `docs/` folder
3. Open a pull request
### Development Setup
**Requirements:**
- `python3` - Translation management
**Git Hooks:**
Enable the pre-commit hook to check translation sync status:
```bash
git config core.hooksPath .githooks
```
**Translation Workflow**
Set POEditor credentials:
```bash
export POEDITOR_API_TOKEN="your_api_token"
export POEDITOR_PROJECT_ID="your_project_id"
```
Sync translations before committing:
```bash
python3 scripts/i18nsync.py sync
```
This script:
- Extracts strings from QML files
- Uploads changed English terms to POEditor
- Downloads updated translations from POEditor
- Stages all changes for commit
The pre-commit hook will block commits if translations are out of sync and remind you to run the sync script.
Without POEditor credentials, the hook is skipped and commits proceed normally.
Check the [issues](https://github.com/AvengeMedia/DankMaterialShell/issues) or join the community.
---
For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs).
## Credits
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
- [Ly-sec](http://github.com/ly-sec) for awesome wallpaper effects among other things from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
- [quickshell](https://quickshell.org/) - Shell framework
- [niri](https://github.com/YaLTeR/niri) - Scrolling window manager
- [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=AvengeMedia/DankMaterialShell&type=date&legend=top-left)](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
## License
MIT License - See [LICENSE](LICENSE) for details.

View File

@@ -1,58 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property list<int> values: Array(6)
property int refCount: 0
property bool cavaAvailable: false
Process {
id: cavaCheck
command: ["which", "cava"]
running: false
onExited: exitCode => {
root.cavaAvailable = exitCode === 0
}
}
Component.onCompleted: {
cavaCheck.running = true
}
Process {
id: cavaProcess
running: root.cavaAvailable && root.refCount > 0
command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`]
onRunningChanged: {
if (!running) {
root.values = Array(6).fill(0)
}
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
if (root.refCount > 0 && data.trim()) {
let points = data.split(";").map(p => {
return parseInt(p.trim(), 10)
}).filter(p => {
return !isNaN(p)
})
if (points.length >= 6) {
root.values = points.slice(0, 6)
}
}
}
}
}
}

View File

@@ -1,482 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
Singleton {
id: root
property bool isHyprland: false
property bool isNiri: false
property bool isDwl: false
property bool isSway: false
property bool isLabwc: false
property string compositor: "unknown"
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
readonly property string swaySocket: Quickshell.env("SWAYSOCK")
readonly property string labwcPid: Quickshell.env("LABWC_PID")
property bool useNiriSorting: isNiri && NiriService
property var sortedToplevels: []
property bool _sortScheduled: false
signal toplevelsChanged()
function getScreenScale(screen) {
if (!screen) return 1
if (Quickshell.env("QT_WAYLAND_FORCE_DPI") || Quickshell.env("QT_SCALE_FACTOR")) {
return screen.devicePixelRatio || 1
}
if (WlrOutputService.wlrOutputAvailable && screen) {
const wlrOutput = WlrOutputService.getOutput(screen.name)
if (wlrOutput?.enabled && wlrOutput.scale !== undefined && wlrOutput.scale > 0) {
return wlrOutput.scale
}
}
if (isNiri && screen) {
const niriScale = NiriService.displayScales[screen.name]
if (niriScale !== undefined) return niriScale
}
if (isHyprland && screen) {
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
}
if (isDwl && screen) {
const dwlScale = DwlService.getOutputScale(screen.name)
if (dwlScale !== undefined && dwlScale > 0) return dwlScale
}
return screen?.devicePixelRatio || 1
}
Timer {
id: sortDebounceTimer
interval: 100
repeat: false
onTriggered: {
_sortScheduled = false
sortedToplevels = computeSortedToplevels()
toplevelsChanged()
}
}
function scheduleSort() {
if (_sortScheduled) return
_sortScheduled = true
sortDebounceTimer.restart()
}
Connections {
target: ToplevelManager.toplevels
function onValuesChanged() { root.scheduleSort() }
}
Connections {
target: isHyprland ? Hyprland : null
enabled: isHyprland
function onRawEvent(event) {
if (event.name === "openwindow" ||
event.name === "closewindow" ||
event.name === "movewindow" ||
event.name === "movewindowv2" ||
event.name === "workspace" ||
event.name === "workspacev2" ||
event.name === "focusedmon" ||
event.name === "focusedmonv2" ||
event.name === "activewindow" ||
event.name === "activewindowv2" ||
event.name === "changefloatingmode" ||
event.name === "fullscreen" ||
event.name === "moveintogroup" ||
event.name === "moveoutofgroup") {
try {
Hyprland.refreshToplevels()
} catch(e) {}
root.scheduleSort()
}
}
}
Connections {
target: NiriService
function onWindowsChanged() { root.scheduleSort() }
}
Component.onCompleted: {
detectCompositor()
scheduleSort()
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
}
Connections {
target: DwlService
function onStateChanged() {
if (isDwl && !isHyprland && !isNiri) {
scheduleSort()
}
}
}
function computeSortedToplevels() {
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
return []
if (useNiriSorting)
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
if (isHyprland)
return sortHyprlandToplevelsSafe()
return Array.from(ToplevelManager.toplevels.values)
}
function _get(o, path, fallback) {
try {
let v = o
for (let i = 0; i < path.length; i++) {
if (v === null || v === undefined) return fallback
v = v[path[i]]
}
return (v === undefined || v === null) ? fallback : v
} catch (e) { return fallback }
}
function sortHyprlandToplevelsSafe() {
if (!Hyprland.toplevels || !Hyprland.toplevels.values) return []
const items = Array.from(Hyprland.toplevels.values)
function _get(o, path, fb) {
try {
let v = o
for (let k of path) { if (v == null) return fb; v = v[k] }
return (v == null) ? fb : v
} catch(e) { return fb }
}
let snap = []
for (let i = 0; i < items.length; i++) {
const t = items[i]
if (!t) continue
const addr = t.address || ""
if (!addr) continue
const li = t.lastIpcObject || null
const monName = _get(li, ["monitor"], null) ?? _get(t, ["monitor", "name"], "")
const monX = _get(t, ["monitor", "x"], Number.MAX_SAFE_INTEGER)
const monY = _get(t, ["monitor", "y"], Number.MAX_SAFE_INTEGER)
const wsId = _get(li, ["workspace", "id"], null) ?? _get(t, ["workspace", "id"], Number.MAX_SAFE_INTEGER)
const at = _get(li, ["at"], null)
let atX = (at !== null && at !== undefined && typeof at[0] === "number") ? at[0] : 1e9
let atY = (at !== null && at !== undefined && typeof at[1] === "number") ? at[1] : 1e9
const relX = Number.isFinite(monX) ? (atX - monX) : atX
const relY = Number.isFinite(monY) ? (atY - monY) : atY
snap.push({
monKey: String(monName),
monOrderX: Number.isFinite(monX) ? monX : Number.MAX_SAFE_INTEGER,
monOrderY: Number.isFinite(monY) ? monY : Number.MAX_SAFE_INTEGER,
wsId: (typeof wsId === "number") ? wsId : Number.MAX_SAFE_INTEGER,
x: relX,
y: relY,
title: t.title || "",
address: addr,
wayland: t.wayland
})
}
const groups = new Map()
for (const it of snap) {
const key = it.monKey + "::" + it.wsId
if (!groups.has(key)) groups.set(key, [])
groups.get(key).push(it)
}
let groupList = []
for (const [key, arr] of groups) {
const repr = arr[0]
groupList.push({
key,
monKey: repr.monKey,
monOrderX: repr.monOrderX,
monOrderY: repr.monOrderY,
wsId: repr.wsId,
items: arr
})
}
groupList.sort((a, b) => {
if (a.monOrderX !== b.monOrderX) return a.monOrderX - b.monOrderX
if (a.monOrderY !== b.monOrderY) return a.monOrderY - b.monOrderY
if (a.monKey !== b.monKey) return a.monKey.localeCompare(b.monKey)
if (a.wsId !== b.wsId) return a.wsId - b.wsId
return 0
})
const COLUMN_THRESHOLD = 48
const JITTER_Y = 6
let ordered = []
for (const g of groupList) {
const arr = g.items
const xs = arr.map(it => it.x).filter(x => Number.isFinite(x)).sort((a, b) => a - b)
let colCenters = []
if (xs.length > 0) {
for (const x of xs) {
if (colCenters.length === 0) {
colCenters.push(x)
} else {
const last = colCenters[colCenters.length - 1]
if (x - last >= COLUMN_THRESHOLD) {
colCenters.push(x)
}
}
}
} else {
colCenters = [0]
}
for (const it of arr) {
let bestCol = 0
let bestDist = Number.POSITIVE_INFINITY
for (let ci = 0; ci < colCenters.length; ci++) {
const d = Math.abs(it.x - colCenters[ci])
if (d < bestDist) {
bestDist = d
bestCol = ci
}
}
it._col = bestCol
}
arr.sort((a, b) => {
if (a._col !== b._col) return a._col - b._col
const dy = a.y - b.y
if (Math.abs(dy) > JITTER_Y) return dy
if (a.title !== b.title) return a.title.localeCompare(b.title)
if (a.address !== b.address) return a.address.localeCompare(b.address)
return 0
})
ordered.push.apply(ordered, arr)
}
return ordered.map(x => x.wayland).filter(w => w !== null && w !== undefined)
}
function filterCurrentWorkspace(toplevels, screen) {
if (useNiriSorting) return NiriService.filterCurrentWorkspace(toplevels, screen)
if (isHyprland) return filterHyprlandCurrentWorkspaceSafe(toplevels, screen)
return toplevels
}
function filterHyprlandCurrentWorkspaceSafe(toplevels, screenName) {
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) return toplevels
let currentWorkspaceId = null
try {
const hy = Array.from(Hyprland.toplevels.values)
for (const t of hy) {
const mon = _get(t, ["monitor", "name"], "")
const wsId = _get(t, ["workspace", "id"], null)
const active = !!_get(t, ["activated"], false)
if (mon === screenName && wsId !== null) {
if (active) { currentWorkspaceId = wsId; break }
if (currentWorkspaceId === null) currentWorkspaceId = wsId
}
}
if (currentWorkspaceId === null && Hyprland.workspaces) {
const wss = Array.from(Hyprland.workspaces.values)
const focusedId = _get(Hyprland, ["focusedWorkspace", "id"], null)
for (const ws of wss) {
const monName = _get(ws, ["monitor"], "")
const wsId = _get(ws, ["id"], null)
if (monName === screenName && wsId !== null) {
if (focusedId !== null && wsId === focusedId) { currentWorkspaceId = wsId; break }
if (currentWorkspaceId === null) currentWorkspaceId = wsId
}
}
}
} catch (e) {
console.warn("CompositorService: workspace snapshot failed:", e)
}
if (currentWorkspaceId === null) return toplevels
// Map wayland → wsId snapshot
let map = new Map()
try {
const hy = Array.from(Hyprland.toplevels.values)
for (const t of hy) {
const wsId = _get(t, ["workspace", "id"], null)
if (t && t.wayland && wsId !== null) map.set(t.wayland, wsId)
}
} catch (e) {}
return toplevels.filter(w => map.get(w) === currentWorkspaceId)
}
Timer {
id: compositorInitTimer
interval: 100
running: true
repeat: false
onTriggered: {
detectCompositor()
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
}
}
function detectCompositor() {
if (hyprlandSignature && hyprlandSignature.length > 0 &&
!niriSocket && !swaySocket && !labwcPid) {
isHyprland = true
isNiri = false
isDwl = false
isSway = false
isLabwc = false
compositor = "hyprland"
console.info("CompositorService: Detected Hyprland")
return
}
if (niriSocket && niriSocket.length > 0) {
Proc.runCommand("niriSocketCheck", ["test", "-S", niriSocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = true
isHyprland = false
isDwl = false
isSway = false
isLabwc = false
compositor = "niri"
console.info("CompositorService: Detected Niri with socket:", niriSocket)
NiriService.generateNiriBinds()
NiriService.generateNiriBlurrule()
}
}, 0)
return
}
if (swaySocket && swaySocket.length > 0) {
Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false
isHyprland = false
isDwl = false
isSway = true
isLabwc = false
compositor = "sway"
console.info("CompositorService: Detected Sway with socket:", swaySocket)
}
}, 0)
return
}
if (labwcPid && labwcPid.length > 0) {
isHyprland = false
isNiri = false
isDwl = false
isSway = false
isLabwc = true
compositor = "labwc"
console.info("CompositorService: Detected LabWC with PID:", labwcPid)
return
}
if (DMSService.dmsAvailable) {
Qt.callLater(checkForDwl)
} else {
isHyprland = false
isNiri = false
isDwl = false
isSway = false
isLabwc = false
compositor = "unknown"
console.warn("CompositorService: No compositor detected")
}
}
Connections {
target: DMSService
function onCapabilitiesReceived() {
if (!isHyprland && !isNiri && !isDwl && !isLabwc) {
checkForDwl()
}
}
}
function checkForDwl() {
if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) {
isHyprland = false
isNiri = false
isDwl = true
isSway = false
isLabwc = false
compositor = "dwl"
console.info("CompositorService: Detected DWL via DMS capability")
}
}
function powerOffMonitors() {
if (isNiri) return NiriService.powerOffMonitors()
if (isHyprland) return Hyprland.dispatch("dpms off")
if (isDwl) return _dwlPowerOffMonitors()
if (isSway) { try { I3.dispatch("output * dpms off") } catch(_){} return }
console.warn("CompositorService: Cannot power off monitors, unknown compositor")
}
function powerOnMonitors() {
if (isNiri) return NiriService.powerOnMonitors()
if (isHyprland) return Hyprland.dispatch("dpms on")
if (isDwl) return _dwlPowerOnMonitors()
if (isSway) { try { I3.dispatch("output * dpms on") } catch(_){} return }
console.warn("CompositorService: Cannot power on monitors, unknown compositor")
}
function _dwlPowerOffMonitors() {
if (!Quickshell.screens || Quickshell.screens.length === 0) {
console.warn("CompositorService: No screens available for DWL power off")
return
}
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i]
if (screen && screen.name) {
Quickshell.execDetached(["mmsg", "-d", "disable_monitor," + screen.name])
}
}
}
function _dwlPowerOnMonitors() {
if (!Quickshell.screens || Quickshell.screens.length === 0) {
console.warn("CompositorService: No screens available for DWL power on")
return
}
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i]
if (screen && screen.name) {
Quickshell.execDetached(["mmsg", "-d", "enable_monitor," + screen.name])
}
}
}
}

View File

@@ -1,371 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property int refCount: 0
property var printerNames: []
property var printers: []
property string selectedPrinter: ""
property bool cupsAvailable: false
property bool stateInitialized: false
signal cupsStateUpdate
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
checkDMSCapabilities()
}
}
Connections {
target: DMSService
function onConnectionStateChanged() {
if (DMSService.isConnected) {
checkDMSCapabilities()
}
}
}
Connections {
target: DMSService
enabled: DMSService.isConnected
function onCupsStateUpdate(data) {
console.log("CupsService: Subscription update received")
getState()
}
function onCapabilitiesChanged() {
checkDMSCapabilities()
}
}
function checkDMSCapabilities() {
if (!DMSService.isConnected) {
return
}
if (DMSService.capabilities.length === 0) {
return
}
cupsAvailable = DMSService.capabilities.includes("cups")
if (cupsAvailable && !stateInitialized) {
stateInitialized = true
getState()
}
}
function getState() {
if (!cupsAvailable)
return
DMSService.sendRequest("cups.getPrinters", null, response => {
if (response.result) {
updatePrinters(response.result)
fetchAllJobs()
}
})
}
function updatePrinters(printersData) {
printerNames = printersData.map(p => p.name)
let printersObj = {}
for (var i = 0; i < printersData.length; i++) {
let printer = printersData[i]
printersObj[printer.name] = {
"state": printer.state,
"stateReason": printer.stateReason,
"jobs": []
}
}
printers = printersObj
if (printerNames.length > 0) {
if (selectedPrinter.length > 0) {
if (!printerNames.includes(selectedPrinter)) {
selectedPrinter = printerNames[0]
}
} else {
selectedPrinter = printerNames[0]
}
}
}
function fetchAllJobs() {
for (var i = 0; i < printerNames.length; i++) {
fetchJobsForPrinter(printerNames[i])
}
}
function fetchJobsForPrinter(printerName) {
const params = {
"printerName": printerName
}
DMSService.sendRequest("cups.getJobs", params, response => {
if (response.result && printers[printerName]) {
let updatedPrinters = Object.assign({}, printers)
updatedPrinters[printerName].jobs = response.result
printers = updatedPrinters
}
})
}
function getSelectedPrinter() {
return selectedPrinter
}
function setSelectedPrinter(printerName) {
if (printerNames.length > 0) {
if (printerNames.includes(printerName)) {
selectedPrinter = printerName
} else {
selectedPrinter = printerNames[0]
}
}
}
function getPrintersNum() {
if (!cupsAvailable)
return 0
return printerNames.length
}
function getPrintersNames() {
if (!cupsAvailable)
return []
return printerNames
}
function getTotalJobsNum() {
if (!cupsAvailable)
return 0
var result = 0
for (var i = 0; i < printerNames.length; i++) {
var printerName = printerNames[i]
if (printers[printerName] && printers[printerName].jobs) {
result += printers[printerName].jobs.length
}
}
return result
}
function getCurrentPrinterState() {
if (!cupsAvailable || !selectedPrinter)
return ""
var printer = printers[selectedPrinter]
return printer.state
}
function getCurrentPrinterStatePrettyShort() {
if (!cupsAvailable || !selectedPrinter)
return ""
var printer = printers[selectedPrinter]
return getPrinterStateTranslation(printer.state) + " (" + getPrinterStateReasonTranslation(printer.stateReason) + ")"
}
function getCurrentPrinterStatePretty() {
if (!cupsAvailable || !selectedPrinter)
return ""
var printer = printers[selectedPrinter]
return getPrinterStateTranslation(printer.state) + " (" + I18n.tr("Reason") + ": " + getPrinterStateReasonTranslation(printer.stateReason) + ")"
}
function getCurrentPrinterJobs() {
if (!cupsAvailable || !selectedPrinter)
return []
return getJobs(selectedPrinter)
}
function getJobs(printerName) {
if (!cupsAvailable)
return ""
var printer = printers[printerName]
return printer.jobs
}
function getJobsNum(printerName) {
if (!cupsAvailable)
return 0
var printer = printers[printerName]
return printer.jobs.length
}
function pausePrinter(printerName) {
if (!cupsAvailable)
return
const params = {
"printerName": printerName
}
DMSService.sendRequest("cups.pausePrinter", params, response => {
if (response.error) {
ToastService.showError(I18n.tr("Failed to pause printer") + " - " + response.error)
} else {
getState()
}
})
}
function resumePrinter(printerName) {
if (!cupsAvailable)
return
const params = {
"printerName": printerName
}
DMSService.sendRequest("cups.resumePrinter", params, response => {
if (response.error) {
ToastService.showError(I18n.tr("Failed to resume printer") + " - " + response.error)
} else {
getState()
}
})
}
function cancelJob(printerName, jobID) {
if (!cupsAvailable)
return
const params = {
"printerName": printerName,
"jobID": jobID
}
DMSService.sendRequest("cups.cancelJob", params, response => {
if (response.error) {
ToastService.showError(I18n.tr("Failed to cancel selected job") + " - " + response.error)
} else {
fetchJobsForPrinter(printerName)
}
})
}
function purgeJobs(printerName) {
if (!cupsAvailable)
return
const params = {
"printerName": printerName
}
DMSService.sendRequest("cups.purgeJobs", params, response => {
if (response.error) {
ToastService.showError(I18n.tr("Failed to cancel all jobs") + " - " + response.error)
} else {
fetchJobsForPrinter(printerName)
}
})
}
readonly property var states: ({
"idle": I18n.tr("Idle"),
"processing": I18n.tr("Processing"),
"stopped": I18n.tr("Stopped")
})
readonly property var reasonsGeneral: ({
"none": I18n.tr("None"),
"other": I18n.tr("Other")
})
readonly property var reasonsSupplies: ({
"toner-low": I18n.tr("Toner Low"),
"toner-empty": I18n.tr("Toner Empty"),
"marker-supply-low": I18n.tr("Marker Supply Low"),
"marker-supply-empty": I18n.tr("Marker Supply Empty"),
"marker-waste-almost-full": I18n.tr("Marker Waste Almost Full"),
"marker-waste-full": I18n.tr("Marker Waste Full")
})
readonly property var reasonsMedia: ({
"media-low": I18n.tr("Media Low"),
"media-empty": I18n.tr("Media Empty"),
"media-needed": I18n.tr("Media Needed"),
"media-jam": I18n.tr("Media Jam")
})
readonly property var reasonsParts: ({
"cover-open": I18n.tr("Cover Open"),
"door-open": I18n.tr("Door Open"),
"interlock-open": I18n.tr("Interlock Open"),
"output-tray-missing": I18n.tr("Output Tray Missing"),
"output-area-almost-full": I18n.tr("Output Area Almost Full"),
"output-area-full": I18n.tr("Output Area Full")
})
readonly property var reasonsErrors: ({
"paused": I18n.tr("Paused"),
"shutdown": I18n.tr("Shutdown"),
"connecting-to-device": I18n.tr("Connecting to Device"),
"timed-out": I18n.tr("Timed Out"),
"stopping": I18n.tr("Stopping"),
"stopped-partly": I18n.tr("Stopped Partly")
})
readonly property var reasonsService: ({
"spool-area-full": I18n.tr("Spool Area Full"),
"cups-missing-filter-warning": I18n.tr("CUPS Missing Filter Warning"),
"cups-insecure-filter-warning": I18n.tr("CUPS Insecure Filter Warning")
})
readonly property var reasonsConnectivity: ({
"offline-report": I18n.tr("Offline Report"),
"moving-to-paused": I18n.tr("Moving to Paused")
})
readonly property var severitySuffixes: ({
"-error": I18n.tr("Error"),
"-warning": I18n.tr("Warning"),
"-report": I18n.tr("Report")
})
function getPrinterStateTranslation(state) {
return states[state] || state
}
function getPrinterStateReasonTranslation(reason) {
let allReasons = Object.assign({}, reasonsGeneral, reasonsSupplies, reasonsMedia, reasonsParts, reasonsErrors, reasonsService, reasonsConnectivity)
let basReason = reason
let suffix = ""
for (let s in severitySuffixes) {
if (reason.endsWith(s)) {
basReason = reason.slice(0, -s.length)
suffix = severitySuffixes[s]
break
}
}
let translation = allReasons[basReason] || basReason
return suffix ? translation + " (" + suffix + ")" : translation
}
}

View File

@@ -1,536 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property bool dmsAvailable: false
property var capabilities: []
property int apiVersion: 0
readonly property int expectedApiVersion: 1
property var availablePlugins: []
property var installedPlugins: []
property bool isConnected: false
property bool isConnecting: false
property bool subscribeConnected: false
readonly property bool forceExtWorkspace: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
property var pendingRequests: ({})
property int requestIdCounter: 0
property bool shownOutdatedError: false
property string updateCommand: "dms update"
property bool checkingUpdateCommand: false
signal pluginsListReceived(var plugins)
signal installedPluginsReceived(var plugins)
signal searchResultsReceived(var plugins)
signal operationSuccess(string message)
signal operationError(string error)
signal connectionStateChanged
signal networkStateUpdate(var data)
signal cupsStateUpdate(var data)
signal loginctlStateUpdate(var data)
signal loginctlEvent(var event)
signal capabilitiesReceived
signal credentialsRequest(var data)
signal bluetoothPairingRequest(var data)
signal dwlStateUpdate(var data)
signal brightnessStateUpdate(var data)
signal brightnessDeviceUpdate(var device)
signal extWorkspaceStateUpdate(var data)
signal wlrOutputStateUpdate(var data)
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput"]
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
detectUpdateCommand()
}
}
function detectUpdateCommand() {
checkingUpdateCommand = true
checkAurHelper.running = true
}
function startSocketConnection() {
if (socketPath && socketPath.length > 0) {
testProcess.running = true
}
}
Process {
id: checkAurHelper
command: ["sh", "-c", "command -v paru || command -v yay"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const helper = text.trim()
if (helper.includes("paru")) {
checkDmsPackage.helper = "paru"
checkDmsPackage.running = true
} else if (helper.includes("yay")) {
checkDmsPackage.helper = "yay"
checkDmsPackage.running = true
} else {
updateCommand = "dms update"
checkingUpdateCommand = false
startSocketConnection()
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
updateCommand = "dms update"
checkingUpdateCommand = false
startSocketConnection()
}
}
}
Process {
id: checkDmsPackage
property string helper: ""
command: ["sh", "-c", "pacman -Qi dms-shell-git 2>/dev/null || pacman -Qi dms-shell-bin 2>/dev/null"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("dms-shell-git")) {
updateCommand = checkDmsPackage.helper + " -S dms-shell-git"
} else if (text.includes("dms-shell-bin")) {
updateCommand = checkDmsPackage.helper + " -S dms-shell-bin"
} else {
updateCommand = "dms update"
}
checkingUpdateCommand = false
startSocketConnection()
}
}
onExited: exitCode => {
if (exitCode !== 0) {
updateCommand = "dms update"
checkingUpdateCommand = false
startSocketConnection()
}
}
}
Process {
id: testProcess
command: ["test", "-S", root.socketPath]
onExited: exitCode => {
if (exitCode === 0) {
root.dmsAvailable = true
connectSocket()
} else {
root.dmsAvailable = false
}
}
}
function connectSocket() {
if (!dmsAvailable || isConnected || isConnecting) {
return
}
isConnecting = true
requestSocket.connected = true
}
DankSocket {
id: requestSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
if (connected) {
root.isConnected = true
root.isConnecting = false
root.connectionStateChanged()
subscribeSocket.connected = true
} else {
root.isConnected = false
root.isConnecting = false
root.apiVersion = 0
root.capabilities = []
root.connectionStateChanged()
}
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0) {
return
}
console.log("DMSService: Request socket <<", line)
try {
const response = JSON.parse(line)
handleResponse(response)
} catch (e) {
console.warn("DMSService: Failed to parse request response:", line, e)
}
}
}
}
DankSocket {
id: subscribeSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
root.subscribeConnected = connected
if (connected) {
sendSubscribeRequest()
}
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0) {
return
}
console.log("DMSService: Subscribe socket <<", line)
try {
const response = JSON.parse(line)
handleSubscriptionEvent(response)
} catch (e) {
console.warn("DMSService: Failed to parse subscription event:", line, e)
}
}
}
}
function sendSubscribeRequest() {
const request = {
"method": "subscribe"
}
if (activeSubscriptions.length > 0) {
request.params = {
"services": activeSubscriptions
}
console.log("DMSService: Subscribing to services:", JSON.stringify(activeSubscriptions))
} else {
console.log("DMSService: Subscribing to all services")
}
subscribeSocket.send(request)
}
function subscribe(services) {
if (!Array.isArray(services)) {
services = [services]
}
activeSubscriptions = services
if (subscribeConnected) {
subscribeSocket.connected = false
Qt.callLater(() => {
subscribeSocket.connected = true
})
}
}
function addSubscription(service) {
if (activeSubscriptions.includes("all")) {
console.warn("DMSService: Cannot add specific subscription when subscribed to 'all'")
return
}
if (!activeSubscriptions.includes(service)) {
const newSubs = [...activeSubscriptions, service]
subscribe(newSubs)
}
}
function removeSubscription(service) {
if (activeSubscriptions.includes("all")) {
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace"]
const filtered = allServices.filter(s => s !== service)
subscribe(filtered)
} else {
const filtered = activeSubscriptions.filter(s => s !== service)
if (filtered.length === 0) {
console.warn("DMSService: Cannot remove last subscription")
return
}
subscribe(filtered)
}
}
function subscribeAll() {
subscribe(["all"])
}
function subscribeAllExcept(excludeServices) {
if (!Array.isArray(excludeServices)) {
excludeServices = [excludeServices]
}
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace"]
const filtered = allServices.filter(s => !excludeServices.includes(s))
subscribe(filtered)
}
function handleSubscriptionEvent(response) {
if (response.error) {
if (response.error.includes("unknown method") && response.error.includes("subscribe")) {
if (!shownOutdatedError) {
console.error("DMSService: Server does not support subscribe method")
ToastService.showError(I18n.tr("DMS out of date"), I18n.tr("To update, run the following command:"), updateCommand)
shownOutdatedError = true
}
}
return
}
if (!response.result) {
return
}
const service = response.result.service
const data = response.result.data
if (service === "server") {
apiVersion = data.apiVersion || 0
capabilities = data.capabilities || []
console.info("DMSService: Connected (API v" + apiVersion + ") -", JSON.stringify(capabilities))
if (apiVersion < expectedApiVersion) {
ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")")
}
capabilitiesReceived()
} else if (service === "network") {
networkStateUpdate(data)
} else if (service === "network.credentials") {
credentialsRequest(data)
} else if (service === "loginctl") {
if (data.event) {
loginctlEvent(data)
} else {
loginctlStateUpdate(data)
}
} else if (service === "bluetooth.pairing") {
bluetoothPairingRequest(data)
} else if (service === "cups") {
cupsStateUpdate(data)
} else if (service === "dwl") {
dwlStateUpdate(data)
} else if (service === "brightness") {
brightnessStateUpdate(data)
} else if (service === "brightness.update") {
if (data.device) {
brightnessDeviceUpdate(data.device)
}
} else if (service === "extworkspace") {
extWorkspaceStateUpdate(data)
} else if (service === "wlroutput") {
wlrOutputStateUpdate(data)
}
}
function sendRequest(method, params, callback) {
if (!isConnected) {
console.warn("DMSService.sendRequest: Not connected, method:", method)
if (callback) {
callback({
"error": "not connected to DMS socket"
})
}
return
}
requestIdCounter++
const id = Date.now() + requestIdCounter
const request = {
"id": id,
"method": method
}
if (params) {
request.params = params
}
if (callback) {
pendingRequests[id] = callback
}
console.log("DMSService.sendRequest: Sending request id=" + id + " method=" + method)
requestSocket.send(request)
}
function handleResponse(response) {
const callback = pendingRequests[response.id]
if (callback) {
delete pendingRequests[response.id]
callback(response)
}
}
function ping(callback) {
sendRequest("ping", null, callback)
}
function listPlugins(callback) {
sendRequest("plugins.list", null, response => {
if (response.result) {
availablePlugins = response.result
pluginsListReceived(response.result)
}
if (callback) {
callback(response)
}
})
}
function listInstalled(callback) {
sendRequest("plugins.listInstalled", null, response => {
if (response.result) {
installedPlugins = response.result
installedPluginsReceived(response.result)
}
if (callback) {
callback(response)
}
})
}
function search(query, category, compositor, capability, callback) {
const params = {
"query": query
}
if (category) {
params.category = category
}
if (compositor) {
params.compositor = compositor
}
if (capability) {
params.capability = capability
}
sendRequest("plugins.search", params, response => {
if (response.result) {
searchResultsReceived(response.result)
}
if (callback) {
callback(response)
}
})
}
function install(pluginName, callback) {
sendRequest("plugins.install", {
"name": pluginName
}, response => {
if (callback) {
callback(response)
}
if (!response.error) {
listInstalled()
}
})
}
function uninstall(pluginName, callback) {
sendRequest("plugins.uninstall", {
"name": pluginName
}, response => {
if (callback) {
callback(response)
}
if (!response.error) {
listInstalled()
}
})
}
function update(pluginName, callback) {
sendRequest("plugins.update", {
"name": pluginName
}, response => {
if (callback) {
callback(response)
}
if (!response.error) {
listInstalled()
}
})
}
function lockSession(callback) {
sendRequest("loginctl.lock", null, callback)
}
function unlockSession(callback) {
sendRequest("loginctl.unlock", null, callback)
}
function bluetoothPair(devicePath, callback) {
sendRequest("bluetooth.pair", {
"device": devicePath
}, callback)
}
function bluetoothConnect(devicePath, callback) {
sendRequest("bluetooth.connect", {
"device": devicePath
}, callback)
}
function bluetoothDisconnect(devicePath, callback) {
sendRequest("bluetooth.disconnect", {
"device": devicePath
}, callback)
}
function bluetoothRemove(devicePath, callback) {
sendRequest("bluetooth.remove", {
"device": devicePath
}, callback)
}
function bluetoothTrust(devicePath, callback) {
sendRequest("bluetooth.trust", {
"device": devicePath
}, callback)
}
function bluetoothSubmitPairing(token, secrets, accept, callback) {
sendRequest("bluetooth.pairing.submit", {
"token": token,
"secrets": secrets,
"accept": accept
}, callback)
}
function bluetoothCancelPairing(token, callback) {
sendRequest("bluetooth.pairing.cancel", {
"token": token
}, callback)
}
}

View File

@@ -1,936 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property bool brightnessAvailable: devices.length > 0
property var devices: []
property var deviceBrightness: ({})
property var deviceBrightnessUserSet: ({})
property var deviceMaxCache: ({})
property int brightnessVersion: 0
property string currentDevice: ""
property string lastIpcDevice: ""
property int brightnessLevel: {
brightnessVersion
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
if (!deviceToUse) {
return 50
}
return getDeviceBrightness(deviceToUse)
}
property int maxBrightness: 100
property bool brightnessInitialized: false
signal brightnessChanged(bool showOsd)
signal deviceSwitched
property bool nightModeActive: nightModeEnabled
property bool nightModeEnabled: false
property bool automationAvailable: false
property bool gammaControlAvailable: false
function updateSingleDevice(device) {
const deviceIndex = devices.findIndex(d => d.id === device.id)
if (deviceIndex !== -1) {
const newDevices = [...devices]
const existingDevice = devices[deviceIndex]
const cachedMax = deviceMaxCache[device.id]
let displayMax = cachedMax || (device.class === "ddc" ? device.max : 100)
if (displayMax > 0 && !cachedMax) {
const newCache = Object.assign({}, deviceMaxCache)
newCache[device.id] = displayMax
deviceMaxCache = newCache
}
newDevices[deviceIndex] = {
"id": device.id,
"name": device.id,
"class": device.class,
"current": device.current,
"percentage": device.currentPercent,
"max": device.max,
"backend": device.backend,
"displayMax": displayMax
}
devices = newDevices
}
const isExponential = SessionData.getBrightnessExponential(device.id)
const userSetValue = deviceBrightnessUserSet[device.id]
let displayValue = device.currentPercent
if (isExponential) {
if (userSetValue !== undefined) {
displayValue = userSetValue
} else {
displayValue = linearToExponential(device.currentPercent, device.id)
}
}
const newBrightness = Object.assign({}, deviceBrightness)
newBrightness[device.id] = displayValue
deviceBrightness = newBrightness
brightnessVersion++
}
function updateFromBrightnessState(state) {
if (!state || !state.devices) {
return
}
const newMaxCache = Object.assign({}, deviceMaxCache)
devices = state.devices.map(d => {
const cachedMax = deviceMaxCache[d.id]
let displayMax = cachedMax || (d.class === "ddc" ? d.max : 100)
if (displayMax > 0 && !cachedMax) {
newMaxCache[d.id] = displayMax
}
return {
"id": d.id,
"name": d.id,
"class": d.class,
"current": d.current,
"percentage": d.currentPercent,
"max": d.max,
"backend": d.backend,
"displayMax": displayMax
}
})
deviceMaxCache = newMaxCache
const newBrightness = {}
for (const device of state.devices) {
const isExponential = SessionData.getBrightnessExponential(device.id)
const userSetValue = deviceBrightnessUserSet[device.id]
if (isExponential) {
if (userSetValue !== undefined) {
newBrightness[device.id] = userSetValue
} else {
newBrightness[device.id] = linearToExponential(device.currentPercent, device.id)
}
} else {
newBrightness[device.id] = device.currentPercent
}
}
deviceBrightness = newBrightness
brightnessVersion++
brightnessAvailable = devices.length > 0
if (devices.length > 0 && !currentDevice) {
const lastDevice = SessionData.lastBrightnessDevice || ""
const deviceExists = devices.some(d => d.id === lastDevice)
if (deviceExists) {
setCurrentDevice(lastDevice, false)
} else {
const backlight = devices.find(d => d.class === "backlight")
const nonKbdDevice = devices.find(d => !d.id.includes("kbd"))
const defaultDevice = backlight || nonKbdDevice || devices[0]
setCurrentDevice(defaultDevice.id, false)
}
}
if (!brightnessInitialized) {
brightnessInitialized = true
}
}
function setBrightness(percentage, device, suppressOsd) {
const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice())
if (!actualDevice) {
console.warn("DisplayService: No device selected for brightness change")
return
}
const deviceInfo = getCurrentDeviceInfoByName(actualDevice)
const isExponential = SessionData.getBrightnessExponential(actualDevice)
let minValue = 0
let maxValue = 100
if (isExponential) {
minValue = 1
maxValue = 100
} else {
minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
maxValue = deviceInfo?.displayMax || 100
}
if (maxValue <= 0) {
console.warn("DisplayService: Invalid max value for device", actualDevice, "- skipping brightness change")
return
}
const clampedValue = Math.max(minValue, Math.min(maxValue, percentage))
if (!DMSService.isConnected) {
console.warn("DisplayService: Not connected to DMS")
return
}
const newBrightness = Object.assign({}, deviceBrightness)
newBrightness[actualDevice] = clampedValue
deviceBrightness = newBrightness
brightnessVersion++
if (isExponential) {
const newUserSet = Object.assign({}, deviceBrightnessUserSet)
newUserSet[actualDevice] = clampedValue
deviceBrightnessUserSet = newUserSet
SessionData.setBrightnessUserSetValue(actualDevice, clampedValue)
}
if (!suppressOsd) {
brightnessChanged(true)
}
const params = {
"device": actualDevice,
"percent": clampedValue
}
if (isExponential) {
params.exponential = true
params.exponent = SessionData.getBrightnessExponent(actualDevice)
}
DMSService.sendRequest("brightness.setBrightness", params, response => {
if (response.error) {
console.error("DisplayService: Failed to set brightness:", response.error)
ToastService.showError("Failed to set brightness: " + response.error, "", "", "brightness")
} else {
ToastService.dismissCategory("brightness")
}
})
}
function setCurrentDevice(deviceName, saveToSession = false) {
if (currentDevice === deviceName) {
return
}
currentDevice = deviceName
lastIpcDevice = deviceName
if (saveToSession) {
SessionData.setLastBrightnessDevice(deviceName)
}
deviceSwitched()
}
function getDeviceBrightness(deviceName) {
if (!deviceName) {
return 50
}
if (deviceName in deviceBrightness) {
return deviceBrightness[deviceName]
}
return 50
}
function linearToExponential(linearPercent, deviceName) {
const exponent = SessionData.getBrightnessExponent(deviceName)
const hardwarePercent = linearPercent / 100.0
const normalizedPercent = Math.pow(hardwarePercent, 1.0 / exponent)
return Math.round(normalizedPercent * 100.0)
}
function getDefaultDevice() {
for (const device of devices) {
if (device.class === "backlight") {
return device.id
}
}
return devices.length > 0 ? devices[0].id : ""
}
function getCurrentDeviceInfo() {
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
if (!deviceToUse) {
return null
}
for (const device of devices) {
if (device.id === deviceToUse) {
return device
}
}
return null
}
function isCurrentDeviceReady() {
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
return deviceToUse !== ""
}
function getCurrentDeviceInfoByName(deviceName) {
if (!deviceName) {
return null
}
for (const device of devices) {
if (device.id === deviceName) {
return device
}
}
return null
}
function getDeviceMax(deviceName) {
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
if (!deviceInfo) {
return 100
}
return deviceInfo.displayMax || 100
}
// Night Mode Functions - Simplified
function enableNightMode() {
if (!gammaControlAvailable) {
ToastService.showWarning("Night mode failed: DMS gamma control not available")
return
}
nightModeEnabled = true
SessionData.setNightModeEnabled(true)
DMSService.sendRequest("wayland.gamma.setEnabled", {
"enabled": true
}, response => {
if (response.error) {
console.error("DisplayService: Failed to enable gamma control:", response.error)
ToastService.showError("Failed to enable night mode: " + response.error, "", "", "night-mode")
nightModeEnabled = false
SessionData.setNightModeEnabled(false)
return
}
ToastService.dismissCategory("night-mode")
if (SessionData.nightModeAutoEnabled) {
startAutomation()
} else {
applyNightModeDirectly()
}
})
}
function disableNightMode() {
nightModeEnabled = false
SessionData.setNightModeEnabled(false)
if (!gammaControlAvailable) {
return
}
DMSService.sendRequest("wayland.gamma.setEnabled", {
"enabled": false
}, response => {
if (response.error) {
console.error("DisplayService: Failed to disable gamma control:", response.error)
ToastService.showError("Failed to disable night mode: " + response.error, "", "", "night-mode")
} else {
ToastService.dismissCategory("night-mode")
}
})
}
function toggleNightMode() {
if (nightModeEnabled) {
disableNightMode()
} else {
enableNightMode()
}
}
function applyNightModeDirectly() {
const temperature = SessionData.nightModeTemperature || 4000
DMSService.sendRequest("wayland.gamma.setManualTimes", {
"sunrise": null,
"sunset": null
}, response => {
if (response.error) {
console.error("DisplayService: Failed to clear manual times:", response.error)
return
}
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false
}, response => {
if (response.error) {
console.error("DisplayService: Failed to disable IP location:", response.error)
return
}
DMSService.sendRequest("wayland.gamma.setTemperature", {
"low": temperature,
"high": 6500
}, response => {
if (response.error) {
console.error("DisplayService: Failed to set temperature:", response.error)
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
} else {
ToastService.dismissCategory("night-mode")
}
})
})
})
}
function startAutomation() {
if (!automationAvailable) {
return
}
const mode = SessionData.nightModeAutoMode || "time"
switch (mode) {
case "time":
startTimeBasedMode()
break
case "location":
startLocationBasedMode()
break
}
}
function startTimeBasedMode() {
const temperature = SessionData.nightModeTemperature || 4000
const highTemp = SessionData.nightModeHighTemperature || 6500
const sunriseHour = SessionData.nightModeEndHour
const sunriseMinute = SessionData.nightModeEndMinute
const sunsetHour = SessionData.nightModeStartHour
const sunsetMinute = SessionData.nightModeStartMinute
const sunrise = `${String(sunriseHour).padStart(2, '0')}:${String(sunriseMinute).padStart(2, '0')}`
const sunset = `${String(sunsetHour).padStart(2, '0')}:${String(sunsetMinute).padStart(2, '0')}`
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false
}, response => {
if (response.error) {
console.error("DisplayService: Failed to disable IP location:", response.error)
return
}
DMSService.sendRequest("wayland.gamma.setTemperature", {
"low": temperature,
"high": highTemp
}, response => {
if (response.error) {
console.error("DisplayService: Failed to set temperature:", response.error)
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
return
}
DMSService.sendRequest("wayland.gamma.setManualTimes", {
"sunrise": sunrise,
"sunset": sunset
}, response => {
if (response.error) {
console.error("DisplayService: Failed to set manual times:", response.error)
ToastService.showError("Failed to set night mode schedule: " + response.error, "", "", "night-mode")
} else {
ToastService.dismissCategory("night-mode")
}
})
})
})
}
function startLocationBasedMode() {
const temperature = SessionData.nightModeTemperature || 4000
const highTemp = SessionData.nightModeHighTemperature || 6500
DMSService.sendRequest("wayland.gamma.setManualTimes", {
"sunrise": null,
"sunset": null
}, response => {
if (response.error) {
console.error("DisplayService: Failed to clear manual times:", response.error)
return
}
DMSService.sendRequest("wayland.gamma.setTemperature", {
"low": temperature,
"high": highTemp
}, response => {
if (response.error) {
console.error("DisplayService: Failed to set temperature:", response.error)
ToastService.showError("Failed to set night mode temperature: " + response.error, "", "", "night-mode")
return
}
if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": true
}, response => {
if (response.error) {
console.error("DisplayService: Failed to enable IP location:", response.error)
ToastService.showError("Failed to enable IP location: " + response.error, "", "", "night-mode")
} else {
ToastService.dismissCategory("night-mode")
}
})
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false
}, response => {
if (response.error) {
console.error("DisplayService: Failed to disable IP location:", response.error)
return
}
DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude,
"longitude": SessionData.longitude
}, response => {
if (response.error) {
console.error("DisplayService: Failed to set location:", response.error)
ToastService.showError("Failed to set night mode location: " + response.error, "", "", "night-mode")
} else {
ToastService.dismissCategory("night-mode")
}
})
})
} else {
console.warn("DisplayService: Location mode selected but no coordinates set and IP location disabled")
}
})
})
}
function setNightModeAutomationMode(mode) {
SessionData.setNightModeAutoMode(mode)
}
function evaluateNightMode() {
if (!nightModeEnabled) {
return
}
if (SessionData.nightModeAutoEnabled) {
restartTimer.nextAction = "automation"
restartTimer.start()
} else {
restartTimer.nextAction = "direct"
restartTimer.start()
}
}
function checkGammaControlAvailability() {
if (!DMSService.isConnected) {
return
}
if (DMSService.apiVersion < 6) {
gammaControlAvailable = false
automationAvailable = false
return
}
if (!DMSService.capabilities.includes("gamma")) {
gammaControlAvailable = false
automationAvailable = false
return
}
DMSService.sendRequest("wayland.gamma.getState", null, response => {
if (response.error) {
gammaControlAvailable = false
automationAvailable = false
console.error("DisplayService: Gamma control not available:", response.error)
} else {
gammaControlAvailable = true
automationAvailable = true
if (nightModeEnabled) {
DMSService.sendRequest("wayland.gamma.setEnabled", {
"enabled": true
}, enableResponse => {
if (enableResponse.error) {
console.error("DisplayService: Failed to enable gamma control on startup:", enableResponse.error)
return
}
if (SessionData.nightModeAutoEnabled) {
startAutomation()
} else {
applyNightModeDirectly()
}
})
}
}
})
}
Timer {
id: restartTimer
property string nextAction: ""
interval: 100
repeat: false
onTriggered: {
if (nextAction === "automation") {
startAutomation()
} else if (nextAction === "direct") {
applyNightModeDirectly()
}
nextAction = ""
}
}
function rescanDevices() {
if (!DMSService.isConnected) {
return
}
DMSService.sendRequest("brightness.rescan", null, response => {
if (response.error) {
console.error("DisplayService: Failed to rescan brightness devices:", response.error)
}
})
}
function updateDeviceBrightnessDisplay(deviceName) {
brightnessVersion++
brightnessChanged()
}
Component.onCompleted: {
nightModeEnabled = SessionData.nightModeEnabled
deviceBrightnessUserSet = Object.assign({}, SessionData.brightnessUserSetValues)
if (DMSService.isConnected) {
checkGammaControlAvailability()
}
}
Connections {
target: Quickshell
function onScreensChanged() {
rescanDevices()
}
}
Connections {
target: DMSService
function onConnectionStateChanged() {
if (DMSService.isConnected) {
checkGammaControlAvailability()
} else {
brightnessAvailable = false
gammaControlAvailable = false
automationAvailable = false
}
}
function onCapabilitiesReceived() {
checkGammaControlAvailability()
}
function onBrightnessStateUpdate(data) {
updateFromBrightnessState(data)
}
function onBrightnessDeviceUpdate(device) {
updateSingleDevice(device)
}
}
// Session Data Connections
Connections {
target: SessionData
function onNightModeEnabledChanged() {
nightModeEnabled = SessionData.nightModeEnabled
evaluateNightMode()
}
function onNightModeAutoEnabledChanged() {
evaluateNightMode()
}
function onNightModeAutoModeChanged() {
evaluateNightMode()
}
function onNightModeStartHourChanged() {
evaluateNightMode()
}
function onNightModeStartMinuteChanged() {
evaluateNightMode()
}
function onNightModeEndHourChanged() {
evaluateNightMode()
}
function onNightModeEndMinuteChanged() {
evaluateNightMode()
}
function onNightModeTemperatureChanged() {
evaluateNightMode()
}
function onNightModeHighTemperatureChanged() {
evaluateNightMode()
}
function onLatitudeChanged() {
evaluateNightMode()
}
function onLongitudeChanged() {
evaluateNightMode()
}
function onNightModeUseIPLocationChanged() {
evaluateNightMode()
}
}
// IPC Handler for external control
IpcHandler {
function set(percentage: string, device: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available"
}
const value = parseInt(percentage)
if (isNaN(value)) {
return "Invalid brightness value: " + percentage
}
const targetDevice = device || ""
if (targetDevice && !root.devices.some(d => d.id === targetDevice)) {
return "Device not found: " + targetDevice
}
const deviceInfo = targetDevice ? root.getCurrentDeviceInfoByName(targetDevice) : null
const minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
const clampedValue = Math.max(minValue, Math.min(100, value))
root.lastIpcDevice = targetDevice
if (targetDevice && targetDevice !== root.currentDevice) {
root.setCurrentDevice(targetDevice, false)
}
root.setBrightness(clampedValue, targetDevice, false)
if (targetDevice) {
return "Brightness set to " + clampedValue + "% on " + targetDevice
} else {
return "Brightness set to " + clampedValue + "%"
}
}
function increment(step: string, device: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available"
}
const targetDevice = device || ""
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
return "Device not found: " + actualDevice
}
const stepValue = parseInt(step || "5")
root.lastIpcDevice = actualDevice
if (actualDevice && actualDevice !== root.currentDevice) {
root.setCurrentDevice(actualDevice, false)
}
const isExponential = SessionData.getBrightnessExponential(actualDevice)
const currentBrightness = root.getDeviceBrightness(actualDevice)
const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice)
let maxValue = 100
if (isExponential) {
maxValue = 100
} else {
maxValue = deviceInfo?.displayMax || 100
}
const newBrightness = Math.min(maxValue, currentBrightness + stepValue)
root.setBrightness(newBrightness, actualDevice, false)
return "Brightness increased by " + stepValue + "%" + (targetDevice ? " on " + targetDevice : "")
}
function decrement(step: string, device: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available"
}
const targetDevice = device || ""
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
return "Device not found: " + actualDevice
}
const stepValue = parseInt(step || "5")
root.lastIpcDevice = actualDevice
if (actualDevice && actualDevice !== root.currentDevice) {
root.setCurrentDevice(actualDevice, false)
}
const isExponential = SessionData.getBrightnessExponential(actualDevice)
const currentBrightness = root.getDeviceBrightness(actualDevice)
const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice)
let minValue = 0
if (isExponential) {
minValue = 1
} else {
minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0
}
const newBrightness = Math.max(minValue, currentBrightness - stepValue)
root.setBrightness(newBrightness, actualDevice, false)
return "Brightness decreased by " + stepValue + "%" + (targetDevice ? " on " + targetDevice : "")
}
function status(): string {
if (!root.brightnessAvailable) {
return "Brightness control not available"
}
return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%"
}
function list(): string {
if (!root.brightnessAvailable) {
return "No brightness devices available"
}
let result = "Available devices:\n"
for (const device of root.devices) {
const isExp = SessionData.getBrightnessExponential(device.id)
result += device.id + " (" + device.class + ")" + (isExp ? " [exponential]" : "") + "\n"
}
return result
}
function enableExponential(device: string): string {
const targetDevice = device || root.currentDevice
if (!targetDevice) {
return "No device specified"
}
if (!root.devices.some(d => d.id === targetDevice)) {
return "Device not found: " + targetDevice
}
SessionData.setBrightnessExponential(targetDevice, true)
return "Exponential mode enabled for " + targetDevice
}
function disableExponential(device: string): string {
const targetDevice = device || root.currentDevice
if (!targetDevice) {
return "No device specified"
}
if (!root.devices.some(d => d.id === targetDevice)) {
return "Device not found: " + targetDevice
}
SessionData.setBrightnessExponential(targetDevice, false)
return "Exponential mode disabled for " + targetDevice
}
function toggleExponential(device: string): string {
const targetDevice = device || root.currentDevice
if (!targetDevice) {
return "No device specified"
}
if (!root.devices.some(d => d.id === targetDevice)) {
return "Device not found: " + targetDevice
}
const currentState = SessionData.getBrightnessExponential(targetDevice)
SessionData.setBrightnessExponential(targetDevice, !currentState)
return "Exponential mode " + (!currentState ? "enabled" : "disabled") + " for " + targetDevice
}
target: "brightness"
}
// IPC Handler for night mode control
IpcHandler {
function toggle(): string {
root.toggleNightMode()
return root.nightModeEnabled ? "Night mode enabled" : "Night mode disabled"
}
function enable(): string {
root.enableNightMode()
return "Night mode enabled"
}
function disable(): string {
root.disableNightMode()
return "Night mode disabled"
}
function status(): string {
return root.nightModeEnabled ? "Night mode is enabled" : "Night mode is disabled"
}
function temperature(value: string): string {
if (!value) {
return "Current temperature: " + SessionData.nightModeTemperature + "K"
}
const temp = parseInt(value)
if (isNaN(temp)) {
return "Invalid temperature. Use a value between 2500 and 6000 (in steps of 500)"
}
// Validate temperature is in valid range and steps
if (temp < 2500 || temp > 6000) {
return "Temperature must be between 2500K and 6000K"
}
// Round to nearest 500
const rounded = Math.round(temp / 500) * 500
SessionData.setNightModeTemperature(rounded)
// Restart night mode with new temperature if active
if (root.nightModeEnabled) {
if (SessionData.nightModeAutoEnabled) {
root.startAutomation()
} else {
root.applyNightModeDirectly()
}
}
if (rounded !== temp) {
return "Night mode temperature set to " + rounded + "K (rounded from " + temp + "K)"
} else {
return "Night mode temperature set to " + rounded + "K"
}
}
target: "night"
}
}

View File

@@ -1,259 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property bool dwlAvailable: false
property var outputs: ({})
property var tagCount: 9
property var layouts: []
property string activeOutput: ""
property var outputScales: ({})
signal stateChanged()
Connections {
target: DMSService
function onCapabilitiesReceived() {
checkCapabilities()
}
function onConnectionStateChanged() {
if (DMSService.isConnected) {
checkCapabilities()
} else {
dwlAvailable = false
}
}
function onDwlStateUpdate(data) {
if (dwlAvailable) {
handleStateUpdate(data)
}
}
}
Component.onCompleted: {
if (DMSService.dmsAvailable) {
checkCapabilities()
}
if (dwlAvailable) {
refreshOutputScales()
}
}
function checkCapabilities() {
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
dwlAvailable = false
return
}
const hasDwl = DMSService.capabilities.includes("dwl")
if (hasDwl && !dwlAvailable) {
dwlAvailable = true
console.info("DwlService: DWL capability detected")
requestState()
refreshOutputScales()
} else if (!hasDwl) {
dwlAvailable = false
}
}
function requestState() {
if (!DMSService.isConnected || !dwlAvailable) {
return
}
DMSService.sendRequest("dwl.getState", null, response => {
if (response.result) {
handleStateUpdate(response.result)
}
})
}
function handleStateUpdate(state) {
outputs = state.outputs || {}
tagCount = state.tagCount || 9
layouts = state.layouts || []
activeOutput = state.activeOutput || ""
stateChanged()
}
function setTags(outputName, tagmask, toggleTagset) {
if (!DMSService.isConnected || !dwlAvailable) {
return
}
DMSService.sendRequest("dwl.setTags", {
"output": outputName,
"tagmask": tagmask,
"toggleTagset": toggleTagset
}, response => {
if (response.error) {
console.warn("DwlService: setTags error:", response.error)
}
})
}
function setClientTags(outputName, andTags, xorTags) {
if (!DMSService.isConnected || !dwlAvailable) {
return
}
DMSService.sendRequest("dwl.setClientTags", {
"output": outputName,
"andTags": andTags,
"xorTags": xorTags
}, response => {
if (response.error) {
console.warn("DwlService: setClientTags error:", response.error)
}
})
}
function setLayout(outputName, index) {
if (!DMSService.isConnected || !dwlAvailable) {
return
}
DMSService.sendRequest("dwl.setLayout", {
"output": outputName,
"index": index
}, response => {
if (response.error) {
console.warn("DwlService: setLayout error:", response.error)
}
})
}
function getOutputState(outputName) {
if (!outputs || !outputs[outputName]) {
return null
}
return outputs[outputName]
}
function getActiveTags(outputName) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
return []
}
return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag)
}
function getTagsWithClients(outputName) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
return []
}
return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag)
}
function getUrgentTags(outputName) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
return []
}
return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag)
}
function switchToTag(outputName, tagIndex) {
const tagmask = 1 << tagIndex
setTags(outputName, tagmask, 0)
}
function toggleTag(outputName, tagIndex) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
console.log("toggleTag: no output or tags for", outputName)
return
}
let currentMask = 0
output.tags.forEach(tag => {
if (tag.state === 1) {
currentMask |= (1 << tag.tag)
}
})
const clickedMask = 1 << tagIndex
const newMask = currentMask ^ clickedMask
console.log("toggleTag:", outputName, "tag:", tagIndex, "currentMask:", currentMask.toString(2), "clickedMask:", clickedMask.toString(2), "newMask:", newMask.toString(2))
if (newMask === 0) {
console.log("toggleTag: newMask is 0, switching to tag", tagIndex)
setTags(outputName, 1 << tagIndex, 0)
} else {
console.log("toggleTag: setting combined mask", newMask)
setTags(outputName, newMask, 0)
}
}
function quit() {
Quickshell.execDetached(["mmsg", "-d", "quit"])
}
Process {
id: scaleQueryProcess
command: ["mmsg", "-A"]
running: false
stdout: StdioCollector {
onStreamFinished: {
try {
const newScales = {}
const lines = text.trim().split('\n')
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 3 && parts[1] === "scale_factor") {
const outputName = parts[0]
const scale = parseFloat(parts[2])
if (!isNaN(scale)) {
newScales[outputName] = scale
}
}
}
outputScales = newScales
} catch (e) {
console.warn("DwlService: Failed to parse mmsg output:", e)
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("DwlService: mmsg failed with exit code:", exitCode)
}
}
}
function refreshOutputScales() {
if (!dwlAvailable) return
scaleQueryProcess.running = true
}
function getOutputScale(outputName) {
return outputScales[outputName]
}
function getVisibleTags(outputName) {
const output = getOutputState(outputName)
if (!output || !output.tags) {
return []
}
const visibleTags = new Set()
output.tags.forEach(tag => {
if (tag.state === 1 || tag.clients > 0) {
visibleTags.add(tag.tag)
}
})
return Array.from(visibleTags).sort((a, b) => a - b)
}
}

View File

@@ -1,60 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtCore
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property string currentProvider: "hyprland"
property var keybinds: ({"title": "", "provider": "", "binds": []})
Process {
id: getKeybinds
running: false
command: ["dms", "keybinds", "show", root.currentProvider]
stdout: StdioCollector {
onStreamFinished: {
try {
root.keybinds = JSON.parse(text)
} catch (e) {
console.error("[KeybindsService] Error parsing keybinds:", e)
}
}
}
onExited: (code) => {
if (code !== 0 && code !== 15) {
console.warn("[KeybindsService] Process exited with code:", code)
}
}
}
Timer {
interval: 500
running: true
repeat: false
onTriggered: {
getKeybinds.running = true
}
}
function loadProvider(provider) {
root.currentProvider = provider
reload()
}
function reload() {
if (getKeybinds.running) {
getKeybinds.running = false
}
Qt.callLater(function() {
getKeybinds.running = true
})
}
}

File diff suppressed because it is too large Load Diff

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