1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-03 11:02:08 -04:00

Compare commits

...

143 Commits

Author SHA1 Message Date
purian23
1d25d18ca7 Refactor shadow levels to their own layer 2026-02-28 16:46:11 -05:00
purian23
771e59fcb8 Restore click to expand animation on notif cards 2026-02-28 16:09:52 -05:00
purian23
35593fa9d9 Fix some moar m3 changes w/shadows 2026-02-28 15:46:53 -05:00
purian23
88b7889447 Core m3 spec complete 2026-02-27 10:10:28 -05:00
purian23
3c5d8ba66a Moar M3 iteration stuffs 2026-02-26 23:02:49 -05:00
purian23
972bc8397d theme: Continue to iterate m3 specs 2026-02-26 16:18:59 -05:00
purian23
788c1fa9e6 feat: Implement Material Design 3 elevation and shadow effects
- Added global toggle for enabling M3 elevation & shadow effects
- Refactored various components to sync updated specs
2026-02-26 01:09:21 -05:00
Kangheng Liu
a6269084c0 Systray: call context menu fallback for legacy protocol (#1839)
* systray: add call contextmenu fallback

directly call dbus contextmenu method. needs refactoring to be more
robust.

* add TODO

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-25 17:19:09 -05:00
bbedward
8271d8423d greeter: sync power menu options 2026-02-25 14:50:06 -05:00
bbedward
c76e29c457 dankdash: fix menu overlays 2026-02-25 14:37:55 -05:00
purian23
4750a7553b feat: Add independent power action confirmation settings for dms greeter 2026-02-25 14:33:09 -05:00
Joaquim S.
60786921a9 matugen/template: Pasterizing neovim. (#1828)
* matugen/template: Pasterizing neovim.

* matugen/template: More contrast
2026-02-25 13:58:36 -05:00
bbedward
751bbcc127 desktop widgets: fix deactive loaders when widgets disabled fixes #1813 2026-02-25 12:34:09 -05:00
null
58e8dd5456 feat: add more disk usage viewing options (#1833)
* feat: show memory widget in gb

* cleanup

* even more cleanup

* fix

* feat: add more disk usage viewing options
2026-02-25 10:53:12 -05:00
bbedward
1586c25847 dankbar: layer enabled false + binding tweak 2026-02-25 10:45:08 -05:00
null
cded5a7948 feat: show memory widget in gb (#1825)
* feat: show memory widget in gb

* cleanup

* even more cleanup

* fix
2026-02-25 08:01:41 -05:00
purian23
6238e065f2 distros: Workflows input type updates 2026-02-24 23:24:05 -05:00
purian23
72fbbfdd0d distros: Update workflows 2026-02-24 22:59:17 -05:00
purian23
2796c1cd4d fix: Defer DankCircularImage saving until the window is available 2026-02-24 21:23:36 -05:00
bbedward
54c9886627 settings: make horizontal change more smart 2026-02-24 20:48:42 -05:00
bbedward
05713cb389 settings: restore notifyHorizontalBarChanged 2026-02-24 19:42:29 -05:00
purian23
8bb3ee5f18 fix: Update HTML rendering injections 2026-02-24 19:34:46 -05:00
purian23
bc0b4825f1 dbar: Refactor to memoize dbar & widget state via json 2026-02-24 18:56:30 -05:00
purian23
ef7f17abf4 cpu widget: Fix monitor binding 2026-02-24 17:21:42 -05:00
Yamada.Kazuyoshi
876cd21f0b display battery consumption / charging W (#1814) 2026-02-24 15:42:47 -05:00
bbedward
5c92d49873 settings: use Image in theme colors tab wp preview 2026-02-24 15:22:47 -05:00
bbedward
da47b573be popout: fully unload popout layers on close 2026-02-24 15:19:30 -05:00
bbedward
2f04be8778 wallpaper: handle initial load better, add dms randr command for quick
physical scale retrieval
2026-02-24 15:09:04 -05:00
bbedward
69178ddfd8 privacy indicator: fix width when not active 2026-02-24 13:59:38 -05:00
bbedward
a310f6fff0 settings: use Image for per-mode previews 2026-02-24 13:36:57 -05:00
bbedward
7474abe286 matugen: skip theme refreshes if no colors changed 2026-02-24 13:28:13 -05:00
bbedward
df2ba3a3c6 dock: fix tooltip positioning 2026-02-24 13:28:13 -05:00
bbedward
e536456236 dankbar: fix some defaults in reset 2026-02-24 13:28:13 -05:00
bbedward
8d77122da3 widgets: set updatesEnabled false on background layers, if qs supports
it
2026-02-24 13:28:13 -05:00
bbedward
fb66effa51 widgets: fix moddedAppID consistency 2026-02-24 13:28:13 -05:00
purian23
5052e71c31 settings: Re-adjust dbar layout 2026-02-24 12:24:05 -05:00
purian23
bfc78d16ca settings: Dankbar layout updates 2026-02-24 12:02:16 -05:00
purian23
c425e3562b fix: Clipboard button widget alignment 2026-02-24 11:50:36 -05:00
bbedward
1f26092aa9 dankbar: fix syncing settings to new bars 2026-02-24 11:00:43 -05:00
bbedward
2849bb96f4 i18n: term sync 2026-02-24 10:50:16 -05:00
bbedward
7b749f2a4c dankbar: restore horizontal change debounce 2026-02-24 10:49:09 -05:00
bbedward
8803c94ce0 dpms: disable fade overlay in onRequestMonitorOn 2026-02-24 10:38:12 -05:00
bbedward
f5235c943b dankbar: optimize bindings in bar window 2026-02-24 10:22:43 -05:00
bbedward
59fec889b5 widgets: fix undefined icon warnings 2026-02-24 10:05:28 -05:00
null
f42f04a807 feat: improve icon resolution and align switcher fallback styling (#1823)
- Implement deep search icon resolution in DesktopService with runtime caching.
- Update Paths.getAppIcon to utilize enhanced resolution for mismatched app IDs.
- Align Workspace Switcher fallback icons with AppsDock visual style.
- Synchronize fallback text logic between Switcher and Dock using app names.
2026-02-24 09:40:15 -05:00
purian23
51f6f37925 display: Fix output config on delete & popup height 2026-02-24 01:03:51 -05:00
purian23
9651a4ca98 template: Refine bug report tracker 2026-02-24 00:41:04 -05:00
purian23
2b7fd36322 feat: Refactor DankBar w/New granular options
- New background toggles
- New maxIcon & maxText widget sizes (global)
- Dedicated M3 padding slider
- New independent icon scale options
- Updated logic to improve performance on single & dual bar modes
2026-02-23 23:18:27 -05:00
purian23
b8014fd4df fix: Animated Image warnings 2026-02-23 23:18:02 -05:00
Lucas
07460f6e1f doctor: fix imageformats detection (#1811) 2026-02-23 19:44:51 -05:00
bbedward
f7bf3b2afb keybinds: preserve scroll position of expanded item on list change
fixes #1766
2026-02-23 19:32:13 -05:00
bbedward
056f298cdf widgets: fallback when AnimatedImage probe fails to static Image 2026-02-23 19:03:14 -05:00
bbedward
e83da53162 thememode: connect to loginctl PrepareForSleep event 2026-02-23 19:01:20 -05:00
purian23
9f38a47a02 dms-greeter: Update dankinstall greeter automation w/distro packages 2026-02-23 15:36:17 -05:00
bbedward
236a4d4a6d launcher: don't tie unload to visibility 2026-02-23 15:28:29 -05:00
purian23
0909471510 audio: Sync audio hide opts w/dash Output devices 2026-02-23 13:48:33 -05:00
bbedward
05eaf59c89 audio: fix cycle output, improve icon resolution for sink
fixes #1808
2026-02-23 13:21:43 -05:00
Lucas
7749613801 nix: update flake.lock (#1809) 2026-02-23 12:31:36 -05:00
bbedward
e3dbaedbb4 audio: disable effects when mpris player is playing 2026-02-23 12:31:04 -05:00
bbedward
9f17ced6de launcher: implement memory for selected tab
fixes #1806
2026-02-23 10:19:31 -05:00
dms-ci[bot]
de54ef871d nix: update vendorHash for go.mod changes 2026-02-23 15:04:39 +00:00
Jonas Bloch
b0da45d6d0 Wifi QR code generation (requires testing) (#1794)
* fix: add mockery v2 to nix flake pkgs

* feat: add requests for generating wifi qr codes as png files in /tmp, and to delete them later. only supports NetworkManager backend for now.

* feat: add modal for sharing wifi via qr code and saving the code as png file.

* fix: uncomment QR code file deletion

* network: light refactor and cleanup for QR code generation

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-23 10:02:51 -05:00
bbedward
9b2a46fa92 widgets: make AnimatedImage conditional in DankCircularImage
- Cut potential overhead of always using AnimatedImage
2026-02-23 09:51:56 -05:00
bbedward
12099d2db6 osd: disable media playback OSD by default 2026-02-23 09:25:23 -05:00
Triệu Kha
84fa75936a clipboard: fix html elements get parsed in clipboard entry (#1798)
* clipboard: fix html elements get parsed in clipboard entry

* Revert "clipboard: fix html elements get parsed in clipboard entry"

This reverts commit 52b11eeb98.

* clipboard: fix html elements get parsed in clipboard entry
2026-02-23 09:16:15 -05:00
Jonas Bloch
d78d8121a1 fix(notepad): decode path URI when saving/creating a file (#1805) 2026-02-23 09:15:51 -05:00
Jonas Bloch
a9a3a52872 feat: add support for animated gifs as profile pictures (#1804) 2026-02-23 09:15:26 -05:00
purian23
912e3bdfce dms-greeter: Enable greetd via dms greeter install all-in-one cmd 2026-02-22 23:26:40 -05:00
bbedward
ee1b25d9e8 matugen: unconditionally run portal sync even if matugen errors 2026-02-22 22:57:51 -05:00
purian23
20ef5e2c18 dms-greeter: Enhance DMS Greeter dankinstall & packaging across distros
- Added support for Debian, Ubuntu, Fedora, Arch, and OpenSUSE on dankinstall / dms greeter install
2026-02-22 22:40:51 -05:00
bbedward
6ee419bc52 launcher: fix frecency ranking in search results
fixes #1799
2026-02-22 22:34:26 -05:00
bbedward
85b00d3c76 scripts: fix shellcheck 2026-02-22 22:13:57 -05:00
bbedward
bc4ad31d48 bluetooth: expose trust/untrust on devices 2026-02-22 22:10:07 -05:00
长夜月玩Fedora
71aad8ee32 Add support for 'evernight' distribution in Fedora (#1786) 2026-02-22 22:02:37 -05:00
Triệu Kha
8bb8231559 Fix dock visible when theres no app (#1797)
* clipboard: improve image thumbnail
- thumbnail image is now bigger
- circular mask has been replaced with rounded rectangular mask

* dock: fix dock still visible when there's no app
2026-02-22 21:56:33 -05:00
purian23
3cf9caae89 feat: DMS Greeter packaging for Debian/OpenSUSE on OBS 2026-02-22 15:44:42 -05:00
Lucas
f983c67135 zen: add more commands to detection (#1792) 2026-02-21 14:58:36 -05:00
bbedward
f2aef5b93f Merge branch 'master' of github.com:AvengeMedia/DankMaterialShell 2026-02-21 10:43:42 -05:00
purian23
46d4288969 ipc: Fix DankDash Wallpaper call 2026-02-21 00:52:08 -05:00
purian23
65516e872f theme: Fix Light/Dark mode portal sync 2026-02-21 00:31:26 -05:00
Connor Welsh
171329246c distro: add cups-pk-helper as suggested dependency (#1670) 2026-02-20 14:26:59 -05:00
bbedward
b2bee699e0 window rules: default to fixed for width/height
part of #1774
2026-02-20 13:51:03 -05:00
purian23
95c66b4d67 ubuntu: Fix dms-git Go versioning to restore builds 2026-02-20 13:12:12 -05:00
bbedward
babc8feb2b clipboard: fix memory leak from unbounded offer maps and unguarded file reads 2026-02-20 11:39:41 -05:00
bbedward
2f445c546a keybinds/niri: fix quote preservation 2026-02-20 11:37:02 -05:00
bbedward
a0283b3e3e dankdash: fix widgets across different bar section
fixes #1764s
2026-02-20 11:28:56 -05:00
bbedward
61bd156fb0 core/screenshot: light cleanups 2026-02-20 11:16:53 -05:00
Patrick Fischer
8ad0cf8e5f screensaver: emit ActiveChanged on lock/unlock (#1761) 2026-02-20 11:05:30 -05:00
Triệu Kha
ecd6d70da6 clipboard: improve image thumbnail (#1759)
- thumbnail image is now bigger
- circular mask has been replaced with rounded rectangular mask
2026-02-20 11:02:20 -05:00
purian23
359617d927 template: Default install method 2026-02-20 10:15:20 -05:00
purian23
38c286329a issues: Template fix 2026-02-20 09:57:58 -05:00
purian23
401b4095cc templates: Fix GitHub issue labels 2026-02-20 09:44:54 -05:00
shorinkiwata
06ab1a8ef0 feat(distros): allow CatOS to run DMS installer (#1768)
- This PR adds support for **CatOS**
- CatOS is fully compatible with Arch Linux
2026-02-20 09:28:42 -05:00
purian23
726fb8b015 templates: Update DMS issue formats 2026-02-20 09:26:20 -05:00
bbedward
b3b5c7a59f running apps: fix ordering on niri 2026-02-19 20:46:14 -05:00
purian23
d18f934978 fedora: Update trial run of bundled go deps 2026-02-19 19:21:01 -05:00
bbedward
e67f1f79bc launcher/dsearch: support for folder search and extra filters 2026-02-19 18:19:46 -05:00
purian23
e931829411 distros: Fix Go versioning on dms-git builds 2026-02-19 17:20:22 -05:00
bbedward
db8ebd606c launcher: fix premature exit of file search
fixes #1749
2026-02-19 16:46:50 -05:00
Jonas Bloch
072a358a94 Search keybinds fixes (#1748)
* fix: close keybind cheatsheet on escape press

* feat: match all space separated words in keybind cheatsheet search
2026-02-19 16:26:45 -05:00
bbedward
6ceb1b150c audio: fix hide device not working 2026-02-19 16:24:28 -05:00
bbedward
a4e03e1877 i18n: term sync 2026-02-19 13:32:06 -05:00
Youseffo13
02b3e4277b Added missing i18n strings and changed reset button (#1746)
* Update it.json

* Enhance SettingsSliderRow: add resetText property and update reset button styling

* added i18n strings

* adjust reset button width to be dynamic based on content size

* added i18n strings

* Update template.json

* reverted changes

* Update it.json

* Update template.json
2026-02-19 13:31:24 -05:00
bbedward
37daf801e6 dankbar: remove behaviors from monitoring widgets 2026-02-19 11:49:55 -05:00
bbedward
68d9f7eeb2 dgop: round computed values to match display format 2026-02-19 11:02:56 -05:00
bbedward
526e2420ca flake: fix dev flake for go 1.25 and ashellchheck 2026-02-19 09:30:05 -05:00
bbedward
a9cc58fc28 hyprland: add serial to output model generator 2026-02-19 09:22:10 -05:00
bbedward
77889ce1c6 dock: fix context menu styling
fixes #1742
2026-02-19 09:15:26 -05:00
bbedward
549073119e dock: fix transparency setting
fixes #1739
2026-02-19 09:11:30 -05:00
bbedward
5c5af5795f launcher: improve perf of settings search 2026-02-19 08:46:19 -05:00
bbedward
68e10934e4 launcher: always heuristic lookup cached entries 2026-02-18 23:47:00 -05:00
bbedward
c67bb1444a launcher v2: always heuristicLookup tab actions 2026-02-18 19:07:03 -05:00
bbedward
07389a152e i18n: term updates 2026-02-18 18:34:41 -05:00
bbedward
e562e21555 system tray: fix to take up 0 space when empty 2026-02-18 18:31:33 -05:00
Youseffo13
86dfe7dd3f Added Missing i18n strings (#1729)
* inverted dock visibility and position option

* added missing I18n strings

* added missing i18n strings

* added i18n strings

* Added missing i18n strings

* updated translations

* Update it.json
2026-02-18 18:28:57 -05:00
bbedward
ac0a8f3449 widgets: add openWith/toggleWith modes for dankbar widgets 2026-02-18 16:23:42 -05:00
bbedward
8e4a63db67 keybinds: fix escape in keybinds modal 2026-02-18 14:57:34 -05:00
bbedward
c02c63806f launcher v2: remove calc
cc: enhancements for plugins to size details
2026-02-18 14:48:20 -05:00
beluch-dev
42e5d7f6e9 fix: correct parameter name in Hyprland windowrule (no_initial_focus) (#1726)
##Description
This PR corrects the parameter name to match new Hyprland standard.

## Changes
-Before: 'noinitialfocus'
-After: 'no_initial_focus'
2026-02-18 13:41:12 -05:00
bbedward
d8cf1af422 plugins: fix settings focus loss 2026-02-18 13:36:23 -05:00
Evgeny Zemtsov
9723661c80 handle recycled server object IDs for workspace/group handles (#1725)
When switching tabs rapidly or closing multiple tabs, the taskbar shows
"ghost" workspaces — entries with no name, no coordinates, and no active
state. The ghosts appear at positions where workspaces were removed and
then recreated by the compositor.

When a compositor removes a workspace (sends `removed` event) and the
client calls Destroy(), the proxy is marked as zombie but stays in the
Context.objects map. For server-created objects (IDs >= 0xFF000000), the
server never sends `delete_id`, so the zombie proxy persists indefinitely.

When the compositor later creates a new workspace that gets a recycled
server object ID, GetProxy() returns the old zombie proxy. The dispatch
loop in GetDispatch() checks IsZombie() and silently drops ALL events
for zombie proxies — including property events (name, id, coordinates,
state, capabilities) intended for the new workspace. This causes the
ghost workspaces with empty properties in the UI.

Fix: check IsZombie() when handling `workspace` and `workspace_group`
events that carry a `new_id` argument. If the existing proxy is a
zombie, treat it as absent and create a fresh proxy via
registerServerProxy(), which replaces the zombie in the map. Subsequent
property events are then dispatched to the live proxy.
2026-02-18 12:51:17 -05:00
bbedward
81cba7ad97 popout: decouple shadow from content layer 2026-02-18 10:45:45 -05:00
bbedward
c23f58de40 popout: disable layer after animation 2026-02-18 10:34:07 -05:00
purian23
2cf67ca7da notifications: Maintain shadow during expansion 2026-02-18 10:27:57 -05:00
purian23
392bd850ea notifications: Update initial popup height surfaces 2026-02-18 10:16:11 -05:00
bbedward
3b2ad9d1bd running apps: fix scroll events being propagated
fixes #1724
2026-02-18 10:12:15 -05:00
bbedward
27b7474180 matugen: make v4 detection more resilient 2026-02-18 09:55:45 -05:00
bbedward
63948d728e process list: fix scaling with fonts
fixes #1721
2026-02-18 09:55:45 -05:00
purian23
d219d3b873 dankinstall: Fix Debian ARM64 detection 2026-02-18 09:41:36 -05:00
bbedward
93ab290bc1 matugen: detect emacs directory
fixes #1720
2026-02-18 09:23:13 -05:00
bbedward
7335c5d79a osd: optimize bindings 2026-02-18 09:04:39 -05:00
bbedward
242ead722a screenshot: adjust cursor CLI option to be more explicit 2026-02-17 22:28:19 -05:00
bbedward
8a6d9696a8 settings: workaround crash 2026-02-17 22:20:01 -05:00
purian23
896b7ea242 notifications: Tweak animation scale & settings 2026-02-17 22:05:19 -05:00
bbedward
0c7f4c7828 settings: guard internal writes from watcher 2026-02-17 22:03:36 -05:00
bbedward
3d35af2a87 cc: fix plugin reloading in bar position changes 2026-02-17 17:24:22 -05:00
bbedward
fed3c36f84 popout: anchor height changing popout surfaces to top and bottom 2026-02-17 17:18:07 -05:00
bbedward
414d81aa40 workspaces: fix named workspace icons 2026-02-17 16:02:13 -05:00
bbedward
d548803769 dankinstall: no_anim on dms layers 2026-02-17 15:32:08 -05:00
bbedward
1180258394 system updater: fix hide no update option 2026-02-17 13:53:17 -05:00
bbedward
48a566a24b launcher: fix kb navigation not always showing last delegate in view 2026-02-17 13:07:43 -05:00
bbedward
3bc5d1df81 doctor: add qt6-imageformats check 2026-02-17 12:58:09 -05:00
bbedward
c7222e2e86 bump version, codename, disable changelog 2026-02-17 12:02:36 -05:00
249 changed files with 14970 additions and 6510 deletions

View File

@@ -7,32 +7,32 @@ body:
attributes:
value: |
## DankMaterialShell Bug Report
Limit your report to one issue per submission unless closely related
- type: checkboxes
Limit your report to one issue per submission unless similarly related
- type: dropdown
id: compositor
attributes:
label: Compositor
options:
- label: Niri
- label: Hyprland
- label: MangoWC (dwl)
- label: Sway
- Niri
- Hyprland
- MangoWC (dwl)
- Sway
validations:
required: true
- type: checkboxes
- type: dropdown
id: distribution
attributes:
label: Distribution
options:
- label: Arch Linux
- label: CachyOS
- label: Fedora
- label: NixOS
- label: Debian
- label: Ubuntu
- label: Gentoo
- label: OpenSUSE
- label: Other (specify below)
- Arch Linux
- CachyOS
- Fedora
- NixOS
- Debian
- Ubuntu
- Gentoo
- OpenSUSE
- Other (specify below)
validations:
required: true
- type: input
@@ -42,12 +42,45 @@ body:
placeholder: e.g., PikaOS, Void Linux, etc.
validations:
required: false
- type: dropdown
id: installation_method
attributes:
label: Select your Installation Method
options:
- DankInstaller
- Distro Packaging
- Source
validations:
required: true
- type: dropdown
id: original_installation_method
attributes:
label: Was this your original Installation method?
options:
- "Yes"
- No (specify below)
default: 0
validations:
required: false
- type: input
id: original_installation_method_specify
attributes:
label: If no, specify
placeholder: e.g., Distro Packaging, then Source
validations:
required: false
- type: textarea
id: dms_doctor
attributes:
label: dms doctor -v
description: Output of `dms doctor -v` command
placeholder: Paste the output of `dms doctor -v` here
label: dms doctor -vC
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
placeholder: Paste the output of `dms doctor -vC` here
value: |
<details>
<summary>Click to expand</summary>
</details>
validations:
required: true
- type: textarea
@@ -69,7 +102,7 @@ body:
- type: textarea
id: steps_to_reproduce
attributes:
label: Steps to Reproduce & Installation Method
label: Steps to Reproduce
description: Please provide detailed steps to reproduce the issue
placeholder: |
1. ...

View File

@@ -23,18 +23,25 @@ body:
placeholder: Why is this feature important?
validations:
required: false
- type: checkboxes
- type: dropdown
id: compositor
attributes:
label: Compositor(s)
description: Is this feature specific to one or more compositors?
options:
- label: All compositors
- label: Niri
- label: Hyprland
- label: MangoWC (dwl)
- label: Sway
- label: Other (specify below)
- All compositors
- Niri
- Hyprland
- MangoWC (dwl)
- Sway
- Other (specify below)
validations:
required: true
- type: input
id: compositor_other
attributes:
label: If Other, please specify
placeholder: e.g., Wayfire, Mutter, etc.
validations:
required: false
- type: textarea

View File

@@ -7,32 +7,87 @@ body:
attributes:
value: |
## DankMaterialShell Support Request
- type: checkboxes
- type: dropdown
id: compositor
attributes:
label: Compositor
options:
- label: Niri
- label: Hyprland
- label: MangoWC (dwl)
- label: Sway
- label: Other (specify below)
- Niri
- Hyprland
- MangoWC (dwl)
- Sway
- Other (specify below)
validations:
required: true
- type: input
id: compositor_other
attributes:
label: If Other, please specify
placeholder: e.g., Wayfire, Mutter, etc.
validations:
required: false
- type: input
- type: dropdown
id: distribution
attributes:
label: Distribution
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
placeholder: Your Linux distribution
options:
- Arch Linux
- CachyOS
- Fedora
- NixOS
- Debian
- Ubuntu
- Gentoo
- OpenSUSE
- Other (specify below)
validations:
required: true
- type: input
id: distribution_other
attributes:
label: If Other, please specify
placeholder: e.g., PikaOS, Void Linux, etc.
validations:
required: false
- type: dropdown
id: installation_method
attributes:
label: Select your Installation Method
options:
- DankInstaller
- Distro Packaging
- Source
validations:
required: true
- type: dropdown
id: original_installation_method_different
attributes:
label: Was your original Installation method different?
options:
- "Yes"
- No (specify below)
default: 0
validations:
required: false
- type: input
id: original_installation_method_specify
attributes:
label: If no, specify
placeholder: e.g., Distro Packaging, then Source
validations:
required: false
- type: textarea
id: dms_doctor
attributes:
label: dms doctor -v
description: Output of `dms doctor -v` command
placeholder: Paste the output of `dms doctor -v` here
label: dms doctor -vC
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
placeholder: Paste the output of `dms doctor -vC` here
value: |
<details>
<summary>Click to expand</summary>
</details>
validations:
required: false
- type: textarea

View File

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

View File

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

@@ -9,6 +9,7 @@ on:
type: choice
options:
- dms
- dms-greeter
- dms-git
- all
default: "dms"
@@ -72,12 +73,27 @@ jobs:
fi
}
# Helper function to check dms-greeter stable tag
check_dms_greeter_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag selected or pushed - always update stable package
echo "packages=dms" >> $GITHUB_OUTPUT
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
# Run from tag with no package specified - update both stable packages
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
@@ -103,15 +119,18 @@ jobs:
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
# Check each stable package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
fi
if check_dms_greeter_stable; then
PACKAGES_TO_UPDATE+=("dms-greeter")
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -120,7 +139,7 @@ jobs:
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
echo "✓ Both packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
@@ -144,6 +163,18 @@ jobs:
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_dms_greeter_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
@@ -171,15 +202,8 @@ jobs:
- name: Determine packages to update
id: packages
run: |
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag selected or pushed - use the tag from GITHUB_REF
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION"
# Check if check-updates already determined a version (from auto-detection)
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
# Use check-updates outputs when available
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
# Use version from check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
@@ -191,40 +215,16 @@ jobs:
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch
# Determine version for dms stable
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
# Use github.ref if tag selected, otherwise auto-detect latest
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION"
# Determine version for dms stable and dms-greeter using the API
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
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 "Using latest release from API: $LATEST_TAG"
else
# Auto-detect latest release for dms
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Auto-detected latest release: $LATEST_TAG"
else
echo "ERROR: Could not auto-detect latest release"
exit 1
fi
fi
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
# Use github.ref if tag selected, otherwise auto-detect latest
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION"
else
# Auto-detect latest release for "all"
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Auto-detected latest release: $LATEST_TAG"
else
echo "ERROR: Could not auto-detect latest release"
exit 1
fi
echo "ERROR: Could not fetch latest release from API"
exit 1
fi
fi
@@ -244,7 +244,7 @@ jobs:
fi
- name: Update dms-git spec version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
if: contains(steps.packages.outputs.packages, 'dms-git')
run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
@@ -265,7 +265,7 @@ jobs:
} > 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'
if: contains(steps.packages.outputs.packages, 'dms-git')
run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
@@ -283,55 +283,66 @@ jobs:
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog"
- name: Update dms stable version
- name: Update stable version (dms + dms-greeter)
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
PACKAGES="${{ steps.packages.outputs.packages }}"
echo "==> Updating packaging files to version: $VERSION_NO_V"
# Update spec file
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Update dms spec and changelog when dms is in the upload list
if [[ "$PACKAGES" == *"dms"* ]]; then
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
# 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"
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
# 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"
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 dms changelog to ${VERSION_NO_V}db1"
fi
done
fi
# Update Debian changelog for dms stable (single entry, history on OBS website)
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
# Update dms-greeter changelog when dms-greeter is in the upload list
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
echo "dms-greeter (${VERSION_NO_V}db1) unstable; 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"
} > "distro/debian/dms-greeter/debian/changelog"
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
fi
# Update Debian _service files for packages in upload list (download_url paths)
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, dms-greeter 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
- name: Install Go
uses: actions/setup-go@v5
with:
@@ -354,6 +365,7 @@ jobs:
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
id: upload
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
TAG_VERSION: ${{ steps.packages.outputs.version }}
@@ -362,6 +374,8 @@ jobs:
if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!"
echo "uploaded_packages=" >> $GITHUB_OUTPUT
echo "skipped_packages=" >> $GITHUB_OUTPUT
exit 0
fi
@@ -371,7 +385,10 @@ jobs:
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
fi
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
echo ""
@@ -382,13 +399,37 @@ jobs:
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
LOG_FILE=$(mktemp)
set +e
if [[ "$PKG" == "dms-git" ]]; then
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
else
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
fi
STATUS=$?
set -e
cat "$LOG_FILE"
if [[ $STATUS -ne 0 ]]; then
rm -f "$LOG_FILE"
echo "❌ Upload failed for $PKG"
exit $STATUS
fi
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
echo " $PKG is already up to date. Skipped."
SKIPPED_PACKAGES+=("$PKG")
else
UPLOADED_PACKAGES+=("$PKG")
fi
rm -f "$LOG_FILE"
done
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
- name: Summary
if: always()
run: |
@@ -402,20 +443,59 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
else
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
UPLOADED_COUNT=0
SKIPPED_COUNT=0
if [[ -n "$UPLOADED_PACKAGES" ]]; then
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
fi
if [[ -n "$SKIPPED_PACKAGES" ]]; then
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
fi
in_list() {
local item="$1"
local list="$2"
[[ " $list " == *" $item "* ]]
}
if [[ "${{ job.status }}" == "success" ]]; then
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
else
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do
STATUS_ICON="✅"
STATUS_TEXT="uploaded"
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
STATUS_ICON=""
STATUS_TEXT="up to date (skipped)"
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
STATUS_ICON="❌"
STATUS_TEXT="failed"
fi
case "$PKG" in
dms)
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [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
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then

View File

@@ -4,9 +4,15 @@ on:
workflow_dispatch:
inputs:
package:
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: false
default: "dms-git"
description: "Package to upload"
required: true
type: choice
options:
- dms
- dms-greeter
- dms-git
- all
default: "dms"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false
@@ -139,7 +145,7 @@ jobs:
fi
else
# Fallback
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "packages=dms" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
@@ -209,7 +215,7 @@ jobs:
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# PACKAGES can be space-separated list (e.g., "dms-git dms dms-greeter" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
# Map package to PPA name

View File

@@ -5,11 +5,13 @@ repos:
- id: trailing-whitespace
- id: check-yaml
- id: end-of-file-fixer
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
- repo: local
hooks:
- id: shellcheck
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
name: shellcheck
entry: shellcheck -e SC2164 -e SC2001 -e SC2012 -e SC2317
language: system
types: [shell]
- repo: local
hooks:
- id: go-mod-tidy

View File

@@ -22,7 +22,7 @@ nix develop
This will provide:
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
- Go 1.25+ toolchain (go, gopls, delve, go-tools) and GNU Make
- Quickshell and required QML packages
- Properly configured QML2_IMPORT_PATH

View File

@@ -96,7 +96,7 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
## Building
Requires Go 1.24+
Requires Go 1.25+
**Development build:**

View File

@@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
doctorCmd,
configCmd,
dlCmd,
randrCmd,
}
}

View File

@@ -649,58 +649,104 @@ func checkI2CAvailability() checkResult {
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
}
func checkKImageFormats() checkResult {
func checkImageFormatPlugins() []checkResult {
url := doctorDocsURL + "#optional-features"
desc := "Extra image format support (AVIF, HEIF, JXL)"
pluginDir := findQtPluginDir()
if pluginDir == "" {
return checkResult{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (qtpaths not found)", desc, url}
}
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
keyPlugins := []struct{ file, format string }{
{"kimg_avif.so", "AVIF"},
{"kimg_heif.so", "HEIF"},
{"kimg_jxl.so", "JXL"},
{"kimg_exr.so", "EXR"},
}
var found []string
for _, p := range keyPlugins {
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
found = append(found, p.format)
pluginDirs := findQtPluginDirs()
if len(pluginDirs) == 0 {
return []checkResult{
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
}
}
if len(found) == 0 {
return checkResult{catOptionalFeatures, "kimageformats", statusWarn, "Not installed", desc, url}
type pluginCheck struct {
name string
desc string
plugins []struct{ file, format string }
}
details := ""
if doctorVerbose {
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
checks := []pluginCheck{
{
name: "qt6-imageformats",
desc: "WebP, TIFF, GIF, JP2 support",
plugins: []struct{ file, format string }{
{"libqwebp.so", "WebP"},
{"libqtiff.so", "TIFF"},
{"libqgif.so", "GIF"},
{"libqjp2.so", "JP2"},
{"libqicns.so", "ICNS"},
},
},
{
name: "kimageformats",
desc: "AVIF, HEIF, JXL support",
plugins: []struct{ file, format string }{
{"kimg_avif.so", "AVIF"},
{"kimg_heif.so", "HEIF"},
{"kimg_jxl.so", "JXL"},
{"kimg_exr.so", "EXR"},
},
},
}
return checkResult{catOptionalFeatures, "kimageformats", statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
var results []checkResult
for _, c := range checks {
var found []string
var foundDirs []string
for _, pluginDir := range pluginDirs {
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
for _, p := range c.plugins {
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
if !slices.Contains(found, p.format) {
found = append(found, p.format)
}
if !slices.Contains(foundDirs, imageFormatsDir) {
foundDirs = append(foundDirs, imageFormatsDir)
}
}
}
}
var result checkResult
switch {
case len(found) == 0:
result = checkResult{catOptionalFeatures, c.name, statusWarn, "Not installed", c.desc, url}
default:
details := ""
if doctorVerbose {
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":"))
}
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
}
results = append(results, result)
}
return results
}
func findQtPluginDir() string {
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
func findQtPluginDirs() []string {
var dirs []string
addDir := func(dir string) {
if dir != "" {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
dirs = append(dirs, dir)
}
}
}
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
for dir := range strings.SplitSeq(envPath, ":") {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
return dir
}
addDir(dir)
}
}
// Try qtpaths
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
if dir := strings.TrimSpace(string(output)); dir != "" {
return dir
}
addDir(strings.TrimSpace(string(output)))
}
}
@@ -711,12 +757,10 @@ func findQtPluginDir() string {
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
} {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
return dir
}
addDir(dir)
}
return ""
return dirs
}
func detectNetworkBackend(stackResult *network.DetectResult) string {
@@ -773,7 +817,7 @@ func checkOptionalDependencies() []checkResult {
results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL})
results = append(results, checkI2CAvailability())
results = append(results, checkKImageFormats())
results = append(results, checkImageFormatPlugins()...)
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {

View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
@@ -77,6 +78,28 @@ func installGreeter() error {
return err
}
// Debian/openSUSE
greeter.TryInstallGreeterPackage(logFunc, "")
if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() {
return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint())
}
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
}
// If already fully configured, prompt the user
if isGreeterEnabled() {
fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ")
var response string
fmt.Scanln(&response)
response = strings.TrimSpace(strings.ToLower(response))
if response == "n" || response == "no" {
fmt.Println("Run 'dms greeter sync' to re-sync theme and settings at any time.")
return nil
}
fmt.Println()
}
fmt.Println("\nDetecting DMS installation...")
dmsPath, err := greeter.DetectDMSPath()
if err != nil {
@@ -114,7 +137,12 @@ func installGreeter() error {
}
fmt.Println("\nConfiguring greetd...")
if err := greeter.ConfigureGreetd(dmsPath, selectedCompositor, logFunc, ""); err != nil {
// Use empty path when packaged (greeter finds /usr/share/quickshell/dms-greeter); else use user's DMS path
greeterPathForConfig := ""
if !greeter.IsGreeterPackaged() {
greeterPathForConfig = dmsPath
}
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
return err
}
@@ -123,11 +151,22 @@ func installGreeter() error {
return err
}
if err := ensureGraphicalTarget(); err != nil {
return err
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Installation Complete ===")
fmt.Println("\nTo test the greeter, run:")
fmt.Println("\nTo start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nTo enable on boot, run:")
fmt.Println(" sudo systemctl enable --now greetd")
fmt.Println("\nOr reboot to see the greeter at next boot.")
return nil
}
@@ -327,20 +366,29 @@ func ensureGreetdEnabled() error {
fmt.Println(" ✓ Unmasked greetd")
}
switch state.EnabledState {
case "disabled", "masked", "masked-runtime":
if state.EnabledState == "enabled" || state.EnabledState == "enabled-runtime" {
fmt.Println(" Reasserting greetd as active display manager...")
} else {
fmt.Println(" Enabling greetd service...")
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
enableCmd.Stdout = os.Stdout
enableCmd.Stderr = os.Stderr
if err := enableCmd.Run(); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
enableCmd.Stdout = os.Stdout
enableCmd.Stderr = os.Stderr
if err := enableCmd.Run(); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
enabledState, _, verifyErr := checkSystemdServiceEnabled("greetd")
if verifyErr != nil {
fmt.Printf(" ⚠ Warning: Could not verify greetd enabled state: %v\n", verifyErr)
} else {
switch enabledState {
case "enabled", "enabled-runtime", "static", "indirect", "alias":
fmt.Printf(" ✓ greetd enabled (state: %s)\n", enabledState)
default:
return fmt.Errorf("greetd is still in state '%s' after enable operation", enabledState)
}
fmt.Println(" ✓ Enabled greetd service")
case "enabled", "enabled-runtime":
fmt.Println(" ✓ greetd is already enabled")
default:
fmt.Printf(" greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
}
return nil
@@ -446,6 +494,10 @@ func enableGreeter() error {
}
configContent := string(data)
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
}
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
if configAlreadyCorrect {
@@ -611,6 +663,48 @@ func detectConfiguredCompositor() string {
return ""
}
func packageInstallHint() string {
osInfo, err := distros.GetOSInfo()
if err != nil {
return "Install package: dms-greeter"
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return "Install package: dms-greeter"
}
switch config.Family {
case distros.FamilyDebian:
return "Install with 'sudo apt install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#debian)"
case distros.FamilySUSE:
return "Install with 'sudo zypper install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#opensuse)"
case distros.FamilyUbuntu:
return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)"
case distros.FamilyFedora:
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
case distros.FamilyArch:
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
default:
return "Run 'dms greeter install' to install greeter"
}
}
func isPackageOnlyGreeterDistro() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return false
}
return config.Family == distros.FamilyDebian ||
config.Family == distros.FamilySUSE ||
config.Family == distros.FamilyUbuntu ||
config.Family == distros.FamilyFedora ||
config.Family == distros.FamilyArch
}
func promptCompositorChoice(compositors []string) (string, error) {
fmt.Println("\nMultiple compositors detected:")
for i, comp := range compositors {
@@ -679,7 +773,7 @@ func checkGreeterStatus() error {
}
} else {
fmt.Println(" ✗ Greeter config not found")
fmt.Println(" Run 'dms greeter install' to install greeter")
fmt.Printf(" %s\n", packageInstallHint())
}
fmt.Println("\nGroup Membership:")
@@ -689,12 +783,13 @@ func checkGreeterStatus() error {
return fmt.Errorf("failed to check groups: %w", err)
}
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
greeterGroup := greeter.DetectGreeterGroup()
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
if inGreeterGroup {
fmt.Println(" ✓ User is in greeter group")
fmt.Printf(" ✓ User is in %s group\n", greeterGroup)
} else {
fmt.Println(" ✗ User is NOT in greeter group")
fmt.Println(" Run 'dms greeter install' to add user to greeter group")
fmt.Printf(" ✗ User is NOT in %s group\n", greeterGroup)
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
}
cacheDir := "/var/cache/dms-greeter"
@@ -703,7 +798,7 @@ func checkGreeterStatus() error {
fmt.Printf(" ✓ %s exists\n", cacheDir)
} else {
fmt.Printf(" ✗ %s not found\n", cacheDir)
fmt.Println(" Run 'dms greeter install' to create cache directory")
fmt.Printf(" %s\n", packageInstallHint())
return nil
}

View File

@@ -3,7 +3,9 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -95,7 +97,11 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
func runMatugenGenerate(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd)
if err := matugen.Run(opts); err != nil {
err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err)
}
}
@@ -129,7 +135,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
if !wait {
if err := sendServerRequestFireAndForget(request); err != nil {
log.Info("Server unavailable, running synchronously")
if err := matugen.Run(opts); err != nil {
err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err)
}
return
@@ -146,11 +156,15 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
resp, ok := tryServerRequest(request)
if !ok {
log.Info("Server unavailable, running synchronously")
if err := matugen.Run(opts); err != nil {
err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
resultCh <- matugen.ErrNoChanges
case err != nil:
resultCh <- err
return
default:
resultCh <- nil
}
resultCh <- nil
return
}
if resp.Error != "" {
@@ -162,7 +176,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
select {
case err := <-resultCh:
if err != nil {
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err)
}
fmt.Println("Theme generation completed")

View File

@@ -0,0 +1,58 @@
package main
import (
"encoding/json"
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var randrCmd = &cobra.Command{
Use: "randr",
Short: "Query output display information",
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
Run: runRandr,
}
func init() {
randrCmd.Flags().Bool("json", false, "Output in JSON format")
}
type randrJSON struct {
Outputs []randrOutput `json:"outputs"`
}
func runRandr(cmd *cobra.Command, args []string) {
outputs, err := queryRandr()
if err != nil {
log.Fatalf("%v", err)
}
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
data, err := json.Marshal(randrJSON{Outputs: outputs})
if err != nil {
log.Fatalf("failed to marshal JSON: %v", err)
}
fmt.Println(string(data))
return
}
for i, out := range outputs {
if i > 0 {
fmt.Println()
}
status := "enabled"
if !out.Enabled {
status = "disabled"
}
fmt.Printf("%s (%s)\n", out.Name, status)
fmt.Printf(" Scale: %.4g\n", out.Scale)
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
if out.Refresh > 0 {
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
}
}
}

View File

@@ -13,16 +13,16 @@ import (
)
var (
ssOutputName string
ssIncludeCursor bool
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
ssOutputName string
ssCursor string
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
)
var screenshotCmd = &cobra.Command{
@@ -52,7 +52,7 @@ Examples:
dms screenshot last # Last region (pre-selected)
dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only
dms screenshot --cursor # Include cursor
dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
}
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
@@ -136,7 +136,9 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig()
config.Mode = mode
config.OutputName = ssOutputName
config.IncludeCursor = ssIncludeCursor
if strings.EqualFold(ssCursor, "on") {
config.Cursor = screenshot.CursorOn
}
config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify

View File

@@ -18,7 +18,7 @@ func init() {
runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)

View File

@@ -0,0 +1,172 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type randrOutput struct {
Name string `json:"name"`
Scale float64 `json:"scale"`
Width int32 `json:"width"`
Height int32 `json:"height"`
Refresh int32 `json:"refresh"`
Enabled bool `json:"enabled"`
}
type randrHead struct {
name string
enabled bool
scale float64
currentModeID uint32
modeIDs []uint32
}
type randrMode struct {
width int32
height int32
refresh int32
}
type randrClient struct {
display *wlclient.Display
ctx *wlclient.Context
manager *wlr_output_management.ZwlrOutputManagerV1
heads map[uint32]*randrHead
modes map[uint32]*randrMode
done bool
err error
}
func queryRandr() ([]randrOutput, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
}
c := &randrClient{
display: display,
ctx: display.Context(),
heads: make(map[uint32]*randrHead),
modes: make(map[uint32]*randrMode),
}
defer c.ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, fmt.Errorf("failed to get registry: %w", err)
}
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
version := min(e.Version, 4)
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
c.handleHead(e)
})
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
c.done = true
})
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
c.manager = mgr
}
}
})
// First roundtrip: discover globals and bind manager
syncCallback, err := display.Sync()
if err != nil {
return nil, fmt.Errorf("failed to sync display: %w", err)
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
if c.manager == nil {
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
c.done = true
}
// Otherwise wait for manager's DoneHandler
})
for !c.done {
if err := c.ctx.Dispatch(); err != nil {
return nil, fmt.Errorf("dispatch error: %w", err)
}
}
if c.err != nil {
return nil, c.err
}
return c.buildOutputs(), nil
}
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
handle := e.Head
headID := handle.ID()
head := &randrHead{
modeIDs: make([]uint32, 0),
}
c.heads[headID] = head
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
head.name = e.Name
})
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
head.enabled = e.Enabled != 0
})
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
head.scale = e.Scale
})
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
head.currentModeID = e.Mode.ID()
})
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
modeHandle := e.Mode
modeID := modeHandle.ID()
head.modeIDs = append(head.modeIDs, modeID)
mode := &randrMode{}
c.modes[modeID] = mode
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
mode.width = e.Width
mode.height = e.Height
})
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
mode.refresh = e.Refresh
})
})
}
func (c *randrClient) buildOutputs() []randrOutput {
outputs := make([]randrOutput, 0, len(c.heads))
for _, head := range c.heads {
out := randrOutput{
Name: head.name,
Scale: head.scale,
Enabled: head.enabled,
}
if mode, ok := c.modes[head.currentModeID]; ok {
out.Width = mode.width
out.Height = mode.height
out.Refresh = mode.refresh
}
outputs = append(outputs, out)
}
return outputs
}

View File

@@ -16,6 +16,8 @@ require (
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.3.0
github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3
@@ -32,15 +34,19 @@ require (
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
)

View File

@@ -58,6 +58,8 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -75,6 +77,8 @@ github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -115,6 +119,8 @@ github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -142,6 +148,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=

View File

@@ -100,7 +100,7 @@ windowrule = float on, match:class ^(blueman-manager)$
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
windowrule = float on, match:class ^(xdg-desktop-portal)$
windowrule = noinitialfocus on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
@@ -111,6 +111,7 @@ windowrule = float on, match:class ^(zoom)$
# windowrule = float on, match:class ^(org.quickshell)$
layerrule = no_anim on, match:namespace ^(quickshell)$
layerrule = no_anim on, match:namespace ^dms:.*
source = ./dms/colors.conf
source = ./dms/outputs.conf

View File

@@ -26,6 +26,9 @@ func init() {
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("catos", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
@@ -94,6 +97,7 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectGit())
dependencies = append(dependencies, a.detectWindowManager(wm))
dependencies = append(dependencies, a.detectQuickshell())
dependencies = append(dependencies, a.detectDMSGreeter())
dependencies = append(dependencies, a.detectXDGPortal())
dependencies = append(dependencies, a.detectAccountsService())
@@ -121,6 +125,10 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
}
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
}
func (a *ArchDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("pacman", "-Q", pkg)
err := cmd.Run()
@@ -136,6 +144,7 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
"git": {Name: "git", Repository: RepoTypeSystem},
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
"matugen": a.getMatugenMapping(variants["matugen"]),
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},

View File

@@ -102,6 +102,19 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo
}
}
func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency {
status := deps.StatusMissing
if installed {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: false,
}
}
func (b *BaseDistribution) detectGit() deps.Dependency {
return b.detectCommand("git", "Version control system")
}

View File

@@ -61,6 +61,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectGit())
dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectDMSGreeter())
dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectAccountsService())
@@ -86,6 +87,10 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
}
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
}
func (d *DebianDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run()
@@ -108,6 +113,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// DMS packages from OBS with variant support
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
@@ -430,7 +436,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
}
// Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,

View File

@@ -13,6 +13,9 @@ func init() {
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
@@ -75,6 +78,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectGit())
dependencies = append(dependencies, f.detectWindowManager(wm))
dependencies = append(dependencies, f.detectQuickshell())
dependencies = append(dependencies, f.detectDMSGreeter())
dependencies = append(dependencies, f.detectXDGPortal())
dependencies = append(dependencies, f.detectAccountsService())
@@ -120,6 +124,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// COPR packages
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
@@ -191,6 +196,10 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
}
}
func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency {
return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter"))
}
func (f *FedoraDistribution) getPrerequisites() []string {
return []string{
"dnf-plugins-core",

View File

@@ -55,6 +55,7 @@ const (
PhaseAURPackages
PhaseCursorTheme
PhaseConfiguration
PhaseGreeterSetup
PhaseComplete
)

View File

@@ -71,6 +71,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectGit())
dependencies = append(dependencies, o.detectWindowManager(wm))
dependencies = append(dependencies, o.detectQuickshell())
dependencies = append(dependencies, o.detectDMSGreeter())
dependencies = append(dependencies, o.detectXDGPortal())
dependencies = append(dependencies, o.detectAccountsService())
@@ -100,6 +101,10 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
return err == nil
}
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
}
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
@@ -116,6 +121,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
// DMS packages from OBS
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},

View File

@@ -63,6 +63,7 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, u.detectGit())
dependencies = append(dependencies, u.detectWindowManager(wm))
dependencies = append(dependencies, u.detectQuickshell())
dependencies = append(dependencies, u.detectDMSGreeter())
dependencies = append(dependencies, u.detectXDGPortal())
dependencies = append(dependencies, u.detectAccountsService())
@@ -94,6 +95,10 @@ func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
}
func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter"))
}
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run()
@@ -116,6 +121,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// DMS packages from PPAs
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},

View File

@@ -17,7 +17,6 @@ import (
"github.com/sblinch/kdl-go/document"
)
// DetectDMSPath checks for DMS installation following XDG Base Directory specification
func DetectDMSPath() (string, error) {
return config.LocateDMSConfig()
}
@@ -37,7 +36,6 @@ func DetectGreeterGroup() string {
return "greeter"
}
// DetectCompositors checks which compositors are installed
func DetectCompositors() []string {
var compositors []string
@@ -51,7 +49,6 @@ func DetectCompositors() []string {
return compositors
}
// PromptCompositorChoice asks user to choose between compositors
func PromptCompositorChoice(compositors []string) (string, error) {
fmt.Println("\nMultiple compositors detected:")
for i, comp := range compositors {
@@ -79,9 +76,18 @@ func PromptCompositorChoice(compositors []string) (string, error) {
}
}
// EnsureGreetdInstalled checks if greetd is installed and installs it if not
// EnsureGreetdInstalled checks if greetd is installed - greetd is a daemon in /usr/sbin on Debian/Ubuntu
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
if utils.CommandExists("greetd") {
greetdFound := utils.CommandExists("greetd")
if !greetdFound {
for _, p := range []string{"/usr/sbin/greetd", "/sbin/greetd"} {
if _, err := os.Stat(p); err == nil {
greetdFound = true
break
}
}
}
if greetdFound {
logFunc("✓ greetd is already installed")
return nil
}
@@ -142,6 +148,14 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
}
case distros.FamilyGentoo:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
"emerge --ask n sys-apps/greetd")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-apps/greetd")
}
case distros.FamilyNix:
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
@@ -160,13 +174,124 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
return nil
}
// IsGreeterPackaged returns true if dms-greeter was installed from a system package.
func IsGreeterPackaged() bool {
if !utils.CommandExists("dms-greeter") {
return false
}
packagedPath := "/usr/share/quickshell/dms-greeter"
info, err := os.Stat(packagedPath)
return err == nil && info.IsDir()
}
// HasLegacyLocalGreeterWrapper returns true when a manually installed wrapper exists.
func HasLegacyLocalGreeterWrapper() bool {
info, err := os.Stat("/usr/local/bin/dms-greeter")
return err == nil && !info.IsDir()
}
// TryInstallGreeterPackage attempts to install dms-greeter from the distro's official repo.
func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return false
}
if IsGreeterPackaged() {
logFunc("✓ dms-greeter package already installed")
return true
}
ctx := context.Background()
var installCmd *exec.Cmd
var failHint string
switch config.Family {
case distros.FamilyDebian:
obsSlug := getDebianOBSSlug(osInfo)
keyURL := fmt.Sprintf("https://download.opensuse.org/repositories/home:AvengeMedia:danklinux/%s/Release.key", obsSlug)
repoLine := fmt.Sprintf("deb [signed-by=/etc/apt/keyrings/danklinux.gpg] https://download.opensuse.org/repositories/home:/AvengeMedia:/danklinux/%s/ /", obsSlug)
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\ncurl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg\necho '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list\nsudo apt update && sudo apt install dms-greeter", keyURL, repoLine)
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
addKeyCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`curl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg`, keyURL))
addKeyCmd.Stdout = os.Stdout
addKeyCmd.Stderr = os.Stderr
addKeyCmd.Run()
addRepoCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`echo '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list`, repoLine))
addRepoCmd.Stdout = os.Stdout
addRepoCmd.Stderr = os.Stderr
addRepoCmd.Run()
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
case distros.FamilySUSE:
repoURL := getOpenSUSEOBSRepoURL(osInfo)
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
logFunc("Adding DankLinux OBS repository...")
addRepoCmd := exec.CommandContext(ctx, "sudo", "zypper", "addrepo", repoURL)
addRepoCmd.Stdout = os.Stdout
addRepoCmd.Stderr = os.Stderr
addRepoCmd.Run()
exec.CommandContext(ctx, "sudo", "zypper", "refresh").Run()
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "dms-greeter")
case distros.FamilyUbuntu:
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install dms-greeter"
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
ppacmd := exec.CommandContext(ctx, "sudo", "add-apt-repository", "-y", "ppa:avengemedia/danklinux")
ppacmd.Stdout = os.Stdout
ppacmd.Stderr = os.Stderr
ppacmd.Run()
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
case distros.FamilyFedora:
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
logFunc("Enabling COPR avengemedia/danklinux...")
coprcmd := exec.CommandContext(ctx, "sudo", "dnf", "copr", "enable", "-y", "avengemedia/danklinux")
coprcmd.Stdout = os.Stdout
coprcmd.Stderr = os.Stderr
coprcmd.Run()
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "dms-greeter")
case distros.FamilyArch:
aurHelper := ""
for _, helper := range []string{"paru", "yay"} {
if _, err := exec.LookPath(helper); err == nil {
aurHelper = helper
break
}
}
if aurHelper == "" {
logFunc("⚠ No AUR helper found (paru/yay). Install greetd-dms-greeter-git from AUR: https://aur.archlinux.org/packages/greetd-dms-greeter-git")
return false
}
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper)
installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git")
default:
return false
}
logFunc("Installing dms-greeter from official repository...")
installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr
if err := installCmd.Run(); err != nil {
logFunc(failHint)
return false
}
logFunc("✓ dms-greeter package installed")
return true
}
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
// Check if dms-greeter is already in PATH
if utils.CommandExists("dms-greeter") {
logFunc("✓ dms-greeter wrapper already installed")
} else {
// Install the wrapper script
assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets")
wrapperSrc := filepath.Join(assetsDir, "dms-greeter")
@@ -203,7 +328,6 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
}
}
// Create cache directory with proper permissions
cacheDir := "/var/cache/dms-greeter"
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
@@ -224,14 +348,90 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
return nil
}
// EnsureACLInstalled installs the acl package (setfacl/getfacl) if not already present
func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
if utils.CommandExists("setfacl") {
return nil
}
logFunc("setfacl not found installing acl package...")
osInfo, err := distros.GetOSInfo()
if err != nil {
return fmt.Errorf("failed to detect OS: %w", err)
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return fmt.Errorf("unsupported distribution for automatic acl installation: %s", osInfo.Distribution.ID)
}
ctx := context.Background()
var installCmd *exec.Cmd
switch config.Family {
case distros.FamilyArch:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "acl")
}
case distros.FamilyFedora:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "dnf install -y acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "acl")
}
case distros.FamilySUSE:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "zypper install -y acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl")
}
case distros.FamilyUbuntu, distros.FamilyDebian:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
}
case distros.FamilyGentoo:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-fs/acl")
}
case distros.FamilyNix:
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
default:
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
}
installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install acl: %w", err)
}
logFunc("✓ acl package installed")
return nil
}
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
if err := EnsureACLInstalled(logFunc, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: could not install acl package: %v", err))
logFunc(" ACL permissions will be skipped; theme sync may not work correctly.")
return nil
}
if !utils.CommandExists("setfacl") {
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
logFunc(" If theme sync doesn't work, you may need to install acl package:")
logFunc(" - Fedora/RHEL: sudo dnf install acl")
logFunc(" - Debian/Ubuntu: sudo apt-get install acl")
logFunc(" - Arch: sudo pacman -S acl")
// setfacl still not found after install attempt (e.g. unsupported filesystem)
logFunc("⚠ Warning: setfacl still not available after install attempt; skipping ACL setup.")
return nil
}
@@ -264,7 +464,6 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
}
}
// Set ACL to allow greeter user read+execute permission (for session discovery)
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path))
@@ -299,7 +498,6 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
if err == nil && strings.Contains(string(groupsOutput), group) {
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
} else {
// Add current user to greeter group for file access permissions
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
}
@@ -339,7 +537,6 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
logFunc(fmt.Sprintf("✓ Set group permissions for %s", dir.desc))
}
// Set up ACLs on parent directories to allow greeter user traversal
if err := SetupParentDirectoryACLs(logFunc, sudoPassword); err != nil {
return fmt.Errorf("failed to setup parent directory ACLs: %w", err)
}
@@ -393,7 +590,7 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
}
}
runSudoCmd(sudoPassword, "rm", "-f", link.target) //nolint:errcheck
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err))
@@ -787,15 +984,19 @@ user = "%s"
}
}
// Determine wrapper command path
// If dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter
wrapperCmd := "dms-greeter"
if !utils.CommandExists("dms-greeter") {
wrapperCmd = "/usr/local/bin/dms-greeter"
}
// Build command based on compositor and dms path
compositorLower := strings.ToLower(compositor)
command := fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath)
var command string
if dmsPath == "" {
command = fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
} else {
command = fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath)
}
var finalLines []string
inDefaultSession := false
@@ -832,7 +1033,11 @@ user = "%s"
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
}
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s --command %s -p %s)", greeterUser, wrapperCmd, compositorLower, dmsPath))
cmdDesc := fmt.Sprintf("%s --command %s", wrapperCmd, compositorLower)
if dmsPath != "" {
cmdDesc = fmt.Sprintf("%s -p %s", cmdDesc, dmsPath)
}
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, cmdDesc))
return nil
}
@@ -867,6 +1072,47 @@ func stripConfigFlag(command string) string {
return command
}
// getDebianOBSSlug returns the OBS repository slug for the running Debian version.
func getDebianOBSSlug(osInfo *distros.OSInfo) string {
versionID := strings.ToLower(osInfo.VersionID)
codename := strings.ToLower(osInfo.VersionCodename)
prettyName := strings.ToLower(osInfo.PrettyName)
if strings.Contains(prettyName, "sid") || strings.Contains(prettyName, "unstable") ||
codename == "sid" || versionID == "sid" {
return "Debian_Unstable"
}
if versionID == "testing" || codename == "testing" {
return "Debian_Testing"
}
if versionID != "" {
return "Debian_" + versionID // "Debian_13"
}
return "Debian_Unstable"
}
// getOpenSUSEOBSRepoURL returns the OBS .repo file URL for the running openSUSE variant.
func getOpenSUSEOBSRepoURL(osInfo *distros.OSInfo) string {
const base = "https://download.opensuse.org/repositories/home:AvengeMedia:danklinux"
var slug string
switch osInfo.Distribution.ID {
case "opensuse-leap":
v := osInfo.VersionID
if v != "" && !strings.Contains(v, ".") {
v += ".0" // "16" → "16.0"
}
if v == "" {
v = "16.0"
}
slug = v
case "opensuse-slowroll":
slug = "openSUSE_Slowroll"
default: // opensuse-tumbleweed || unknown version
slug = "openSUSE_Tumbleweed"
}
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
}
func runSudoCmd(sudoPassword string, command string, args ...string) error {
var cmd *exec.Cmd
@@ -887,3 +1133,140 @@ func runSudoCmd(sudoPassword string, command string, args ...string) error {
cmd.Stderr = os.Stderr
return cmd.Run()
}
func checkSystemdEnabled(service string) (string, error) {
cmd := exec.Command("systemctl", "is-enabled", service)
output, _ := cmd.Output()
return strings.TrimSpace(string(output)), nil
}
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
for _, dm := range conflictingDMs {
state, err := checkSystemdEnabled(dm)
if err != nil || state == "" || state == "not-found" {
continue
}
switch state {
case "enabled", "enabled-runtime", "static", "indirect", "alias":
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
if err := runSudoCmd(sudoPassword, "systemctl", "disable", "--now", dm); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
} else {
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
}
}
}
return nil
}
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
func EnableGreetd(sudoPassword string, logFunc func(string)) error {
state, err := checkSystemdEnabled("greetd")
if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err)
}
if state == "not-found" {
return fmt.Errorf("greetd service not found; ensure greetd is installed")
}
if state == "masked" || state == "masked-runtime" {
logFunc(" Unmasking greetd...")
if err := runSudoCmd(sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
return fmt.Errorf("failed to unmask greetd: %w", err)
}
logFunc(" ✓ Unmasked greetd")
}
logFunc(" Enabling greetd service (--force)...")
if err := runSudoCmd(sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
logFunc("✓ greetd enabled")
return nil
}
func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
cmd := exec.Command("systemctl", "get-default")
output, err := cmd.Output()
if err != nil {
logFunc(fmt.Sprintf("⚠ Warning: could not get default systemd target: %v", err))
return nil
}
current := strings.TrimSpace(string(output))
if current == "graphical.target" {
logFunc("✓ Default target is already graphical.target")
return nil
}
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
if err := runSudoCmd(sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
return fmt.Errorf("failed to set graphical target: %w", err)
}
logFunc("✓ Default target set to graphical.target")
return nil
}
// AutoSetupGreeter performs the full non-interactive greeter setup
func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) error {
if IsGreeterPackaged() && HasLegacyLocalGreeterWrapper() {
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; " +
"remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
}
logFunc("Ensuring greetd is installed...")
if err := EnsureGreetdInstalled(logFunc, sudoPassword); err != nil {
return fmt.Errorf("greetd install failed: %w", err)
}
dmsPath := ""
if !IsGreeterPackaged() {
detected, err := DetectDMSPath()
if err != nil {
return fmt.Errorf("DMS installation not found: %w", err)
}
dmsPath = detected
logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath))
} else {
logFunc("✓ Using packaged dms-greeter (/usr/share/quickshell/dms-greeter)")
}
logFunc("Setting up dms-greeter group and permissions...")
if err := SetupDMSGroup(logFunc, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: group/permissions setup error: %v", err))
}
logFunc("Copying greeter files...")
if err := CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil {
return fmt.Errorf("failed to copy greeter files: %w", err)
}
logFunc("Configuring greetd...")
greeterPathForConfig := ""
if !IsGreeterPackaged() {
greeterPathForConfig = dmsPath
}
if err := ConfigureGreetd(greeterPathForConfig, compositor, logFunc, sudoPassword); err != nil {
return fmt.Errorf("failed to configure greetd: %w", err)
}
logFunc("Synchronizing DMS configurations...")
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
}
logFunc("Checking for conflicting display managers...")
if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: %v", err))
}
logFunc("Enabling greetd service...")
if err := EnableGreetd(sudoPassword, logFunc); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
logFunc("Ensuring graphical.target as default...")
if err := EnsureGraphicalTarget(sudoPassword, logFunc); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: %v", err))
}
logFunc("✓ DMS greeter setup complete")
return nil
}

View File

@@ -341,6 +341,8 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
val := arg.ValueString()
if val == "" {
parts = append(parts, `""`)
} else if strings.ContainsAny(val, " \t") {
parts = append(parts, `"`+strings.ReplaceAll(val, `"`, `\"`)+`"`)
} else {
parts = append(parts, val)
}

View File

@@ -1,7 +1,9 @@
package matugen
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
"os"
@@ -19,6 +21,8 @@ import (
"github.com/lucasb-eyer/go-colorful"
)
var ErrNoChanges = errors.New("no color changes")
type ColorMode string
const (
@@ -33,6 +37,7 @@ const (
TemplateKindTerminal
TemplateKindGTK
TemplateKindVSCode
TemplateKindEmacs
)
type TemplateDef struct {
@@ -53,7 +58,7 @@ var templateRegistry = []TemplateDef{
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
@@ -65,7 +70,7 @@ var templateRegistry = []TemplateDef{
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
}
func (c *ColorMode) GTKTheme() string {
@@ -78,7 +83,8 @@ func (c *ColorMode) GTKTheme() string {
}
var (
matugenVersionOnce sync.Once
matugenVersionMu sync.Mutex
matugenVersionOK bool
matugenSupportsCOE bool
matugenIsV4 bool
)
@@ -158,8 +164,14 @@ func Run(opts Options) error {
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
if err := buildOnce(&opts); err != nil {
return err
changed, buildErr := buildOnce(&opts)
if buildErr != nil {
return buildErr
}
if !changed {
log.Info("No color changes detected, skipping refresh")
return ErrNoChanges
}
if opts.SyncModeWithPortal {
@@ -170,25 +182,27 @@ func Run(opts Options) error {
return nil
}
func buildOnce(opts *Options) error {
func buildOnce(opts *Options) (bool, error) {
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
if err != nil {
return fmt.Errorf("failed to create temp config: %w", err)
return false, fmt.Errorf("failed to create temp config: %w", err)
}
defer os.Remove(cfgFile.Name())
defer cfgFile.Close()
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
if err != nil {
return fmt.Errorf("failed to create temp dir: %w", err)
return false, fmt.Errorf("failed to create temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
return fmt.Errorf("failed to build config: %w", err)
return false, fmt.Errorf("failed to build config: %w", err)
}
cfgFile.Close()
oldColors, _ := os.ReadFile(opts.ColorsOutput())
var primaryDark, primaryLight, surface string
var dank16JSON string
var importArgs []string
@@ -200,7 +214,7 @@ func buildOnce(opts *Options) error {
surface = extractNestedColor(opts.StockColors, "surface", "dark")
if primaryDark == "" {
return fmt.Errorf("failed to extract primary dark from stock colors")
return false, fmt.Errorf("failed to extract primary dark from stock colors")
}
if primaryLight == "" {
primaryLight = primaryDark
@@ -214,14 +228,14 @@ func buildOnce(opts *Options) error {
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
args = append(args, importArgs...)
if err := runMatugen(args); err != nil {
return err
return false, err
}
} else {
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
matJSON, err := runMatugenDryRun(opts)
if err != nil {
return fmt.Errorf("matugen dry-run failed: %w", err)
return false, fmt.Errorf("matugen dry-run failed: %w", err)
}
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
@@ -229,7 +243,7 @@ func buildOnce(opts *Options) error {
surface = extractMatugenColor(matJSON, "surface", "dark")
if primaryDark == "" {
return fmt.Errorf("failed to extract primary color")
return false, fmt.Errorf("failed to extract primary color")
}
if primaryLight == "" {
primaryLight = primaryDark
@@ -250,10 +264,15 @@ func buildOnce(opts *Options) error {
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
args = append(args, importArgs...)
if err := runMatugen(args); err != nil {
return err
return false, err
}
}
newColors, _ := os.ReadFile(opts.ColorsOutput())
if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 {
return false, nil
}
if isDMSGTKActive(opts.ConfigDir) {
switch opts.Mode {
case ColorModeLight:
@@ -271,7 +290,7 @@ func buildOnce(opts *Options) error {
signalTerminals(opts)
return nil
return true, nil
}
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
@@ -334,6 +353,10 @@ output_path = '%s'
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
}
default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
}
@@ -491,6 +514,9 @@ func substituteVars(content, shellDir string) string {
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
}
return result
}
@@ -511,79 +537,160 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
return content[startIdx : startIdx+endIdx]
}
func checkMatugenVersion() {
matugenVersionOnce.Do(func() {
cmd := exec.Command("matugen", "--version")
output, err := cmd.Output()
if err != nil {
return
}
versionStr := strings.TrimSpace(string(output))
versionStr = strings.TrimPrefix(versionStr, "matugen ")
parts := strings.Split(versionStr, ".")
if len(parts) < 2 {
return
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return
}
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
matugenIsV4 = major >= 4
if matugenSupportsCOE {
log.Infof("Matugen %s supports --continue-on-error", versionStr)
}
if matugenIsV4 {
log.Infof("Matugen %s: using v4 flags", versionStr)
}
})
type matugenFlags struct {
supportsCOE bool
isV4 bool
}
func runMatugen(args []string) error {
checkMatugenVersion()
func detectMatugenVersion() (matugenFlags, error) {
matugenVersionMu.Lock()
defer matugenVersionMu.Unlock()
if matugenVersionOK {
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
}
return detectMatugenVersionLocked()
}
func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
matugenVersionMu.Lock()
defer matugenVersionMu.Unlock()
matugenVersionOK = false
flags, err := detectMatugenVersionLocked()
if err != nil {
return old, false
}
changed := flags.supportsCOE != old.supportsCOE || flags.isV4 != old.isV4
return flags, changed
}
func detectMatugenVersionLocked() (matugenFlags, error) {
cmd := exec.Command("matugen", "--version")
output, err := cmd.Output()
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
}
versionStr := strings.TrimSpace(string(output))
versionStr = strings.TrimPrefix(versionStr, "matugen ")
parts := strings.Split(versionStr, ".")
if len(parts) < 2 {
return matugenFlags{}, fmt.Errorf("unexpected matugen version format: %q", versionStr)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to parse matugen major version %q: %w", parts[0], err)
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to parse matugen minor version %q: %w", parts[1], err)
}
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
matugenIsV4 = major >= 4
matugenVersionOK = true
if matugenSupportsCOE {
args = append([]string{"--continue-on-error"}, args...)
log.Infof("Matugen %s supports --continue-on-error", versionStr)
}
if matugenIsV4 {
log.Infof("Matugen %s: using v4 flags", versionStr)
}
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
}
func buildMatugenArgs(baseArgs []string, flags matugenFlags) []string {
args := make([]string, 0, len(baseArgs)+4)
if flags.supportsCOE {
args = append(args, "--continue-on-error")
}
args = append(args, baseArgs...)
if flags.isV4 {
args = append(args, "--source-color-index", "0")
}
return args
}
func runMatugen(baseArgs []string) error {
flags, err := detectMatugenVersion()
if err != nil {
return err
}
args := buildMatugenArgs(baseArgs, flags)
cmd := exec.Command("matugen", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
runErr := cmd.Run()
if runErr == nil {
return nil
}
log.Warnf("Matugen failed (v4=%v): %v", flags.isV4, runErr)
newFlags, changed := redetectMatugenVersion(flags)
if !changed {
return runErr
}
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
args = buildMatugenArgs(baseArgs, newFlags)
retryCmd := exec.Command("matugen", args...)
retryCmd.Stdout = os.Stdout
retryCmd.Stderr = os.Stderr
return retryCmd.Run()
}
func runMatugenDryRun(opts *Options) (string, error) {
checkMatugenVersion()
var args []string
switch opts.Kind {
case "hex":
args = []string{"color", "hex", opts.Value}
default:
args = []string{opts.Kind, opts.Value}
}
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
if matugenIsV4 {
args = append(args, "--source-color-index", "0", "--old-json-output")
}
cmd := exec.Command("matugen", args...)
output, err := cmd.Output()
flags, err := detectMatugenVersion()
if err != nil {
return "", err
}
output, dryErr := execDryRun(opts, flags)
if dryErr == nil {
return output, nil
}
log.Warnf("Matugen dry-run failed (v4=%v): %v", flags.isV4, dryErr)
newFlags, changed := redetectMatugenVersion(flags)
if !changed {
return "", dryErr
}
log.Warnf("Matugen version changed (v4: %v -> %v), retrying dry-run", flags.isV4, newFlags.isV4)
return execDryRun(opts, newFlags)
}
func execDryRun(opts *Options, flags matugenFlags) (string, error) {
var baseArgs []string
switch opts.Kind {
case "hex":
baseArgs = []string{"color", "hex", opts.Value}
default:
baseArgs = []string{opts.Kind, opts.Value}
}
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
if flags.isV4 {
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
}
cmd := exec.Command("matugen", baseArgs...)
var stderr strings.Builder
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
if stderr.Len() > 0 {
return "", fmt.Errorf("matugen %v failed (v4=%v): %s", baseArgs, flags.isV4, strings.TrimSpace(stderr.String()))
}
return "", fmt.Errorf("matugen %v failed (v4=%v): %w", baseArgs, flags.isV4, err)
}
return strings.ReplaceAll(string(output), "\n", ""), nil
}
@@ -819,6 +926,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
detected = true
case tmpl.Kind == TemplateKindVSCode:
detected = checkVSCodeExtension(homeDir)
case tmpl.Kind == TemplateKindEmacs:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
default:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
}

View File

@@ -2,6 +2,7 @@ package matugen
import (
"context"
"errors"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -93,10 +94,13 @@ func (q *Queue) runWorker() {
err := Run(job.Options)
var result Result
if err != nil {
result = Result{Success: false, Error: err}
} else {
switch {
case err == nil:
result = Result{Success: true}
case errors.Is(err, ErrNoChanges):
result = Result{Success: true}
default:
result = Result{Success: false, Error: err}
}
q.finishJob(result)

View File

@@ -1062,6 +1062,62 @@ func (_c *MockBackend_GetWiFiNetworkDetails_Call) RunAndReturn(run func(string)
return _c
}
// GetWiFiQRCodeContent provides a mock function with given fields: ssid
func (_m *MockBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
ret := _m.Called(ssid)
if len(ret) == 0 {
panic("no return value specified for GetWiFiQRCodeContent")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
return rf(ssid)
}
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(ssid)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(ssid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockBackend_GetWiFiQRCodeContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWiFiQRCodeContent'
type MockBackend_GetWiFiQRCodeContent_Call struct {
*mock.Call
}
// GetWiFiQRCodeContent is a helper method to define mock.On call
// - ssid string
func (_e *MockBackend_Expecter) GetWiFiQRCodeContent(ssid interface{}) *MockBackend_GetWiFiQRCodeContent_Call {
return &MockBackend_GetWiFiQRCodeContent_Call{Call: _e.mock.On("GetWiFiQRCodeContent", ssid)}
}
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Run(run func(ssid string)) *MockBackend_GetWiFiQRCodeContent_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Return(_a0 string, _a1 error) *MockBackend_GetWiFiQRCodeContent_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockBackend_GetWiFiQRCodeContent_Call) RunAndReturn(run func(string) (string, error)) *MockBackend_GetWiFiQRCodeContent_Call {
_c.Call.Return(run)
return _c
}
// GetWiredConnections provides a mock function with no fields
func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) {
ret := _m.Called()

View File

@@ -258,7 +258,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy != nil {
if proxy != nil && !proxy.IsZombie() {
e.WorkspaceGroup = proxy.(*ExtWorkspaceGroupHandleV1)
} else {
groupHandle := &ExtWorkspaceGroupHandleV1{}
@@ -278,7 +278,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy != nil {
if proxy != nil && !proxy.IsZombie() {
e.Workspace = proxy.(*ExtWorkspaceHandleV1)
} else {
wsHandle := &ExtWorkspaceHandleV1{}

View File

@@ -108,7 +108,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
screenshoter: s,
outputs: make(map[uint32]*WaylandOutput),
preCapture: make(map[*WaylandOutput]*PreCapture),
showCapturedCursor: true,
showCapturedCursor: s.config.Cursor == CursorOn,
}
}

View File

@@ -453,10 +453,7 @@ func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted
}
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
cursor := int32(0)
if s.config.IncludeCursor {
cursor = 1
}
cursor := int32(s.config.Cursor)
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
if err != nil {
@@ -624,10 +621,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
}
}
cursor := int32(0)
if s.config.IncludeCursor {
cursor = 1
}
cursor := int32(s.config.Cursor)
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
if err != nil {

View File

@@ -19,6 +19,13 @@ const (
FormatPPM
)
type CursorMode int
const (
CursorOff CursorMode = iota
CursorOn
)
type Region struct {
X int32 `json:"x"`
Y int32 `json:"y"`
@@ -42,29 +49,29 @@ type Output struct {
}
type Config struct {
Mode Mode
OutputName string
IncludeCursor bool
Format Format
Quality int
OutputDir string
Filename string
Clipboard bool
SaveFile bool
Notify bool
Stdout bool
Mode Mode
OutputName string
Cursor CursorMode
Format Format
Quality int
OutputDir string
Filename string
Clipboard bool
SaveFile bool
Notify bool
Stdout bool
}
func DefaultConfig() Config {
return Config{
Mode: ModeRegion,
IncludeCursor: false,
Format: FormatPNG,
Quality: 90,
OutputDir: "",
Filename: "",
Clipboard: true,
SaveFile: true,
Notify: true,
Mode: ModeRegion,
Cursor: CursorOff,
Format: FormatPNG,
Quality: 90,
OutputDir: "",
Filename: "",
Clipboard: true,
SaveFile: true,
Notify: true,
}
}

View File

@@ -311,6 +311,10 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
select {
case m.eventQueue <- func() {
time.Sleep(300 * time.Millisecond)
log.Infof("[Bluetooth] Auto-trusting newly paired device: %s", devicePath)
if err := m.TrustDevice(devicePath, true); err != nil {
log.Warnf("[Bluetooth] Auto-trust failed: %v", err)
}
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
if err := m.ConnectDevice(devicePath); err != nil {
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)

View File

@@ -232,8 +232,15 @@ func (m *Manager) setupDataDeviceSync() {
return
}
prevOffer := m.currentOffer
m.currentOffer = offer
if prevOffer != nil && prevOffer != offer {
m.offerMutex.Lock()
delete(m.offerMimeTypes, prevOffer)
m.offerMutex.Unlock()
}
m.offerMutex.RLock()
mimes := m.offerMimeTypes[offer]
m.offerMutex.RUnlock()
@@ -587,20 +594,26 @@ func (m *Manager) uriListPreview(data []byte) (string, bool) {
uris = strings.Split(text, "\n")
}
if len(uris) > 1 {
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
}
if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") {
filePath := strings.TrimPrefix(uris[0], "file://")
if info, err := os.Stat(filePath); err == nil && !info.IsDir() {
info, err := os.Stat(filePath)
if err != nil || info.IsDir() {
return m.textPreview(data), false
}
cfg := m.getConfig()
if info.Size() <= cfg.MaxEntrySize {
if imgData, err := os.ReadFile(filePath); err == nil {
if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); err == nil {
return fmt.Sprintf("[[ file %s %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true
}
}
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
}
}
if len(uris) > 1 {
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
}
return m.textPreview(data), false
@@ -623,6 +636,11 @@ func (m *Manager) tryReadImageFromURI(data []byte) ([]byte, string, bool) {
return nil, "", false
}
cfg := m.getConfig()
if info.Size() > cfg.MaxEntrySize {
return nil, "", false
}
imgData, err := os.ReadFile(filePath)
if err != nil {
return nil, "", false

View File

@@ -16,4 +16,8 @@ const (
dbusScreensaverPath = "/ScreenSaver"
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
dbusGnomeScreensaverName = "org.gnome.ScreenSaver"
dbusGnomeScreensaverPath = "/org/gnome/ScreenSaver"
dbusGnomeScreensaverInterface = "org.gnome.ScreenSaver"
)

View File

@@ -191,6 +191,12 @@ func (m *Manager) Close() {
return true
})
m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
close(ch)
m.screensaverSubscribers.Delete(key)
return true
})
if m.systemConn != nil {
m.systemConn.Close()
}

View File

@@ -1,6 +1,7 @@
package freedesktop
import (
"fmt"
"path/filepath"
"strings"
"sync/atomic"
@@ -15,45 +16,9 @@ type screensaverHandler struct {
manager *Manager
}
func (m *Manager) initializeScreensaver() error {
if m.sessionConn == nil {
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue)
if err != nil {
log.Warnf("Failed to request screensaver name: %v", err)
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name already owned by another process")
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
handler := &screensaverHandler{manager: m}
if err := m.sessionConn.Export(handler, dbusScreensaverPath, dbusScreensaverInterface); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath, err)
return nil
}
if err := m.sessionConn.Export(handler, dbusScreensaverPath2, dbusScreensaverInterface); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath2, err)
return nil
}
screensaverIface := introspect.Interface{
Name: dbusScreensaverInterface,
func screensaverIntrospectIface(ifaceName string) introspect.Interface {
return introspect.Interface{
Name: ifaceName,
Methods: []introspect.Method{
{
Name: "Inhibit",
@@ -69,40 +34,106 @@ func (m *Manager) initializeScreensaver() error {
{Name: "cookie", Type: "u", Direction: "in"},
},
},
{
Name: "GetActive",
Args: []introspect.Arg{
{Name: "active", Type: "b", Direction: "out"},
},
},
{
Name: "SetActive",
Args: []introspect.Arg{
{Name: "active", Type: "b", Direction: "in"},
},
},
{
Name: "Lock",
},
},
Signals: []introspect.Signal{
{
Name: "ActiveChanged",
Args: []introspect.Arg{
{Name: "new_value", Type: "b"},
},
},
},
}
}
func (m *Manager) initializeScreensaver() error {
if m.sessionConn == nil {
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
introNode := &introspect.Node{
Name: dbusScreensaverPath,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
screensaverIface,
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath, err)
}
handler := &screensaverHandler{manager: m}
introNode2 := &introspect.Node{
Name: dbusScreensaverPath2,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
screensaverIface,
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
m.screensaverFreedesktopClaimed = m.claimScreensaverName(handler,
dbusScreensaverName, dbusScreensaverInterface, dbusScreensaverPath, dbusScreensaverPath2)
m.screensaverGnomeClaimed = m.claimScreensaverName(handler,
dbusGnomeScreensaverName, dbusGnomeScreensaverInterface, dbusGnomeScreensaverPath)
if !m.screensaverFreedesktopClaimed && !m.screensaverGnomeClaimed {
log.Warn("No screensaver interface could be claimed")
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
go m.watchPeerDisconnects()
m.stateMutex.Lock()
m.state.Screensaver.Available = true
m.state.Screensaver.Active = false
m.state.Screensaver.Inhibited = false
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
m.stateMutex.Unlock()
log.Info("Screensaver inhibit listener initialized")
log.Info("Screensaver listener initialized")
return nil
}
func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface string, paths ...dbus.ObjectPath) bool {
reply, err := m.sessionConn.RequestName(name, dbus.NameFlagDoNotQueue)
if err != nil {
log.Warnf("Failed to request screensaver name %s: %v", name, err)
return false
}
if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name %s already owned by another process", name)
return false
}
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", name, err)
return false
}
log.Infof("Claimed %s on session bus", name)
return true
}
// exportScreensaverOnPaths exports the handler and introspection on the given
// paths under the specified interface name.
func (m *Manager) exportScreensaverOnPaths(handler *screensaverHandler, ifaceName string, paths ...dbus.ObjectPath) error {
iface := screensaverIntrospectIface(ifaceName)
for _, path := range paths {
if err := m.sessionConn.Export(handler, path, ifaceName); err != nil {
return fmt.Errorf("export handler on %s: %w", path, err)
}
node := &introspect.Node{
Name: string(path),
Interfaces: []introspect.Interface{
introspect.IntrospectData,
iface,
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(node), path, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", path, err)
}
}
return nil
}
@@ -268,3 +299,51 @@ func (m *Manager) NotifyScreensaverSubscribers() {
return true
})
}
func (h *screensaverHandler) GetActive() (bool, *dbus.Error) {
h.manager.stateMutex.RLock()
active := h.manager.state.Screensaver.Active
h.manager.stateMutex.RUnlock()
return active, nil
}
func (h *screensaverHandler) SetActive(active bool) *dbus.Error {
h.manager.SetScreenLockActive(active)
return nil
}
func (h *screensaverHandler) Lock() *dbus.Error {
h.manager.SetScreenLockActive(true)
return nil
}
func (m *Manager) SetScreenLockActive(active bool) {
m.stateMutex.Lock()
changed := m.state.Screensaver.Active != active
m.state.Screensaver.Active = active
m.stateMutex.Unlock()
if !changed {
return
}
log.Infof("Screen lock active changed: %v", active)
defer m.NotifyScreensaverSubscribers()
if m.sessionConn == nil {
return
}
if m.screensaverFreedesktopClaimed {
if err := m.sessionConn.Emit(dbusScreensaverPath, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath, err)
}
if err := m.sessionConn.Emit(dbusScreensaverPath2, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath2, err)
}
}
if m.screensaverGnomeClaimed {
if err := m.sessionConn.Emit(dbusGnomeScreensaverPath, dbusGnomeScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusGnomeScreensaverPath, err)
}
}
}

View File

@@ -0,0 +1,102 @@
package freedesktop
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSetScreenLockActive_ChangesState(t *testing.T) {
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true},
},
stateMutex: sync.RWMutex{},
}
assert.False(t, manager.GetScreensaverState().Active)
manager.SetScreenLockActive(true)
assert.True(t, manager.GetScreensaverState().Active)
manager.SetScreenLockActive(false)
assert.False(t, manager.GetScreensaverState().Active)
}
func TestSetScreenLockActive_NoChangeNoDuplicate(t *testing.T) {
ch := make(chan ScreensaverState, 64)
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true, Active: false},
},
stateMutex: sync.RWMutex{},
}
manager.screensaverSubscribers.Store("test", ch)
defer manager.screensaverSubscribers.Delete("test")
// Setting to same value should not notify
manager.SetScreenLockActive(false)
select {
case <-ch:
t.Fatal("should not have received notification for no-change")
case <-time.After(50 * time.Millisecond):
// Expected: no notification
}
}
func TestSetScreenLockActive_NotifiesSubscribers(t *testing.T) {
ch := make(chan ScreensaverState, 64)
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true, Active: false},
},
stateMutex: sync.RWMutex{},
}
manager.screensaverSubscribers.Store("test", ch)
defer manager.screensaverSubscribers.Delete("test")
manager.SetScreenLockActive(true)
select {
case state := <-ch:
assert.True(t, state.Active)
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber notification")
}
}
func TestSetScreenLockActive_NilSessionConn(t *testing.T) {
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true},
},
stateMutex: sync.RWMutex{},
}
assert.NotPanics(t, func() {
manager.SetScreenLockActive(true)
})
assert.True(t, manager.GetScreensaverState().Active)
}
func TestGetActive_ReturnsCurrentState(t *testing.T) {
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true, Active: true},
},
stateMutex: sync.RWMutex{},
}
handler := &screensaverHandler{manager: manager}
active, dbusErr := handler.GetActive()
assert.Nil(t, dbusErr)
assert.True(t, active)
}
func TestScreensaverState_ActiveDefaultsFalse(t *testing.T) {
state := ScreensaverState{}
assert.False(t, state.Active)
}

View File

@@ -39,6 +39,7 @@ type ScreensaverInhibitor struct {
type ScreensaverState struct {
Available bool `json:"available"`
Active bool `json:"active"`
Inhibited bool `json:"inhibited"`
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
}
@@ -50,14 +51,16 @@ type FreedeskState struct {
}
type Manager struct {
state *FreedeskState
stateMutex sync.RWMutex
systemConn *dbus.Conn
sessionConn *dbus.Conn
accountsObj dbus.BusObject
settingsObj dbus.BusObject
currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState]
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
screensaverCookieCounter uint32
state *FreedeskState
stateMutex sync.RWMutex
systemConn *dbus.Conn
sessionConn *dbus.Conn
accountsObj dbus.BusObject
settingsObj dbus.BusObject
currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState]
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
screensaverCookieCounter uint32
screensaverFreedesktopClaimed bool
screensaverGnomeClaimed bool
}

View File

@@ -10,6 +10,7 @@ type Backend interface {
ScanWiFi() error
ScanWiFiDevice(device string) error
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
GetWiFiQRCodeContent(ssid string) (string, error)
GetWiFiDevices() []WiFiDevice
ConnectWiFi(req ConnectionRequest) error

View File

@@ -111,6 +111,10 @@ func (b *HybridIwdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkI
return b.wifi.GetWiFiNetworkDetails(ssid)
}
func (b *HybridIwdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
return b.wifi.GetWiFiQRCodeContent(ssid)
}
func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
if err := b.wifi.ConnectWiFi(req); err != nil {
return err

View File

@@ -1,6 +1,9 @@
package network
import "fmt"
import (
"fmt"
"os"
)
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
return nil, fmt.Errorf("wired connections not supported by iwd")
@@ -112,3 +115,19 @@ func (b *IWDBackend) getWiFiDevicesLocked() []WiFiDevice {
Networks: b.state.WiFiNetworks,
}}
}
func (b *IWDBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
path := iwdConfigPath(ssid)
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("no saved iwd config for `%s`: %w", ssid, err)
}
passphrase, err := parseIWDPassphrase(string(data))
if err != nil {
return "", fmt.Errorf("failed to read passphrase for `%s`: %w", ssid, err)
}
return FormatWiFiQRString("WPA", ssid, passphrase), nil
}

View File

@@ -18,6 +18,10 @@ func (b *SystemdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInf
return nil, fmt.Errorf("WiFi details not supported by networkd backend")
}
func (b *SystemdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
return "", fmt.Errorf("WiFi QR Code not supported by networkd backend")
}
func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
return fmt.Errorf("WiFi connect not supported by networkd backend")
}

View File

@@ -196,6 +196,65 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
}, nil
}
func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
conn, err := b.findConnection(ssid)
if err != nil {
return "", fmt.Errorf("no saved connection for `%s`: %w", ssid, err)
}
connSettings, err := conn.GetSettings()
if err != nil {
return "", fmt.Errorf("failed to get settings for `%s`: %w", ssid, err)
}
secSettings, ok := connSettings["802-11-wireless-security"]
if !ok {
return "", fmt.Errorf("network `%s` has no security settings", ssid)
}
keyMgmt, ok := secSettings["key-mgmt"].(string)
if !ok {
return "", fmt.Errorf("failed to identify security type of network `%s`", ssid)
}
var securityType string
switch keyMgmt {
case "none":
authAlg, _ := secSettings["auth-alg"].(string)
switch authAlg {
case "open":
securityType = "nopass"
default:
securityType = "WEP"
}
case "ieee8021x":
securityType = "WEP"
default:
securityType = "WPA"
}
if securityType != "WPA" {
return "", fmt.Errorf("QR code generation only supports WPA connections, `%s` uses %s", ssid, securityType)
}
secrets, err := conn.GetSecrets("802-11-wireless-security")
if err != nil {
return "", fmt.Errorf("failed to retrieve connection secrets for `%s`: %w", ssid, err)
}
secSecrets, ok := secrets["802-11-wireless-security"]
if !ok {
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
}
psk, ok := secSecrets["psk"].(string)
if !ok {
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
}
return FormatWiFiQRString(securityType, ssid, psk), nil
}
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
devInfo, err := b.getWifiDeviceForConnection(req.Device)
if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
@@ -40,6 +41,10 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
handleSetPreference(conn, req, manager)
case "network.info":
handleGetNetworkInfo(conn, req, manager)
case "network.qrcode":
handleGetNetworkQRCode(conn, req, manager)
case "network.delete-qrcode":
handleDeleteQRCode(conn, req, manager)
case "network.ethernet.info":
handleGetWiredNetworkInfo(conn, req, manager)
case "network.subscribe":
@@ -320,6 +325,42 @@ func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, network)
}
func handleGetNetworkQRCode(conn net.Conn, req models.Request, manager *Manager) {
ssid, err := params.String(req.Params, "ssid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
content, err := manager.GetNetworkQRCode(ssid)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, content)
}
func handleDeleteQRCode(conn net.Conn, req models.Request, _ *Manager) {
path, err := params.String(req.Params, "path")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if !isValidQRCodePath(path) {
models.RespondError(conn, req.ID, "invalid QR code path")
return
}
if err := os.Remove(path); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "QR code file deleted"})
}
func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
uuid, err := params.String(req.Params, "uuid")
if err != nil {

View File

@@ -6,6 +6,8 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard"
)
func NewManager() (*Manager, error) {
@@ -438,6 +440,43 @@ func (m *Manager) GetNetworkInfoDetailed(ssid string) (*NetworkInfoResponse, err
return m.backend.GetWiFiNetworkDetails(ssid)
}
func (m *Manager) GetNetworkQRCode(ssid string) ([2]string, error) {
content, err := m.backend.GetWiFiQRCodeContent(ssid)
if err != nil {
return [2]string{}, err
}
qrc, err := qrcode.New(content)
if err != nil {
return [2]string{}, fmt.Errorf("failed to create QR code for `%s`: %w", ssid, err)
}
pathThemed, pathNormal := qrCodePaths(ssid)
wThemed, err := standard.New(
pathThemed,
standard.WithBuiltinImageEncoder(standard.PNG_FORMAT),
standard.WithBgTransparent(),
standard.WithFgColorRGBHex("#ffffff"),
)
if err != nil {
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
}
if err := qrc.Save(wThemed); err != nil {
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
}
wNormal, err := standard.New(pathNormal, standard.WithBuiltinImageEncoder(standard.PNG_FORMAT))
if err != nil {
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
}
if err := qrc.Save(wNormal); err != nil {
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
}
return [2]string{pathThemed, pathNormal}, nil
}
func (m *Manager) ToggleWiFi() error {
enabled, err := m.backend.GetWiFiEnabled()
if err != nil {

View File

@@ -0,0 +1,59 @@
package network
import (
"fmt"
"path/filepath"
"regexp"
"strings"
)
const qrCodeTmpPrefix = "/tmp/dank-wifi-qrcode-"
func FormatWiFiQRString(securityType, ssid, password string) string {
return fmt.Sprintf("WIFI:T:%s;S:%s;P:%s;;", securityType, ssid, password)
}
func qrCodePaths(ssid string) (themed, normal string) {
safe := sanitizeSSIDForPath(ssid)
themed = fmt.Sprintf("%s%s-themed.png", qrCodeTmpPrefix, safe)
normal = fmt.Sprintf("%s%s-normal.png", qrCodeTmpPrefix, safe)
return
}
func isValidQRCodePath(path string) bool {
clean := filepath.Clean(path)
return strings.HasPrefix(clean, qrCodeTmpPrefix) && strings.HasSuffix(clean, ".png")
}
var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
func sanitizeSSIDForPath(ssid string) string {
return safePathChar.ReplaceAllString(ssid, "_")
}
var iwdVerbatimSSID = regexp.MustCompile(`^[a-zA-Z0-9 _-]+$`)
func iwdConfigPath(ssid string) string {
switch {
case iwdVerbatimSSID.MatchString(ssid):
return fmt.Sprintf("/var/lib/iwd/%s.psk", ssid)
default:
return fmt.Sprintf("/var/lib/iwd/=%x.psk", []byte(ssid))
}
}
func parseIWDPassphrase(data string) (string, error) {
inSecurity := false
for _, line := range strings.Split(data, "\n") {
line = strings.TrimSpace(line)
switch {
case line == "[Security]":
inSecurity = true
case strings.HasPrefix(line, "["):
inSecurity = false
case inSecurity && strings.HasPrefix(line, "Passphrase="):
return strings.TrimPrefix(line, "Passphrase="), nil
}
}
return "", fmt.Errorf("no passphrase found in iwd config")
}

View File

@@ -1516,7 +1516,11 @@ func Start(printDocs bool) error {
}
}()
loginctlReady := make(chan struct{})
freedesktopReady := make(chan struct{})
go func() {
defer close(loginctlReady)
if err := InitializeLoginctlManager(); err != nil {
log.Warnf("Loginctl manager unavailable: %v", err)
} else {
@@ -1525,6 +1529,7 @@ func Start(printDocs bool) error {
}()
go func() {
defer close(freedesktopReady)
if err := InitializeFreedeskManager(); err != nil {
log.Warnf("Freedesktop manager unavailable: %v", err)
} else if freedesktopManager != nil {
@@ -1533,6 +1538,31 @@ func Start(printDocs bool) error {
}
}()
// Bridge loginctl lock state to the freedesktop/gnome screensaver
// ActiveChanged signal so apps like Bitwarden can detect screen lock.
go func() {
<-loginctlReady
<-freedesktopReady
if loginctlManager == nil || freedesktopManager == nil {
return
}
ch := loginctlManager.Subscribe("dms-lock-bridge")
defer loginctlManager.Unsubscribe("dms-lock-bridge")
initial := loginctlManager.GetState()
lastLocked := initial.Locked
freedesktopManager.SetScreenLockActive(lastLocked)
for state := range ch {
if state.Locked != lastLocked {
lastLocked = state.Locked
freedesktopManager.SetScreenLockActive(lastLocked)
}
}
}()
if err := InitializeWaylandManager(); err != nil {
log.Warnf("Wayland manager unavailable: %v", err)
}
@@ -1569,6 +1599,13 @@ func Start(printDocs bool) error {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
}
fatalErrChan := make(chan error, 1)

View File

@@ -162,7 +162,7 @@ func TestCleanupStaleSockets(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("XDG_RUNTIME_DIR", tempDir)
staleSocket := filepath.Join(tempDir, "danklinux-999999.sock")
staleSocket := filepath.Join(tempDir, "danklinux-4194305.sock")
err := os.WriteFile(staleSocket, []byte{}, 0o600)
require.NoError(t, err)

View File

@@ -5,6 +5,7 @@ import (
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
@@ -187,6 +188,29 @@ func (m *Manager) Close() {
})
}
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
ch := lm.Subscribe("thememode")
m.wg.Add(1)
go func() {
defer m.wg.Done()
defer lm.Unsubscribe("thememode")
for {
select {
case <-m.stopChan:
return
case state, ok := <-ch:
if !ok {
return
}
if state.PreparingForSleep {
continue
}
m.TriggerUpdate()
}
}
}()
}
func (m *Manager) schedulerLoop() {
defer m.wg.Done()
@@ -327,10 +351,12 @@ func statesEqual(a, b *State) bool {
}
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
if config.Mode == "location" {
switch config.Mode {
case "location":
return m.computeLocationSchedule(now, config)
default:
return computeTimeSchedule(now, config)
}
return computeTimeSchedule(now, config)
}
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
@@ -381,10 +407,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
}
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
if cond != wayland.SunNormal {
if cond == wayland.SunMidnightSun {
return true, startOfNextDay(now)
}
switch cond {
case wayland.SunMidnightSun:
return true, startOfNextDay(now)
case wayland.SunPolarNight:
return false, startOfNextDay(now)
}
@@ -397,10 +423,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
nextDay := startOfNextDay(now)
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
if nextCond != wayland.SunNormal {
if nextCond == wayland.SunMidnightSun {
return true, startOfNextDay(nextDay)
}
switch nextCond {
case wayland.SunMidnightSun:
return true, startOfNextDay(nextDay)
case wayland.SunPolarNight:
return false, startOfNextDay(nextDay)
}
@@ -413,13 +439,7 @@ func startOfNextDay(t time.Time) time.Time {
}
func validateHourMinute(hour, minute int) bool {
if hour < 0 || hour > 23 {
return false
}
if minute < 0 || minute > 59 {
return false
}
return true
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59
}
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {

View File

@@ -7,6 +7,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
tea "github.com/charmbracelet/bubbletea"
)
@@ -80,19 +81,24 @@ func (m Model) viewDependencyReview() string {
}
}
note := ""
if dep.Name == "dms-greeter" {
note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
}
var line string
if i == m.selectedDep {
line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
if dep.Version != "" {
line += fmt.Sprintf(" (%s)", dep.Version)
}
line = m.styles.SelectedOption.Render(line)
line = m.styles.SelectedOption.Render(line) + note
} else {
line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
if dep.Version != "" {
line += fmt.Sprintf(" (%s)", dep.Version)
}
line = m.styles.Normal.Render(line)
line = m.styles.Normal.Render(line) + note
}
b.WriteString(line)
@@ -115,6 +121,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
m.state = StateError
} else {
m.dependencies = depsMsg.deps
// dms-greeter is opt-in skipped by default
for _, dep := range depsMsg.deps {
if dep.Name == "dms-greeter" {
m.disabledItems["dms-greeter"] = true
break
}
}
m.state = StateDependencyReview
}
return m, m.listenForLogs()
@@ -230,6 +243,41 @@ func (m Model) installPackages() tea.Cmd {
// Convert installer messages to TUI messages
go func() {
for msg := range installerProgressChan {
// Run optional greeter setup
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
greeterSelected := false
for _, dep := range m.dependencies {
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
greeterSelected = true
break
}
}
if greeterSelected {
compositorName := "niri"
if m.selectedWM == 1 {
compositorName = "Hyprland"
}
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.92,
step: "Configuring DMS greeter...",
logOutput: "Starting automated greeter setup...",
}
greeterLogFunc := func(line string) {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.94,
step: "Configuring DMS greeter...",
logOutput: line,
}
}
if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.96,
step: "Greeter setup warning",
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
}
}
}
}
tuiMsg := packageInstallProgressMsg{
progress: msg.Progress,
step: msg.Step,

View File

@@ -38,6 +38,22 @@ func XDGConfigHome() string {
return filepath.Join(home, ".config")
}
func EmacsConfigDir() string {
home, _ := os.UserHomeDir()
emacsD := filepath.Join(home, ".emacs.d")
if info, err := os.Stat(emacsD); err == nil && info.IsDir() {
return emacsD
}
xdgEmacs := filepath.Join(XDGConfigHome(), "emacs")
if info, err := os.Stat(xdgEmacs); err == nil && info.IsDir() {
return xdgEmacs
}
return ""
}
func ExpandPath(path string) (string, error) {
expanded := os.ExpandEnv(path)
expanded = filepath.Clean(expanded)

View File

@@ -864,10 +864,12 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
func formatSizeProperty(name, value string) string {
parts := strings.SplitN(value, " ", 2)
if len(parts) != 2 {
return fmt.Sprintf(" %s { }", name)
if len(parts) == 2 {
return fmt.Sprintf(" %s { %s %s; }", name, parts[0], parts[1])
}
sizeType := parts[0]
sizeValue := parts[1]
return fmt.Sprintf(" %s { %s %s; }", name, sizeType, sizeValue)
// Bare number without type prefix — default to "fixed"
if _, err := strconv.Atoi(value); err == nil {
return fmt.Sprintf(" %s { fixed %s; }", name, value)
}
return fmt.Sprintf(" %s { }", name)
}

View File

@@ -29,6 +29,7 @@ Depends: ${misc:Depends},
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct
Suggests: cups-pk-helper
Provides: dms
Conflicts: dms
Replaces: dms

View File

@@ -27,12 +27,12 @@ override_dh_auto_build:
# Verify core directory exists (native package format has source at root)
test -d core || (echo "ERROR: core directory not found!" && exit 1)
# Patch go.mod to use Go 1.24 base version (Debian 13 has 1.23.x, may vary)
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
GO_INSTALLED=$$(go version | grep -oP 'go\K[0-9]+\.[0-9]+'); \
sed -i "s/^go [0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$$/go $${GO_INSTALLED}/" core/go.mod; \
sed -i "s/^\(## explicit; go \)[0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$$/\1$${GO_INSTALLED}/" core/vendor/modules.txt
# Build dms-cli from source using vendored dependencies
# Extract version info and build in single shell to preserve variables
# Architecture mapping: Debian amd64/arm64 -> Makefile amd64/arm64
# Build dms-cli (single shell to preserve variables; arch: Debian amd64/arm64 -> Makefile amd64/arm64)
VERSION="$(UPSTREAM_VERSION)"; \
COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown"); \
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \

View File

@@ -0,0 +1,9 @@
<services>
<!-- Download dms-qml source tarball from GitHub releases (greeter + quickshell content) -->
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.3/dms-qml.tar.gz</param>
<param name="filename">dms-qml.tar.gz</param>
</service>
</services>

View File

@@ -0,0 +1,5 @@
dms-greeter (1.4.3db1) unstable; urgency=medium
* Update to v1.4.3 stable release
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 25 Feb 2026 02:40:00 +0000

View File

@@ -0,0 +1,23 @@
Source: dms-greeter
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms-greeter
Architecture: any
Depends: ${misc:Depends},
greetd,
quickshell-git | quickshell
Recommends: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.

View File

@@ -0,0 +1,27 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dms-greeter
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/DankMaterialShell
Files: *
Copyright: 2026 Avenge Media LLC
License: MIT
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,108 @@
#!/bin/sh
set -e
case "$1" in
configure)
# Create greeter user/group if they don't exist
if ! getent group greeter >/dev/null; then
addgroup --system greeter
fi
if ! getent passwd greeter >/dev/null; then
adduser --system --ingroup greeter --home /var/lib/greeter \
--shell /bin/bash --gecos "System Greeter" greeter
fi
if [ -d /var/cache/dms-greeter ]; then
chown -R greeter:greeter /var/cache/dms-greeter 2>/dev/null || true
fi
if [ -d /var/lib/greeter ]; then
chown -R greeter:greeter /var/lib/greeter 2>/dev/null || true
fi
# Check and set graphical.target as default
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
systemctl set-default graphical.target >/dev/null 2>&1 || true
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) ✓"
else
TARGET_STATUS="Already graphical.target ✓"
fi
GREETD_CONFIG="/etc/greetd/config.toml"
CONFIG_STATUS="Not modified (already configured)"
# Check if niri or hyprland exists
COMPOSITOR="niri"
if ! command -v niri >/dev/null 2>&1; then
if command -v Hyprland >/dev/null 2>&1; then
COMPOSITOR="hyprland"
fi
fi
# If config doesn't exist, create a default one
if [ ! -f "$GREETD_CONFIG" ]; then
mkdir -p /etc/greetd
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
GREETD_EOF
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
CONFIG_STATUS="Created new config with $COMPOSITOR ✓"
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
# Backup existing config
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%Y%m%d-%H%M%S)"
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
# Update command in default_session section
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
sed -i '/^\[default_session\]/,/^\[/ s|^user =.*|user = "greeter"|' "$GREETD_CONFIG"
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR ✓"
fi
# Only show banner on initial install
if [ -z "$2" ]; then
cat << 'EOF'
=========================================================================
DMS Greeter Installation Complete!
=========================================================================
Status:
EOF
echo " ✓ Greetd config: $CONFIG_STATUS"
echo " ✓ Default target: $TARGET_STATUS"
cat << 'EOF'
✓ Greeter user: Created
✓ Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
Next steps:
1. Enable the greeter:
dms greeter enable
(This will automatically disable conflicting display managers,
set graphical.target, and enable greetd)
2. Sync your theme with the greeter (optional):
dms greeter sync
3. Check your setup:
dms greeter status
Ready to test? Run: sudo systemctl start greetd
Documentation: https://danklinux.com/docs/dankgreeter/
=========================================================================
EOF
fi
;;
esac
#DEBHELPER#
exit 0

View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -e
case "$1" in
purge)
# Remove greeter cache directory on purge
rm -rf /var/cache/dms-greeter 2>/dev/null || true
;;
esac
#DEBHELPER#
exit 0

View File

@@ -0,0 +1,48 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
%:
dh $@
override_dh_auto_build:
: nothing to build, we use prebuilt tarball content
override_dh_auto_install:
# Same pattern as dms: upstream from combined tarball (native format)
# Build root is either . (we're inside dms-qml) or has dms-qml/ subdir
SOURCE_DIR=""; \
if [ -d dms-qml ]; then SOURCE_DIR="dms-qml"; \
elif [ -f Modules/Greetd/assets/dms-greeter ]; then SOURCE_DIR="."; \
fi; \
if [ -n "$$SOURCE_DIR" ]; then \
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter && \
( cd $$SOURCE_DIR && tar cf - --exclude=debian . ) | \
( cd debian/dms-greeter/usr/share/quickshell/dms-greeter && tar xf - ) && \
install -Dm755 $$SOURCE_DIR/Modules/Greetd/assets/dms-greeter \
debian/dms-greeter/usr/bin/dms-greeter && \
install -Dm644 $$SOURCE_DIR/Modules/Greetd/README.md \
debian/dms-greeter/usr/share/doc/dms-greeter/README.md && \
install -Dm644 $$SOURCE_DIR/LICENSE \
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \
else \
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
echo "Contents of current directory:" && ls -la && exit 1; \
fi
# Remove build and development files
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/core
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/distro
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.git*
rm -f debian/dms-greeter/usr/share/quickshell/dms-greeter/.gitignore
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.github
override_dh_auto_clean:
rm -rf dms-qml
# When build root is dms-qml itself, we're inside it - nothing extra to remove
dh_auto_clean

View File

@@ -0,0 +1 @@
3.0 (native)

View File

@@ -0,0 +1 @@
# OBS _service downloads dms-qml.tar.gz; no extra excludes needed

View File

@@ -28,6 +28,7 @@ Depends: ${misc:Depends},
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct
Suggests: cups-pk-helper
Conflicts: dms-git
Replaces: dms-git
Description: DankMaterialShell - Modern Wayland Desktop Shell

View File

@@ -3,6 +3,7 @@
%global debug_package %{nil}
%global version {{{ git_repo_version }}}
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
%global go_toolchain_version 1.25.7
Name: dms
Epoch: 2
@@ -14,12 +15,12 @@ License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
VCS: {{{ git_repo_vcs }}}
Source0: {{{ git_repo_pack }}}
Source1: https://go.dev/dl/go%{go_toolchain_version}.linux-amd64.tar.gz
Source2: https://go.dev/dl/go%{go_toolchain_version}.linux-arm64.tar.gz
BuildRequires: git-core
BuildRequires: gzip
BuildRequires: golang >= 1.24
BuildRequires: make
BuildRequires: wget
BuildRequires: systemd-rpm-macros
# Core requirements
@@ -28,7 +29,7 @@ Requires: accountsservice
Requires: dms-cli = %{epoch}:%{version}-%{release}
Requires: dgop
# Core utilities (Highly recommended for DMS functionality)
# Core utilities (Recommended for DMS functionality)
Recommends: cava
Recommends: danksearch
Recommends: matugen
@@ -37,6 +38,7 @@ Recommends: quickshell-git
# Recommended system packages
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct
%description
@@ -65,6 +67,28 @@ Provides native DBus bindings, NetworkManager integration, and system utilities.
VERSION="%{version}"
COMMIT=$(echo "%{version}" | grep -oP '[a-f0-9]{7,}' | head -n1 || echo "unknown")
# Use pinned bundled Go toolchain (deterministic across chroots)
case "%{_arch}" in
x86_64)
GO_TARBALL="%{_sourcedir}/go%{go_toolchain_version}.linux-amd64.tar.gz"
;;
aarch64)
GO_TARBALL="%{_sourcedir}/go%{go_toolchain_version}.linux-arm64.tar.gz"
;;
*)
echo "Unsupported architecture for bundled Go: %{_arch}"
exit 1
;;
esac
rm -rf .go
tar -xzf "$GO_TARBALL"
mv go .go
export GOROOT="$PWD/.go"
export PATH="$GOROOT/bin:$PATH"
export GOTOOLCHAIN=local
go version
cd core
make dist VERSION="$VERSION" COMMIT="$COMMIT"

View File

@@ -28,6 +28,7 @@ Recommends: danksearch
Recommends: matugen
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct
%description

View File

@@ -25,6 +25,7 @@ Recommends: matugen
Recommends: quickshell-git
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct
Provides: dms
@@ -55,8 +56,10 @@ mkdir -p $HOME $GOCACHE $GOMODCACHE
# OBS has no network access, so use local toolchain only
export GOTOOLCHAIN=local
# Patch go.mod to use base Go version (e.g., go 1.24 instead of go 1.24.6)
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version
GO_INSTALLED=$(go version | grep -oP 'go\K[0-9]+\.[0-9]+')
sed -i "s/^go [0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$/go ${GO_INSTALLED}/" core/go.mod
sed -i "s/^\(## explicit; go \)[0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$/\1${GO_INSTALLED}/" core/vendor/modules.txt
# Extract version info for embedding in binary
VERSION="%{version}"

View File

@@ -0,0 +1,322 @@
# Spec for DMS Greeter - OpenSUSE/OBS
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell greeter for greetd
Name: dms-greeter
Version: %{version}
Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: greetd
Requires: (quickshell-git or quickshell)
Requires(post): /usr/sbin/useradd
Requires(post): /usr/sbin/groupadd
Recommends: policycoreutils-python-utils
Recommends: acl
Suggests: niri
Suggests: hyprland
Suggests: sway
%description
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.
%prep
%setup -q -c -n dms-qml
%build
%install
# Install greeter files to shared data location
install -dm755 %{buildroot}%{_datadir}/quickshell/dms-greeter
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms-greeter/
install -Dm755 %{_builddir}/dms-qml/Modules/Greetd/assets/dms-greeter %{buildroot}%{_bindir}/dms-greeter
install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docdir}/dms-greeter/README.md
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
# Remove build and development files
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms-greeter/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/distro
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms-greeter" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms-greeter" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
%files
%dir %{_docdir}/dms-greeter
%license %{_docdir}/dms-greeter/LICENSE
%doc %{_docdir}/dms-greeter/README.md
%{_bindir}/dms-greeter
%dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms-greeter/
%{_tmpfilesdir}/%{name}.conf
%pre
# Create greeter user/group if they don't exist
getent group greeter >/dev/null || groupadd -r greeter
getent passwd greeter >/dev/null || \
useradd -r -g greeter -d %{_sharedstatedir}/greeter -s /bin/bash \
-c "System Greeter" greeter
exit 0
%post
# SELinux contexts (no-op on OpenSUSE - semanage/restorecon not present)
if [ -x /usr/sbin/semanage ] && [ -x /usr/sbin/restorecon ]; then
semanage fcontext -a -t bin_t '%{_bindir}/dms-greeter' >/dev/null 2>&1 || true
restorecon %{_bindir}/dms-greeter >/dev/null 2>&1 || true
semanage fcontext -a -t user_home_dir_t '%{_sharedstatedir}/greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_sharedstatedir}/greeter >/dev/null 2>&1 || true
semanage fcontext -a -t cache_home_t '%{_localstatedir}/cache/dms-greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_localstatedir}/cache/dms-greeter >/dev/null 2>&1 || true
semanage fcontext -a -t usr_t '%{_datadir}/quickshell/dms-greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_datadir}/quickshell/dms-greeter >/dev/null 2>&1 || true
restorecon %{_sysconfdir}/pam.d/greetd >/dev/null 2>&1 || true
fi
# Resolve greeter runtime account/group for distro differences
GREETER_USER="greeter"
for candidate in greeter greetd _greeter; do
if getent passwd "$candidate" >/dev/null 2>&1; then
GREETER_USER="$candidate"
break
fi
done
GREETER_GROUP="$GREETER_USER"
if ! getent group "$GREETER_GROUP" >/dev/null 2>&1; then
for candidate in greeter greetd _greeter; do
if getent group "$candidate" >/dev/null 2>&1; then
GREETER_GROUP="$candidate"
break
fi
done
fi
# Ensure proper ownership of greeter directories
chown -R "$GREETER_USER:$GREETER_GROUP" %{_localstatedir}/cache/dms-greeter 2>/dev/null || true
chown -R "$GREETER_USER:$GREETER_GROUP" %{_sharedstatedir}/greeter 2>/dev/null || true
# Verify PAM configuration
PAM_CONFIG="/etc/pam.d/greetd"
write_greetd_pam_config() {
# openSUSE and Debian families usually expose PAM stacks as common-*
if [ -f /etc/pam.d/common-auth ] && [ -f /etc/pam.d/common-account ] && [ -f /etc/pam.d/common-password ] && [ -f /etc/pam.d/common-session ]; then
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth include common-auth
account required pam_nologin.so
account include common-account
password include common-password
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include common-session
PAM_EOF
return
fi
# Fedora/RHEL style system-auth/postlogin stack
if [ -f /etc/pam.d/system-auth ]; then
if [ -f /etc/pam.d/postlogin ]; then
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth substack system-auth
auth include postlogin
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include system-auth
session include postlogin
PAM_EOF
else
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth include system-auth
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include system-auth
PAM_EOF
fi
return
fi
# Last-resort conservative fallback
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth required pam_unix.so nullok
account required pam_unix.so
password required pam_unix.so nullok sha512
session required pam_unix.so
PAM_EOF
}
if [ ! -f "$PAM_CONFIG" ]; then
write_greetd_pam_config
chmod 644 "$PAM_CONFIG"
[ "$1" -eq 1 ] && echo "Created PAM configuration for greetd"
else
NEEDS_PAM_UPDATE=0
if grep -q "common-auth" "$PAM_CONFIG"; then
if [ ! -f /etc/pam.d/common-auth ]; then
NEEDS_PAM_UPDATE=1
fi
elif grep -q "system-auth" "$PAM_CONFIG"; then
if [ ! -f /etc/pam.d/system-auth ]; then
NEEDS_PAM_UPDATE=1
fi
else
NEEDS_PAM_UPDATE=1
fi
if [ "$NEEDS_PAM_UPDATE" -eq 1 ]; then
cp "$PAM_CONFIG" "$PAM_CONFIG.backup-dms-greeter"
write_greetd_pam_config
chmod 644 "$PAM_CONFIG"
[ "$1" -eq 1 ] && echo "Updated PAM configuration (old config backed up to $PAM_CONFIG.backup-dms-greeter)"
fi
fi
# Auto-configure greetd config
GREETD_CONFIG="/etc/greetd/config.toml"
CONFIG_STATUS="Not modified (already configured)"
COMPOSITOR=""
for candidate in niri Hyprland sway; do
if command -v "$candidate" >/dev/null 2>&1; then
case "$candidate" in
Hyprland)
COMPOSITOR="hyprland"
;;
*)
COMPOSITOR="$candidate"
;;
esac
break
fi
done
if [ ! -f "$GREETD_CONFIG" ]; then
mkdir -p /etc/greetd
if [ -n "$COMPOSITOR" ]; then
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "GREETER_USER_PLACEHOLDER"
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
GREETD_EOF
sed -i "s|GREETER_USER_PLACEHOLDER|$GREETER_USER|" "$GREETD_CONFIG"
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
CONFIG_STATUS="Created new config with $COMPOSITOR "
else
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "GREETER_USER_PLACEHOLDER"
command = "agreety --cmd /bin/login"
GREETD_EOF
sed -i "s|GREETER_USER_PLACEHOLDER|$GREETER_USER|" "$GREETD_CONFIG"
CONFIG_STATUS="Created safe fallback config (no supported compositor detected)"
fi
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
if [ -n "$COMPOSITOR" ]; then
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%%Y%%m%%d-%%H%%M%%S)"
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
sed -i "/^\[default_session\]/,/^\[/ s|^user =.*|user = \"$GREETER_USER\"|" "$GREETD_CONFIG"
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR "
else
CONFIG_STATUS="Skipped dms-greeter command update (no supported compositor detected)"
fi
fi
# Set graphical.target as default
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
systemctl set-default graphical.target >/dev/null 2>&1 || true
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) "
else
TARGET_STATUS="Already graphical.target "
fi
if [ "$1" -eq 1 ]; then
cat << 'EOF'
=========================================================================
DMS Greeter Installation Complete!
=========================================================================
Status:
EOF
echo " Greetd config: $CONFIG_STATUS"
echo " Default target: $TARGET_STATUS"
cat << 'EOF'
Greeter user: Created
Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
SELinux contexts: Applied (if applicable)
Next steps:
1. Enable the greeter:
dms greeter enable
2. Sync your theme with the greeter (optional):
dms greeter sync
3. Check your setup:
dms greeter status
Ready to test? Run: sudo systemctl start greetd
Documentation: https://danklinux.com/docs/dankgreeter/
=========================================================================
EOF
fi
%postun
if [ "$1" -eq 0 ] && [ -x /usr/sbin/semanage ]; then
semanage fcontext -d '%{_bindir}/dms-greeter' 2>/dev/null || true
semanage fcontext -d '%{_sharedstatedir}/greeter(/.*)?' 2>/dev/null || true
semanage fcontext -d '%{_localstatedir}/cache/dms-greeter(/.*)?' 2>/dev/null || true
semanage fcontext -d '%{_datadir}/quickshell/dms-greeter(/.*)?' 2>/dev/null || true
fi
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER
- Initial OpenSUSE/OBS port from Fedora

View File

@@ -27,6 +27,7 @@ Recommends: danksearch
Recommends: matugen
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct
%description

View File

@@ -11,7 +11,7 @@
OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts"
ALL_PACKAGES=(dms dms-git)
ALL_PACKAGES=(dms dms-git dms-greeter)
REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0")
ARCHES=("x86_64" "aarch64")
@@ -41,6 +41,9 @@ for pkg in "${PACKAGES[@]}"; do
dms-git)
PROJECT="$OBS_BASE_PROJECT:dms-git"
;;
dms-greeter)
PROJECT="$OBS_BASE_PROJECT:danklinux"
;;
*)
echo "Error: Unknown package '$pkg'"
continue
@@ -74,11 +77,15 @@ for pkg in "${PACKAGES[@]}"; do
COLOR="\033[0;32m" # Green
SYMBOL="✅"
;;
failed)
failed|broken|broken*)
COLOR="\033[0;31m" # Red
SYMBOL="❌"
FAILED_BUILDS+=("$repo $arch")
;;
blocked)
COLOR="\033[0;33m" # Yellow
SYMBOL="⏸️"
;;
unresolvable)
COLOR="\033[0;33m" # Yellow
SYMBOL="⚠️"

View File

@@ -68,13 +68,14 @@ fi
OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts"
AVAILABLE_PACKAGES=(dms dms-git)
AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
if [[ -z "$PACKAGE" ]]; then
echo "Available packages:"
echo ""
echo " 1. dms - Stable DMS"
echo " 2. dms-git - Nightly DMS"
echo " 3. dms-greeter - DMS greeter for greetd"
echo " a. all"
echo ""
read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
@@ -141,7 +142,12 @@ check_obs_version_exists() {
return 0
fi
else
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
# Empty/invalid response: expected on first upload (no spec on server yet), or actual API failure
if [[ -z "$OBS_SPEC" ]]; then
echo " No existing spec on OBS (first upload?) - proceeding"
else
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
fi
return 1
fi
return 1
@@ -168,6 +174,22 @@ update_debian_dms_service() {
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path"
}
update_debian_dms_greeter_service() {
local service_path="$1"
if [[ -z "$service_path" || ! -f "$service_path" ]]; then
return 0
fi
if [[ -z "$CHANGELOG_VERSION" ]]; then
return 0
fi
local base_version
base_version=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
if [[ -z "$base_version" ]]; then
return 0
fi
sed -i "s|/releases/download/v[0-9][^\"]*/dms-qml\.tar\.gz|/releases/download/v${base_version}/dms-qml.tar.gz|" "$service_path"
}
update_opensuse_git_spec() {
local spec_path="$1"
if [[ -z "$spec_path" || ! -f "$spec_path" ]]; then
@@ -248,6 +270,9 @@ dms)
dms-git)
PROJECT="dms-git"
;;
dms-greeter)
PROJECT="danklinux"
;;
*)
echo "Error: Unknown package '$PACKAGE'"
exit 1
@@ -329,9 +354,11 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
echo " - Applied rebuild suffix: $CHANGELOG_VERSION"
fi
# Keep Debian dms _service in sync with changelog version
# Keep Debian _service in sync with changelog version
if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
update_debian_dms_service "distro/debian/$PACKAGE/_service"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
update_debian_dms_greeter_service "distro/debian/$PACKAGE/_service"
fi
# Check if this version already exists in OBS
@@ -341,6 +368,12 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
if [[ "$PACKAGE" == *"-git" ]]; then
echo "==> Error: This commit is already uploaded to OBS"
echo " The same git commit ($(echo "$CHANGELOG_VERSION" | grep -oP '[a-f0-9]{8}' | tail -1)) already exists on OBS."
if [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${CI:-}" ]]; then
echo " CI run detected: skipping upload as a no-op (already up to date)."
echo " If you need to force rebuild this same commit, set REBUILD_RELEASE (e.g. 2, 3, ...)."
echo "✓ Exiting gracefully (no changes needed)"
exit 0
fi
echo " To rebuild the same commit, specify a rebuild number:"
echo " ./distro/scripts/obs-upload.sh $PACKAGE 2"
echo " ./distro/scripts/obs-upload.sh $PACKAGE 3"
@@ -379,6 +412,16 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
CHANGELOG_DATE=$(date '+%a %b %d %Y')
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
# Explicitly set Version:/Release: in case the spec uses %{version} macro
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
fi
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
@@ -444,6 +487,24 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
fi
fi
# For dms-greeter: download dms-qml.tar.gz from _service URL
if [[ -z "${SOURCE_DIR:-}" ]] && [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
DMS_GREETER_URL=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
if [[ -n "$DMS_GREETER_URL" ]]; then
DMS_GREETER_FULL_URL="https://github.com${DMS_GREETER_URL}"
echo " Downloading dms-greeter source from: $DMS_GREETER_FULL_URL"
if wget -q -O "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null || \
curl -L -f -s -o "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null; then
cd "$TEMP_DIR"
tar -xzf dms-qml.tar.gz
if [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
SOURCE_DIR="$TEMP_DIR"
fi
cd "$REPO_ROOT"
fi
fi
fi
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
@@ -452,6 +513,15 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
cd "$OBS_TARBALL_DIR"
case "$PACKAGE" in
dms-greeter)
EXPECTED_DIR="dms-qml"
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
mkdir -p "$EXPECTED_DIR"
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
dms)
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
@@ -584,6 +654,17 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
if [[ -z "$SOURCE_DIR" ]]; then
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
fi
# dms-qml.tar.gz extracts flat (quickshell contents, no top-level dir)
# Create dms-qml wrapper so combined tarball has correct top-level dir (like dms)
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
mkdir -p dms-qml
for f in *; do
if [[ -e "$f" && "$f" != "dms-qml" && "$f" != "source-archive" ]]; then
mv "$f" dms-qml/ 2>/dev/null || true
fi
done
SOURCE_DIR="dms-qml"
fi
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo "Error: Failed to extract source archive or find source directory"
echo "Contents of $TEMP_DIR:"
@@ -660,6 +741,21 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
cd "$OBS_TARBALL_DIR"
case "$PACKAGE" in
dms-greeter)
EXPECTED_DIR="dms-qml"
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
mkdir -p "$EXPECTED_DIR"
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
if [[ "$SOURCE0" == *.tar.xz ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
else
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
fi
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
dms)
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
@@ -709,10 +805,20 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
echo " - OpenSUSE source tarballs created"
fi
# Copy and update OpenSUSE spec file with the correct version (for -git packages)
# Copy and update OpenSUSE spec file with the correct version
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
CHANGELOG_DATE=$(date '+%a %b %d %Y')
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
# Explicitly set Version:/Release: in case the spec uses %{version} macro
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
fi
fi
@@ -839,8 +945,48 @@ EOF
echo " - Quilt format detected: creating debian.tar.gz"
tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/
# For dms-greeter: create orig tarball so Debian build gets upstream (OBS only passes .dsc Files to Debian)
DSC_FILES_DEBIAN=""
if [[ "$PACKAGE" == "dms-greeter" ]]; then
UPSTREAM_VER=$(echo "$VERSION" | sed 's/-[^-]*$//')
ORIG_TARBALL="${PACKAGE}_${UPSTREAM_VER}.orig.tar.gz"
ORIG_DIR="${PACKAGE}-${UPSTREAM_VER}"
if [[ -f "distro/debian/$PACKAGE/_service" ]] && grep -q "download_url" "distro/debian/$PACKAGE/_service"; then
DG_TEMP=$(mktemp -d)
DMS_GREETER_PATH=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
if [[ -n "$DMS_GREETER_PATH" ]]; then
DG_URL="https://github.com${DMS_GREETER_PATH}"
echo " - Downloading dms-greeter source for orig tarball: $DG_URL"
if wget -q -O "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null || curl -L -f -s -o "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null; then
( cd "$DG_TEMP" && tar --no-same-owner -xzf dms-qml.tar.gz && mkdir -p "$ORIG_DIR" && \
for f in *; do [[ "$f" != "dms-qml.tar.gz" && "$f" != "$ORIG_DIR" ]] && mv "$f" "$ORIG_DIR/"; done )
if [[ -d "$DG_TEMP/$ORIG_DIR/Modules" ]] || [[ -f "$DG_TEMP/$ORIG_DIR/LICENSE" ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$ORIG_TARBALL" -C "$DG_TEMP" "$ORIG_DIR"
ORIG_MD5=$(md5sum "$WORK_DIR/$ORIG_TARBALL" | cut -d' ' -f1)
ORIG_SIZE=$(stat -c%s "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null)
DSC_FILES_DEBIAN=" $ORIG_MD5 $ORIG_SIZE $ORIG_TARBALL
"
echo " - Created $ORIG_TARBALL for Debian orig"
fi
rm -rf "$DG_TEMP"
fi
fi
fi
fi
DEBIAN_MD5=$(md5sum "$WORK_DIR/debian.tar.gz" | cut -d' ' -f1)
DEBIAN_SIZE=$(stat -c%s "$WORK_DIR/debian.tar.gz" 2>/dev/null || stat -f%z "$WORK_DIR/debian.tar.gz" 2>/dev/null)
echo " - Generating $PACKAGE.dsc for quilt format"
cat >"$WORK_DIR/$PACKAGE.dsc" <<EOF
# debtransform: DEBTRANSFORM-TAR = orig (upstream), DEBTRANSFORM-FILES-TAR = debian archive
DEBTRANSFORM_EXTRA=""
if [[ -n "$DSC_FILES_DEBIAN" ]] && [[ -n "$ORIG_TARBALL" ]]; then
DEBTRANSFORM_EXTRA="DEBTRANSFORM-TAR: $ORIG_TARBALL
DEBTRANSFORM-FILES-TAR: debian.tar.gz
"
fi
cat >"$WORK_DIR/$PACKAGE.dsc" <<DSCEOF
Format: 3.0 (quilt)
Source: $PACKAGE
Binary: $PACKAGE
@@ -848,10 +994,9 @@ Architecture: any
Version: $VERSION
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13), wget, gzip
DEBTRANSFORM-TAR: debian.tar.gz
Files:
00000000000000000000000000000000 1 debian.tar.gz
EOF
${DEBTRANSFORM_EXTRA}Files:${DSC_FILES_DEBIAN}
$DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz
DSCEOF
fi
fi
fi
@@ -882,6 +1027,13 @@ if [[ -n "$OBS_FILES" ]]; then
continue
fi
# Keep current orig tarball for dms-greeter (Debian 3.0 quilt needs it)
UPSTREAM_VER_CLEAN=$(echo "$CHANGELOG_VERSION" | sed 's/-[^-]*$//' 2>/dev/null)
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ "$old_file" == "${PACKAGE}_${UPSTREAM_VER_CLEAN}.orig.tar.gz" ]]; then
echo " - Keeping orig tarball: $old_file"
continue
fi
if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
echo " - Keeping source tarball: $old_file"
continue

View File

@@ -28,8 +28,17 @@ override_dh_auto_build:
# Launchpad build environment has no internet access
test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1)
# Patch go.mod to use Go 1.24 base version (Ubuntu has 1.24.4, project requires 1.24.6)
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' dms-git-repo/core/go.mod
# Patch go.mod for Launchpad: align go directive w/latest Go toolchain
GO_VERSION=$$(go env GOVERSION | sed -E 's/^go([0-9]+\.[0-9]+).*/\1/'); \
if [ -n "$$GO_VERSION" ]; then \
sed -E -i "s/^go 1\.[0-9]+(\.[0-9]+)?/go $$GO_VERSION/" dms-git-repo/core/go.mod; \
if [ -f dms-git-repo/core/vendor/modules.txt ]; then \
sed -E -i "s/^(## explicit; go )1\.[0-9]+(\.[0-9]+)?$$/\1$$GO_VERSION/" dms-git-repo/core/vendor/modules.txt; \
fi; \
else \
echo "Warning: Could not detect Go version, leaving go.mod go directive unchanged"; \
fi
sed -E -i '/^toolchain go[0-9.]+$$/d' dms-git-repo/core/go.mod
# Build dms-cli from source
# Detect architecture

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1769018530,
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
"lastModified": 1771369470,
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
"rev": "0182a361324364ae3f436a63005877674cf45efb",
"type": "github"
},
"original": {

View File

@@ -80,7 +80,7 @@
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
subPackages = [ "cmd/dms" ];
@@ -181,7 +181,8 @@
buildInputs =
with pkgs;
[
go_1_24
go_1_25
go-mockery_2
gopls
delve
go-tools
@@ -189,6 +190,7 @@
prek
uv # for prek
shellcheck
# Nix development tools
nixd

View File

@@ -1 +1 @@
Saffron Bloom
The Wolverine

View File

@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell
import QtCore
import qs.Services
Singleton {
id: root
@@ -83,7 +84,12 @@ Singleton {
if (desktopEntry && desktopEntry.icon) {
return Quickshell.iconPath(desktopEntry.icon, true);
}
return Quickshell.iconPath(appId, true);
const icon = Quickshell.iconPath(appId, true);
if (icon && icon !== "")
return icon;
return DesktopService.resolveIconPath(appId);
}
function getAppName(appId: string, desktopEntry: var): string {

View File

@@ -142,6 +142,8 @@ Singleton {
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex;
}
if (popout.updateSurfacePosition)
popout.updateSurfacePosition();
currentPopoutTriggers[screenName] = triggerId;
return;
}

View File

@@ -126,6 +126,10 @@ Singleton {
property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: []
property string launcherLastMode: "all"
property string appDrawerLastMode: "apps"
property string niriOverviewLastMode: "apps"
Component.onCompleted: {
if (!isGreeterMode) {
loadSettings();
@@ -1100,6 +1104,21 @@ Singleton {
saveSettings();
}
function setLauncherLastMode(mode) {
launcherLastMode = mode;
saveSettings();
}
function setAppDrawerLastMode(mode) {
appDrawerLastMode = mode;
saveSettings();
}
function setNiriOverviewLastMode(mode) {
niriOverviewLastMode = mode;
saveSettings();
}
function syncWallpaperForCurrentMode() {
if (!perModeWallpaper)
return;

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton {
id: root
readonly property int settingsConfigVersion: 5
readonly property int settingsConfigVersion: 6
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -60,6 +60,7 @@ Singleton {
property bool _hasLoaded: false
property bool _isReadOnly: false
property bool _hasUnsavedChanges: false
property bool _selfWrite: false
property var _loadedSettingsSnapshot: null
property var pluginSettings: ({})
property var builtInPluginSettings: ({})
@@ -164,6 +165,22 @@ Singleton {
property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings()
property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12
onM3ElevationIntensityChanged: saveSettings()
property int m3ElevationOpacity: 30
onM3ElevationOpacityChanged: saveSettings()
property string m3ElevationColorMode: "default"
onM3ElevationColorModeChanged: saveSettings()
property string m3ElevationCustomColor: "#000000"
onM3ElevationCustomColorChanged: saveSettings()
property bool modalElevationEnabled: true
onModalElevationEnabledChanged: saveSettings()
property bool popoutElevationEnabled: true
onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true
onBarElevationEnabledChanged: saveSettings()
property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false
@@ -517,7 +534,7 @@ Singleton {
property int osdPosition: SettingsData.Position.BottomCenter
property bool osdVolumeEnabled: true
property bool osdMediaVolumeEnabled: true
property bool osdMediaPlaybackEnabled: true
property bool osdMediaPlaybackEnabled: false
property bool osdBrightnessEnabled: true
property bool osdIdleInhibitorEnabled: true
property bool osdMicMuteEnabled: true
@@ -571,6 +588,10 @@ Singleton {
"widgetTransparency": 1.0,
"squareCorners": false,
"noBackground": false,
"maximizeWidgetIcons": false,
"maximizeWidgetText": false,
"removeWidgetPadding": false,
"widgetPadding": 8,
"gothCornersEnabled": false,
"gothCornerRadiusOverride": false,
"gothCornerRadiusValue": 12,
@@ -583,6 +604,7 @@ Singleton {
"widgetOutlineOpacity": 1.0,
"widgetOutlineThickness": 1,
"fontScale": 1.0,
"iconScale": 1.0,
"autoHide": false,
"autoHideDelay": 250,
"showOnWindowsOpen": false,
@@ -596,7 +618,7 @@ Singleton {
"scrollYBehavior": "workspace",
"shadowIntensity": 0,
"shadowOpacity": 60,
"shadowColorMode": "text",
"shadowColorMode": "default",
"shadowCustomColor": "#000000",
"clickThrough": false
}
@@ -1025,6 +1047,7 @@ Singleton {
elif command -v dconf >/dev/null 2>&1; then
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
fi`;
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
if (exitCode !== 0)
return;
@@ -1243,6 +1266,7 @@ Singleton {
function saveSettings() {
if (_loading || _parseError || !_hasLoaded)
return;
_selfWrite = true;
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
if (_isReadOnly)
_checkSettingsWritable();
@@ -2589,7 +2613,13 @@ Singleton {
blockWrites: true
atomicWrites: true
watchChanges: true
onFileChanged: settingsFileReloadDebounce.restart()
onFileChanged: {
if (_selfWrite) {
_selfWrite = false;
return;
}
settingsFileReloadDebounce.restart();
}
onLoaded: {
if (isGreeterMode)
return;

View File

@@ -673,6 +673,92 @@ Singleton {
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
readonly property bool elevationEnabled: typeof SettingsData !== "undefined" && (SettingsData.m3ElevationEnabled ?? true)
readonly property real elevationBlurMax: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? Math.min(128, Math.max(32, SettingsData.m3ElevationIntensity * 2)) : 64
readonly property real _elevMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? SettingsData.m3ElevationIntensity / 12 : 1
readonly property real _opMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationOpacity !== undefined ? SettingsData.m3ElevationOpacity / 60 : 1
readonly property var elevationLevel1: ({
blurPx: 4 * _elevMult,
offsetY: 1 * _elevMult,
spreadPx: 0,
alpha: 0.2 * _opMult
})
readonly property var elevationLevel2: ({
blurPx: 8 * _elevMult,
offsetY: 4 * _elevMult,
spreadPx: 0,
alpha: 0.25 * _opMult
})
readonly property var elevationLevel3: ({
blurPx: 12 * _elevMult,
offsetY: 6 * _elevMult,
spreadPx: 0,
alpha: 0.3 * _opMult
})
readonly property var elevationLevel4: ({
blurPx: 16 * _elevMult,
offsetY: 8 * _elevMult,
spreadPx: 0,
alpha: 0.3 * _opMult
})
readonly property var elevationLevel5: ({
blurPx: 20 * _elevMult,
offsetY: 10 * _elevMult,
spreadPx: 0,
alpha: 0.3 * _opMult
})
function elevationShadowColor(level) {
const alpha = (level && level.alpha !== undefined) ? level.alpha : 0.3;
let r = 0;
let g = 0;
let b = 0;
if (typeof SettingsData !== "undefined") {
const mode = SettingsData.m3ElevationColorMode || "default";
if (mode === "default") {
r = 0;
g = 0;
b = 0;
} else if (mode === "text") {
r = surfaceText.r;
g = surfaceText.g;
b = surfaceText.b;
} else if (mode === "primary") {
r = primary.r;
g = primary.g;
b = primary.b;
} else if (mode === "surfaceVariant") {
r = surfaceVariant.r;
g = surfaceVariant.g;
b = surfaceVariant.b;
} else if (mode === "custom" && SettingsData.m3ElevationCustomColor) {
const c = Qt.color(SettingsData.m3ElevationCustomColor);
r = c.r;
g = c.g;
b = c.b;
}
}
return Qt.rgba(r, g, b, alpha);
}
function elevationTintOpacity(level) {
if (!level)
return 0;
if (level === elevationLevel1)
return 0.05;
if (level === elevationLevel2)
return 0.08;
if (level === elevationLevel3)
return 0.11;
if (level === elevationLevel4)
return 0.12;
if (level === elevationLevel5)
return 0.14;
return 0.08;
}
readonly property var animationDurations: [
{
"shorter": 0,
@@ -961,7 +1047,6 @@ Singleton {
}
if (!isGreeterMode) {
// Skip with matugen because, our script runner will do it.
if (!matugenAvailable) {
PortalService.setLightMode(light);
}
@@ -1170,21 +1255,23 @@ Singleton {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
}
function barIconSize(barThickness, offset, noBackground) {
function barIconSize(barThickness, offset, maximizeIcon, iconScale) {
const defaultOffset = offset !== undefined ? offset : -6;
const size = (noBackground ?? false) ? iconSizeLarge : iconSize;
const size = (maximizeIcon ?? false) ? iconSizeLarge : iconSize;
const s = iconScale !== undefined ? iconScale : 1.0;
return Math.round((barThickness / 48) * (size + defaultOffset));
return Math.round((barThickness / 48) * (size + defaultOffset) * s);
}
function barTextSize(barThickness, fontScale) {
function barTextSize(barThickness, fontScale, maximizeText) {
const scale = barThickness / 48;
const dankBarScale = fontScale !== undefined ? fontScale : 1.0;
const maxScale = (maximizeText ?? false) ? 1.5 : 1.0;
if (scale <= 0.75)
return Math.round(fontSizeSmall * 0.9 * dankBarScale);
return Math.round(fontSizeSmall * 0.9 * dankBarScale * maxScale);
if (scale >= 1.25)
return Math.round(fontSizeMedium * dankBarScale);
return Math.round(fontSizeSmall * dankBarScale);
return Math.round(fontSizeMedium * dankBarScale * maxScale);
return Math.round(fontSizeSmall * dankBarScale * maxScale);
}
function getBatteryIcon(level, isCharging, batteryAvailable) {

View File

@@ -77,7 +77,11 @@ var SPEC = {
deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] }
hiddenInputDeviceNames: { def: [] },
launcherLastMode: { def: "all" },
appDrawerLastMode: { def: "apps" },
niriOverviewLastMode: { def: "apps" }
};
function getValidKeys() {

View File

@@ -21,7 +21,7 @@ var SPEC = {
widgetColorMode: { def: "default" },
controlCenterTileColorMode: { def: "primary" },
buttonColorMode: { def: "primary" },
cornerRadius: { def: 12, onChange: "updateCompositorLayout" },
cornerRadius: { def: 16, onChange: "updateCompositorLayout" },
niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
@@ -46,6 +46,14 @@ var SPEC = {
modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true },
m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 },
m3ElevationColorMode: { def: "default" },
m3ElevationCustomColor: { def: "#000000" },
modalElevationEnabled: { def: true },
popoutElevationEnabled: { def: true },
barElevationEnabled: { def: true },
wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false },
@@ -341,7 +349,7 @@ var SPEC = {
osdPosition: { def: 5 },
osdVolumeEnabled: { def: true },
osdMediaVolumeEnabled: { def: true },
osdMediaPlaybackEnabled: { def: true },
osdMediaPlaybackEnabled: { def: false },
osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true },
@@ -395,6 +403,10 @@ var SPEC = {
widgetTransparency: 1.0,
squareCorners: false,
noBackground: false,
maximizeWidgetIcons: false,
maximizeWidgetText: false,
removeWidgetPadding: false,
widgetPadding: 8,
gothCornersEnabled: false,
gothCornerRadiusOverride: false,
gothCornerRadiusValue: 12,
@@ -407,6 +419,7 @@ var SPEC = {
widgetOutlineOpacity: 1.0,
widgetOutlineThickness: 1,
fontScale: 1.0,
iconScale: 1.0,
autoHide: false,
autoHideDelay: 250,
showOnWindowsOpen: false,
@@ -420,7 +433,7 @@ var SPEC = {
scrollYBehavior: "workspace",
shadowIntensity: 0,
shadowOpacity: 60,
shadowColorMode: "text",
shadowColorMode: "default",
shadowCustomColor: "#000000",
clickThrough: false
}], onChange: "updateBarConfigs"

View File

@@ -226,6 +226,25 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 5;
}
if (currentVersion < 6) {
console.info("Migrating settings from version", currentVersion, "to version 6");
if (settings.barElevationEnabled === undefined) {
var legacyBars = Array.isArray(settings.barConfigs) ? settings.barConfigs : [];
var hadLegacyBarShadowEnabled = false;
for (var j = 0; j < legacyBars.length; j++) {
var legacyIntensity = Number(legacyBars[j] && legacyBars[j].shadowIntensity);
if (!isNaN(legacyIntensity) && legacyIntensity > 0) {
hadLegacyBarShadowEnabled = true;
break;
}
}
settings.barElevationEnabled = hadLegacyBarShadowEnabled;
}
settings.configVersion = 6;
}
return settings;
}

View File

@@ -142,25 +142,45 @@ Item {
fadeDpmsWindowLoader.item.cancelFade();
}
}
function onRequestMonitorOn() {
if (!fadeDpmsWindowLoader.item)
return;
fadeDpmsWindowLoader.item.cancelFade();
}
}
}
}
property string _barLayoutStateJson: {
const configs = SettingsData.barConfigs;
const mapped = configs.map(c => ({
id: c.id,
position: c.position,
autoHide: c.autoHide,
visible: c.visible
})).sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
if (aVertical !== bVertical) {
return aVertical - bVertical;
}
return String(a.id).localeCompare(String(b.id));
});
return JSON.stringify(mapped);
}
on_BarLayoutStateJsonChanged: {
if (typeof dockRecreateDebounce !== "undefined") {
dockRecreateDebounce.restart();
}
}
Repeater {
id: dankBarRepeater
model: ScriptModel {
id: barRepeaterModel
values: {
const configs = SettingsData.barConfigs;
return configs.map(c => ({
id: c.id,
position: c.position
})).sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
return aVertical - bVertical;
});
}
values: JSON.parse(root._barLayoutStateJson)
}
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
@@ -207,13 +227,6 @@ Item {
PolkitService.polkitAvailable;
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
dockRecreateDebounce.restart();
}
}
Loader {
id: dockLoader
active: root.dockEnabled
@@ -265,6 +278,7 @@ Item {
sourceComponent: Component {
DankDashPopout {
id: dankDashPopout
onPopoutClosed: PopoutService.unloadDankDash()
}
}
}
@@ -284,8 +298,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.notificationCenterLoader = notificationCenterLoader;
}
NotificationCenterPopout {
id: notificationCenter
onPopoutClosed: PopoutService.unloadNotificationCenter()
Component.onCompleted: {
PopoutService.notificationCenterPopout = notificationCenter;
@@ -309,10 +328,15 @@ Item {
property var modalRef: colorPickerModal
property LazyLoader powerModalLoaderRef: powerMenuModalLoader
Component.onCompleted: {
PopoutService.controlCenterLoader = controlCenterLoader;
}
ControlCenterPopout {
id: controlCenterPopout
colorPickerModal: controlCenterLoader.modalRef
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
onPopoutClosed: PopoutService.unloadControlCenter()
onLockRequested: {
lock.activate();
@@ -341,6 +365,23 @@ Item {
}
}
LazyLoader {
id: wifiQRCodeModalLoader
active: false
Component.onCompleted: {
PopoutService.wifiQRCodeModalLoader = wifiQRCodeModalLoader;
}
WifiQRCodeModal {
id: wifiQRCodeModalItem
Component.onCompleted: {
PopoutService.wifiQRCodeModal = wifiQRCodeModalItem;
}
}
}
LazyLoader {
id: polkitAuthModalLoader
active: false
@@ -420,8 +461,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.batteryPopoutLoader = batteryPopoutLoader;
}
BatteryPopout {
id: batteryPopout
onPopoutClosed: PopoutService.unloadBattery()
Component.onCompleted: {
PopoutService.batteryPopout = batteryPopout;
@@ -434,8 +480,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.layoutPopoutLoader = layoutPopoutLoader;
}
DWLLayoutPopout {
id: layoutPopout
onPopoutClosed: PopoutService.unloadLayoutPopout()
Component.onCompleted: {
PopoutService.layoutPopout = layoutPopout;
@@ -448,8 +499,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.vpnPopoutLoader = vpnPopoutLoader;
}
VpnPopout {
id: vpnPopout
onPopoutClosed: PopoutService.unloadVpn()
Component.onCompleted: {
PopoutService.vpnPopout = vpnPopout;
@@ -462,8 +518,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.processListPopoutLoader = processListPopoutLoader;
}
ProcessListPopout {
id: processListPopout
onPopoutClosed: PopoutService.unloadProcessListPopout()
Component.onCompleted: {
PopoutService.processListPopout = processListPopout;
@@ -506,8 +567,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.appDrawerLoader = appDrawerLoader;
}
AppDrawerPopout {
id: appDrawerPopout
onPopoutClosed: PopoutService.unloadAppDrawer()
Component.onCompleted: {
PopoutService.appDrawerPopout = appDrawerPopout;
@@ -539,8 +605,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.clipboardHistoryPopoutLoader = clipboardHistoryPopoutLoader;
}
ClipboardHistoryPopout {
id: clipboardHistoryPopout
onPopoutClosed: PopoutService.unloadClipboardHistoryPopout()
Component.onCompleted: {
PopoutService.clipboardHistoryPopout = clipboardHistoryPopout;
@@ -715,8 +786,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.systemUpdateLoader = systemUpdateLoader;
}
SystemUpdatePopout {
id: systemUpdatePopout
onPopoutClosed: PopoutService.unloadSystemUpdate()
Component.onCompleted: {
PopoutService.systemUpdatePopout = systemUpdatePopout;

View File

@@ -859,6 +859,70 @@ Item {
return success ? `WIDGET_TOGGLE_SUCCESS: ${widgetId}` : `WIDGET_TOGGLE_FAILED: ${widgetId}`;
}
function openWith(widgetId: string, mode: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (typeof widget.openWithMode !== "function")
return `WIDGET_OPEN_WITH_NOT_SUPPORTED: ${widgetId}`;
widget.openWithMode(mode || "all");
return `WIDGET_OPEN_WITH_SUCCESS: ${widgetId} ${mode}`;
}
function toggleWith(widgetId: string, mode: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (typeof widget.toggleWithMode !== "function")
return `WIDGET_TOGGLE_WITH_NOT_SUPPORTED: ${widgetId}`;
widget.toggleWithMode(mode || "all");
return `WIDGET_TOGGLE_WITH_SUCCESS: ${widgetId} ${mode}`;
}
function openQuery(widgetId: string, query: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (typeof widget.openWithQuery !== "function")
return `WIDGET_OPEN_QUERY_NOT_SUPPORTED: ${widgetId}`;
widget.openWithQuery(query || "");
return `WIDGET_OPEN_QUERY_SUCCESS: ${widgetId}`;
}
function toggleQuery(widgetId: string, query: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (typeof widget.toggleWithQuery !== "function")
return `WIDGET_TOGGLE_QUERY_NOT_SUPPORTED: ${widgetId}`;
widget.toggleWithQuery(query || "");
return `WIDGET_TOGGLE_QUERY_SUCCESS: ${widgetId}`;
}
function list(): string {
const widgets = BarWidgetService.getRegisteredWidgetIds();
if (widgets.length === 0)

View File

@@ -18,7 +18,7 @@ FloatingWindow {
}
objectName: "changelogModal"
title: "What's New"
title: i18n("What's New")
minimumSize: Qt.size(modalWidth, modalHeight)
maximumSize: Qt.size(modalWidth, modalHeight)
color: Theme.surfaceContainer
@@ -81,7 +81,7 @@ FloatingWindow {
onClicked: root.dismiss()
DankTooltip {
text: "Close"
text: i18n("Close")
}
}
}
@@ -125,7 +125,7 @@ FloatingWindow {
spacing: Theme.spacingM
DankButton {
text: "Read Full Release Notes"
text: i18n("Read Full Release Notes")
iconName: "open_in_new"
backgroundColor: Theme.surfaceContainerHighest
textColor: Theme.surfaceText
@@ -133,7 +133,7 @@ FloatingWindow {
}
DankButton {
text: "Got It"
text: i18n("Got It")
iconName: "check"
backgroundColor: Theme.primary
textColor: Theme.primaryText

View File

@@ -12,7 +12,7 @@ Singleton {
readonly property int popoutWidth: 550
readonly property int popoutHeight: 500
readonly property int itemHeight: 72
readonly property int thumbnailSize: 48
readonly property int thumbnailSize: 100
readonly property int retryInterval: 50
readonly property int viewportBuffer: 100
readonly property int extendedBuffer: 200

View File

@@ -84,7 +84,8 @@ Rectangle {
anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
height: contentColumn.implicitHeight
// height: contentColumn.implicitHeight
height: ClipboardConstants.itemHeight
clip: true
ClipboardThumbnail {
@@ -92,7 +93,7 @@ Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
height: entryType === "image" ? ClipboardConstants.itemHeight - 4 : Theme.iconSize // 100 - 4 = 96, 96:72 = 4:3
entry: root.entry
entryType: root.entryType
modal: root.modal
@@ -134,6 +135,7 @@ Rectangle {
wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight
textFormat: Text.PlainText
}
}
}

View File

@@ -137,23 +137,23 @@ Item {
anchors.margins: 2
source: thumbnailImage
maskEnabled: true
maskSource: clipboardCircularMask
maskSource: clipboardRoundedRectangularMask
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: clipboardCircularMask
width: ClipboardConstants.thumbnailSize - 4
height: ClipboardConstants.thumbnailSize - 4
id: clipboardRoundedRectangularMask
width: ClipboardConstants.thumbnailSize
height: ClipboardConstants.itemHeight - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px)
color: "black"
antialiasing: true
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Common
@@ -32,9 +33,9 @@ Item {
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 1
property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius
property bool enableShadow: false
property bool enableShadow: true
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
property bool shouldHaveFocus: shouldBeVisible
@@ -142,7 +143,7 @@ Item {
}
}
readonly property real shadowBuffer: 5
readonly property real shadowBuffer: Theme.elevationBlurMax * 1.5 + 24
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -377,12 +378,34 @@ Item {
}
}
Rectangle {
readonly property var elev: Theme.elevationLevel3
readonly property real shadowBlurNorm: Math.max(0, Math.min(1, (elev && elev.blurPx !== undefined ? elev.blurPx : 12) / Theme.elevationBlurMax))
Item {
id: modalShadowLayer
anchors.fill: parent
color: root.backgroundColor
border.color: root.borderColor
border.width: root.borderWidth
radius: root.cornerRadius
layer.enabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
layer.effect: MultiEffect {
autoPaddingEnabled: true
shadowEnabled: true
blurEnabled: false
maskEnabled: false
shadowBlur: animatedContent.shadowBlurNorm
shadowScale: 1
shadowVerticalOffset: animatedContent.elev && animatedContent.elev.offsetY !== undefined ? animatedContent.elev.offsetY : 6
shadowHorizontalOffset: 0
blurMax: Theme.elevationBlurMax
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel3)
}
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: root.backgroundColor
border.color: root.borderColor
border.width: root.borderWidth
}
}
FocusScope {

View File

@@ -56,11 +56,6 @@ Rectangle {
case "app":
if (selectedItem?.isCore)
break;
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
}
}
if (SessionService.nvidiaCommand) {
result.push({
name: I18n.tr("Launch on dGPU"),
@@ -68,6 +63,11 @@ Rectangle {
action: "launch_dgpu"
});
}
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
}
}
break;
}
return result;

View File

@@ -125,13 +125,6 @@ Item {
}
readonly property var sectionDefinitions: [
{
id: "calculator",
title: I18n.tr("Calculator"),
icon: "calculate",
priority: 0,
defaultViewMode: "list"
},
{
id: "favorites",
title: I18n.tr("Pinned"),
@@ -169,6 +162,11 @@ Item {
}
]
property string fileSearchType: "all"
property string fileSearchExt: ""
property string fileSearchFolder: ""
property string fileSearchSort: "score"
property string pluginFilter: ""
property string activePluginName: ""
property var activePluginCategories: []
@@ -353,6 +351,10 @@ Item {
previousSearchMode = "all";
autoSwitchedToFiles = false;
isFileSearching = false;
fileSearchType = "all";
fileSearchExt = "";
fileSearchFolder = "";
fileSearchSort = "score";
sections = [];
flatModel = [];
selectedFlatIndex = 0;
@@ -406,6 +408,34 @@ Item {
performSearch();
}
function setFileSearchType(type) {
if (fileSearchType === type)
return;
fileSearchType = type;
performFileSearch();
}
function setFileSearchExt(ext) {
if (fileSearchExt === ext)
return;
fileSearchExt = ext;
performFileSearch();
}
function setFileSearchFolder(folder) {
if (fileSearchFolder === folder)
return;
fileSearchFolder = folder;
performFileSearch();
}
function setFileSearchSort(sort) {
if (fileSearchSort === sort)
return;
fileSearchSort = sort;
performFileSearch();
}
function clearPluginFilter() {
if (pluginFilter) {
pluginFilter = "";
@@ -681,24 +711,16 @@ Item {
return;
}
var calculatorResult = evaluateCalculator(searchQuery);
if (calculatorResult) {
calculatorResult._preScored = 12000;
allItems.push(calculatorResult);
}
var apps = searchApps(searchQuery);
for (var i = 0; i < apps.length; i++) {
if (searchQuery)
apps[i]._preScored = 11000 - i;
allItems.push(apps[i]);
}
if (searchMode === "all") {
if (searchQuery && searchQuery.length >= 2) {
_pluginPhasePending = true;
_phase1Items = allItems.slice();
_pluginPhaseForceFirst = shouldResetSelection;
_phase1Items = allItems;
pluginPhaseTimer.restart();
isSearching = true;
searchCompleted();
@@ -842,10 +864,20 @@ Item {
var params = {
limit: 20,
fuzzy: true,
sort: "score",
sort: fileSearchSort || "score",
desc: true
};
if (DSearchService.supportsTypeFilter) {
params.type = (fileSearchType && fileSearchType !== "all") ? fileSearchType : "all";
}
if (fileSearchExt) {
params.ext = fileSearchExt;
}
if (fileSearchFolder) {
params.folder = fileSearchFolder;
}
DSearchService.search(fileQuery, params, function (response) {
isFileSearching = false;
if (response.error)
@@ -855,34 +887,73 @@ Item {
for (var i = 0; i < hits.length; i++) {
var hit = hits[i];
var docTypes = hit.locations?.doc_type;
var isDir = docTypes ? !!docTypes["dir"] : false;
fileItems.push(transformFileResult({
path: hit.id || "",
score: hit.score || 0
score: hit.score || 0,
is_dir: isDir
}));
}
var fileSection = {
id: "files",
title: I18n.tr("Files"),
icon: "folder",
priority: 4,
items: fileItems,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
};
var fileSections = [];
var showType = fileSearchType || "all";
if (showType === "all" && DSearchService.supportsTypeFilter) {
var onlyFiles = [];
var onlyDirs = [];
for (var j = 0; j < fileItems.length; j++) {
if (fileItems[j].data?.is_dir)
onlyDirs.push(fileItems[j]);
else
onlyFiles.push(fileItems[j]);
}
if (onlyFiles.length > 0) {
fileSections.push({
id: "files",
title: I18n.tr("Files"),
icon: "insert_drive_file",
priority: 4,
items: onlyFiles,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
});
}
if (onlyDirs.length > 0) {
fileSections.push({
id: "folders",
title: I18n.tr("Folders"),
icon: "folder",
priority: 4.1,
items: onlyDirs,
collapsed: collapsedSections["folders"] || false,
flatStartIndex: 0
});
}
} else {
var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
if (fileItems.length > 0) {
fileSections.push({
id: "files",
title: filesTitle,
icon: filesIcon,
priority: 4,
items: fileItems,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
});
}
}
var newSections;
if (searchMode === "files") {
newSections = fileItems.length > 0 ? [fileSection] : [];
newSections = fileSections;
} else {
var existingNonFile = sections.filter(function (s) {
return s.id !== "files";
return s.id !== "files" && s.id !== "folders";
});
if (fileItems.length > 0) {
newSections = existingNonFile.concat([fileSection]);
} else {
newSections = existingNonFile;
}
newSections = existingNonFile.concat(fileSections);
}
newSections.sort(function (a, b) {
return a.priority - b.priority;
@@ -928,14 +999,7 @@ Item {
}
function transformFileResult(file) {
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"));
}
function evaluateCalculator(query) {
var calc = Utils.evaluateCalculator(query);
if (!calc)
return null;
return Transform.createCalculatorItem(calc, query, I18n.tr("Copy"));
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal"));
}
function detectTrigger(query) {
@@ -1270,6 +1334,23 @@ Item {
CacheData.saveLauncherCache(serializable);
}
function _actionsFromDesktopEntry(appId) {
if (!appId)
return [];
var entry = DesktopEntries.heuristicLookup(appId);
if (!entry || !entry.actions || entry.actions.length === 0)
return [];
var result = [];
for (var i = 0; i < entry.actions.length; i++) {
result.push({
name: entry.actions[i].name,
icon: "play_arrow",
actionData: entry.actions[i]
});
}
return result;
}
function _loadDiskCache() {
var cached = CacheData.loadLauncherCache();
if (!cached || !Array.isArray(cached) || cached.length === 0)
@@ -1297,8 +1378,12 @@ Item {
data: {
id: it.id
},
actions: [],
primaryAction: null,
actions: _actionsFromDesktopEntry(it.id),
primaryAction: it.type === "app" && !it.isCore ? {
name: I18n.tr("Launch"),
icon: "open_in_new",
action: "launch"
} : null,
_diskCached: true,
_hName: "",
_hSub: "",
@@ -1559,9 +1644,6 @@ Item {
case "file":
openFile(item.data?.path);
break;
case "calculator":
copyToClipboard(item.name);
break;
default:
return;
}
@@ -1585,6 +1667,9 @@ Item {
case "copy_path":
copyToClipboard(item.data.path);
break;
case "open_terminal":
openTerminal(item.data.path);
break;
case "copy":
copyToClipboard(item.name);
break;
@@ -1616,25 +1701,41 @@ Item {
itemExecuted();
}
function launchApp(app) {
function _resolveDesktopEntry(app) {
if (!app)
return null;
if (app.command)
return app;
var id = app.id || app.execString || app.exec || "";
if (!id)
return null;
return DesktopEntries.heuristicLookup(id);
}
function launchApp(app) {
var entry = _resolveDesktopEntry(app);
if (!entry)
return;
SessionService.launchDesktopEntry(app);
AppUsageHistoryData.addAppUsage(app);
SessionService.launchDesktopEntry(entry);
AppUsageHistoryData.addAppUsage(entry);
}
function launchAppWithNvidia(app) {
if (!app)
var entry = _resolveDesktopEntry(app);
if (!entry)
return;
SessionService.launchDesktopEntry(app, true);
AppUsageHistoryData.addAppUsage(app);
SessionService.launchDesktopEntry(entry, true);
AppUsageHistoryData.addAppUsage(entry);
}
function launchAppAction(actionItem) {
if (!actionItem || !actionItem.parentApp || !actionItem.actionData)
if (!actionItem || !actionItem.actionData)
return;
SessionService.launchDesktopAction(actionItem.parentApp, actionItem.actionData);
AppUsageHistoryData.addAppUsage(actionItem.parentApp);
var entry = _resolveDesktopEntry(actionItem.parentApp);
if (!entry)
return;
SessionService.launchDesktopAction(entry, actionItem.actionData);
AppUsageHistoryData.addAppUsage(entry);
}
function openFile(path) {
@@ -1650,6 +1751,16 @@ Item {
Qt.openUrlExternally("file://" + folder);
}
function openTerminal(path) {
if (!path)
return;
var terminal = Quickshell.env("TERMINAL") || "xterm";
Quickshell.execDetached({
command: [terminal],
workingDirectory: path
});
}
function copyToClipboard(text) {
if (!text)
return;

View File

@@ -101,35 +101,6 @@ function detectIconType(iconName) {
return "material";
}
function evaluateCalculator(query) {
if (!query || query.length === 0)
return null;
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
if (mathExpr.length < 2)
return null;
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
if (!hasMath)
return null;
try {
var sanitized = mathExpr.replace(/\^/g, "**");
var result = Function('"use strict"; return (' + sanitized + ')')();
if (typeof result === "number" && isFinite(result)) {
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
return {
expression: query,
result: result,
displayResult: displayResult
};
}
} catch (e) { }
return null;
}
function sortPluginIdsByOrder(pluginIds, order) {
if (!order || order.length === 0)
return pluginIds;

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
@@ -75,7 +76,7 @@ Item {
return Theme.primary;
}
}
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 1
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
signal dialogClosed
@@ -102,11 +103,15 @@ Item {
spotlightContent.searchField.text = query;
}
if (spotlightContent.controller) {
var targetMode = mode || "all";
var targetMode = mode || SessionData.launcherLastMode || "all";
spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all";
spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score";
spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null;
@@ -226,6 +231,15 @@ Item {
}
}
Connections {
target: spotlightContent?.controller ?? null
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
return;
SessionData.setLauncherLastMode(mode);
}
}
HyprlandFocusGrab {
id: focusGrab
windows: [launcherWindow]
@@ -284,7 +298,7 @@ Item {
PanelWindow {
id: launcherWindow
visible: root._windowEnabled && (!root.unloadContentOnClose || spotlightOpen || isClosing)
visible: root._windowEnabled && (spotlightOpen || isClosing)
color: "transparent"
exclusionMode: ExclusionMode.Ignore
@@ -377,12 +391,30 @@ Item {
}
}
Rectangle {
Item {
id: launcherShadowLayer
anchors.fill: parent
color: root.backgroundColor
border.color: root.borderColor
border.width: root.borderWidth
radius: root.cornerRadius
layer.enabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
layer.effect: MultiEffect {
autoPaddingEnabled: true
shadowEnabled: true
blurEnabled: false
maskEnabled: false
shadowBlur: Math.max(0, Math.min(1, Theme.elevationLevel3.blurPx / Theme.elevationBlurMax))
shadowScale: 1
shadowVerticalOffset: Theme.elevationLevel3.offsetY
shadowHorizontalOffset: 0
blurMax: Theme.elevationBlurMax
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel3)
}
Rectangle {
anchors.fill: parent
color: root.backgroundColor
border.color: root.borderColor
border.width: root.borderWidth
radius: root.cornerRadius
}
}
MouseArea {

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