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

Compare commits

...

120 Commits

Author SHA1 Message Date
bbedward
3f24cf37ca settings: make horizontal change more smart 2026-02-24 20:49:02 -05:00
bbedward
01218f34cb settings: restore notifyHorizontalBarChanged 2026-02-24 19:43:01 -05:00
purian23
9da58d8296 fix: Update HTML rendering injections 2026-02-24 19:43:01 -05:00
purian23
af0038e634 dbar: Refactor to memoize dbar & widget state via json 2026-02-24 19:20:30 -05:00
purian23
05c312b9eb cpu widget: Fix monitor binding 2026-02-24 19:20:30 -05:00
bbedward
89d5c958c4 settings: use Image in theme colors tab wp preview 2026-02-24 15:23:07 -05:00
bbedward
e4d86ad595 popout: fully unload popout layers on close 2026-02-24 15:20:00 -05:00
bbedward
532b54a028 wallpaper: handle initial load better, add dms randr command for quick physical scale retrieval 2026-02-24 15:20:00 -05:00
bbedward
504d027c3f privacy indicator: fix width when not active 2026-02-24 13:59:58 -05:00
bbedward
e8f95f4533 settings: use Image for per-mode previews 2026-02-24 13:37:34 -05:00
bbedward
b83256c83a matugen: skip theme refreshes if no colors changed 2026-02-24 13:37:34 -05:00
bbedward
8e2cd21be8 dock: fix tooltip positioning 2026-02-24 13:37:34 -05:00
bbedward
c5413608da dankbar: fix some defaults in reset 2026-02-24 13:37:34 -05:00
bbedward
586bcad442 widgets: set updatesEnabled false on background layers, if qs supports it 2026-02-24 13:37:34 -05:00
bbedward
3b3d10f730 widgets: fix moddedAppID consistency 2026-02-24 13:37:34 -05:00
purian23
4834891b36 settings: Re-adjust dbar layout 2026-02-24 13:37:34 -05:00
purian23
f60e65aecb settings: Dankbar layout updates 2026-02-24 13:37:34 -05:00
purian23
01387b0123 fix: Clipboard button widget alignment 2026-02-24 13:37:34 -05:00
bbedward
1476658c23 dankbar: fix syncing settings to new bars 2026-02-24 13:37:34 -05:00
bbedward
7861c6e316 i18n: term sync 2026-02-24 10:52:35 -05:00
bbedward
d2247d7b24 dankbar: restore horizontal change debounce 2026-02-24 10:52:35 -05:00
bbedward
2ff78d4a02 dpms: disable fade overlay in onRequestMonitorOn 2026-02-24 10:52:35 -05:00
bbedward
785243ce5f dankbar: optimize bindings in bar window 2026-02-24 10:52:35 -05:00
bbedward
0e1b868384 widgets: fix undefined icon warnings 2026-02-24 10:52:35 -05:00
null
2b08e800e8 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 10:52:35 -05:00
purian23
74e4f8ea1e display: Fix output config on delete & popup height 2026-02-24 10:52:35 -05:00
purian23
9c58569b4c template: Refine bug report tracker 2026-02-24 10:52:35 -05:00
purian23
29de677e00 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-24 10:52:35 -05:00
purian23
fae4944845 fix: Animated Image warnings 2026-02-24 10:52:34 -05:00
Lucas
07a0ac4b7d doctor: fix imageformats detection (#1811) 2026-02-23 19:45:10 -05:00
bbedward
b2d8f4d73b keybinds: preserve scroll position of expanded item on list change fixes #1766 2026-02-23 19:33:29 -05:00
bbedward
fe58c45233 widgets: fallback when AnimatedImage probe fails to static Image 2026-02-23 19:03:48 -05:00
bbedward
3ea4e389eb thememode: connect to loginctl PrepareForSleep event 2026-02-23 19:03:48 -05:00
purian23
7276f295fc dms-greeter: Update dankinstall greeter automation w/distro packages 2026-02-23 18:53:29 -05:00
bbedward
93ed96a789 launcher: don't tie unload to visibility 2026-02-23 18:53:29 -05:00
purian23
bea325e94c audio: Sync audio hide opts w/dash Output devices 2026-02-23 18:53:29 -05:00
bbedward
2f8f1c30ad audio: fix cycle output, improve icon resolution for sink fixes #1808 2026-02-23 18:53:29 -05:00
Lucas
f859a14173 nix: update flake.lock (#1809) 2026-02-23 18:53:29 -05:00
bbedward
153f39da48 audio: disable effects when mpris player is playing 2026-02-23 18:53:29 -05:00
bbedward
e4accdd1c7 launcher: implement memory for selected tab fixes #1806 2026-02-23 10:20:48 -05:00
dms-ci[bot]
a2c89e0a8c nix: update vendorHash for go.mod changes 2026-02-23 10:20:48 -05:00
bbedward
e282831c2e widgets: make AnimatedImage conditional in DankCircularImage - Cut potential overhead of always using AnimatedImage 2026-02-23 10:20:48 -05:00
bbedward
5c5ff6195a osd: disable media playback OSD by default 2026-02-23 10:20:48 -05:00
Triệu Kha
c4bbf54679 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 10:20:48 -05:00
Jonas Bloch
98acafb4b8 fix(notepad): decode path URI when saving/creating a file (#1805) 2026-02-23 10:20:48 -05:00
Jonas Bloch
da20681fc0 feat: add support for animated gifs as profile pictures (#1804) 2026-02-23 10:20:48 -05:00
purian23
b38cb961b2 dms-greeter: Enable greetd via dms greeter install all-in-one cmd 2026-02-23 10:20:48 -05:00
bbedward
7a0bb07518 matugen: unconditionally run portal sync even if matugen errors 2026-02-22 23:09:18 -05:00
purian23
403e3e90a2 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 23:09:18 -05:00
bbedward
50b91f14b6 launcher: fix frecency ranking in search results fixes #1799 2026-02-22 23:09:18 -05:00
bbedward
b3df47fce0 scripts: fix shellcheck 2026-02-22 23:09:18 -05:00
bbedward
09bd65d746 bluetooth: expose trust/untrust on devices 2026-02-22 23:09:18 -05:00
长夜月玩Fedora
020d56ab7f Add support for 'evernight' distribution in Fedora (#1786) 2026-02-22 23:09:18 -05:00
Triệu Kha
f3bee65da9 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 23:09:18 -05:00
purian23
b14b0946e2 feat: DMS Greeter packaging for Debian/OpenSUSE on OBS 2026-02-22 23:09:18 -05:00
Lucas
ca44205f1c zen: add more commands to detection (#1792) 2026-02-22 23:09:18 -05:00
purian23
2d39e8fd2a ipc: Fix DankDash Wallpaper call 2026-02-22 23:09:18 -05:00
purian23
6d4df6e927 theme: Fix Light/Dark mode portal sync 2026-02-22 23:09:18 -05:00
Connor Welsh
b8ab86e6c0 distro: add cups-pk-helper as suggested dependency (#1670) 2026-02-22 23:09:18 -05:00
bbedward
837329a6d8 window rules: default to fixed for width/height part of #1774 2026-02-22 23:09:18 -05:00
purian23
8c6c2ffd23 ubuntu: Fix dms-git Go versioning to restore builds 2026-02-22 23:09:18 -05:00
bbedward
ad3c8b6755 v1.4.3 version file 2026-02-22 23:07:18 -05:00
bbedward
03a8e1e0d5 clipboard: fix memory leak from unbounded offer maps and unguarded file reads 2026-02-20 11:42:14 -05:00
bbedward
4d4d3c20a1 keybinds/niri: fix quote preservation 2026-02-20 11:42:14 -05:00
bbedward
cef16d6bc9 dankdash: fix widgets across different bar section fixes #1764s 2026-02-20 11:42:14 -05:00
bbedward
aafaad1791 core/screenshot: light cleanups 2026-02-20 11:42:14 -05:00
Patrick Fischer
7906fdc2b0 screensaver: emit ActiveChanged on lock/unlock (#1761) 2026-02-20 11:42:14 -05:00
Triệu Kha
397650ca52 clipboard: improve image thumbnail (#1759)
- thumbnail image is now bigger
- circular mask has been replaced with rounded rectangular mask
2026-02-20 11:42:14 -05:00
purian23
826207006a template: Default install method 2026-02-20 11:42:14 -05:00
purian23
58c2fcd31c issues: Template fix 2026-02-20 11:42:14 -05:00
purian23
b2a2b425ec templates: Fix GitHub issue labels 2026-02-20 11:42:14 -05:00
shorinkiwata
942c9c9609 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 11:42:14 -05:00
purian23
46d6e1cff3 templates: Update DMS issue formats 2026-02-20 11:42:14 -05:00
bbedward
a4137c57c1 running apps: fix ordering on niri 2026-02-19 20:46:26 -05:00
bbedward
1ad8b627f1 launcher: fix premature exit of file search fixes #1749 2026-02-19 16:47:34 -05:00
Jonas Bloch
58a02ce290 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:27:14 -05:00
bbedward
8e1ad1a2be audio: fix hide device not working 2026-02-19 16:24:48 -05:00
bbedward
68cd7ab32c i18n: term sync 2026-02-19 14:11:21 -05:00
Youseffo13
f649ce9a8e 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 14:11:21 -05:00
bbedward
c4df242f07 dankbar: remove behaviors from monitoring widgets 2026-02-19 14:11:21 -05:00
bbedward
26846c8d55 dgop: round computed values to match display format 2026-02-19 14:11:21 -05:00
bbedward
31b44a667c flake: fix dev flake for go 1.25 and ashellchheck 2026-02-19 14:11:21 -05:00
bbedward
4f3b73ee21 hyprland: add serial to output model generator 2026-02-19 09:22:56 -05:00
bbedward
4cfae91f02 dock: fix context menu styling fixes #1742 2026-02-19 09:22:56 -05:00
bbedward
8d947a6e95 dock: fix transparency setting fixes #1739 2026-02-19 09:22:56 -05:00
bbedward
1e84d4252c launcher: improve perf of settings search 2026-02-19 09:22:56 -05:00
bbedward
76072e1d4c launcher: always heuristic lookup cached entries 2026-02-19 09:22:56 -05:00
bbedward
6408dce4a9 launcher v2: always heuristicLookup tab actions 2026-02-18 19:07:30 -05:00
bbedward
0b2e1cca38 i18n: term updates 2026-02-18 18:35:29 -05:00
bbedward
c1bfd8c0b7 system tray: fix to take up 0 space when empty 2026-02-18 18:35:29 -05:00
Youseffo13
90ffa5833b 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:35:29 -05:00
bbedward
169c669286 widgets: add openWith/toggleWith modes for dankbar widgets 2026-02-18 16:24:07 -05:00
bbedward
f8350deafc keybinds: fix escape in keybinds modal 2026-02-18 14:57:53 -05:00
bbedward
0286a1b80b launcher v2: remove calc cc: enhancements for plugins to size details 2026-02-18 14:48:44 -05:00
beluch-dev
7c3e6c1f02 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 14:48:40 -05:00
bbedward
d2d72db3c9 plugins: fix settings focus loss 2026-02-18 13:36:51 -05:00
Evgeny Zemtsov
f81f861408 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 13:36:51 -05:00
bbedward
af494543f5 1.4.2: staging ground 2026-02-18 13:36:43 -05:00
bbedward
db4de55338 popout: decouple shadow from content layer 2026-02-18 10:46:01 -05:00
bbedward
37ecbbbbde popout: disable layer after animation 2026-02-18 10:34:21 -05:00
purian23
d6a6d2a438 notifications: Maintain shadow during expansion 2026-02-18 10:34:21 -05:00
purian23
bf1c6eec74 notifications: Update initial popup height surfaces 2026-02-18 10:34:21 -05:00
bbedward
0ddae80584 running apps: fix scroll events being propagated fixes #1724 2026-02-18 10:34:21 -05:00
bbedward
5c96c03bfa matugen: make v4 detection more resilient 2026-02-18 09:57:35 -05:00
bbedward
dfe36e47d8 process list: fix scaling with fonts fixes #1721 2026-02-18 09:57:35 -05:00
purian23
63e1b75e57 dankinstall: Fix Debian ARM64 detection 2026-02-18 09:57:35 -05:00
bbedward
29efdd8598 matugen: detect emacs directory fixes #1720 2026-02-18 09:57:35 -05:00
bbedward
34d03cf11b osd: optimize bindings 2026-02-18 09:57:35 -05:00
bbedward
c339389d44 screenshot: adjust cursor CLI option to be more explicit 2026-02-17 22:28:46 -05:00
bbedward
af5f6eb656 settings: workaround crash 2026-02-17 22:20:19 -05:00
purian23
a6d28e2553 notifications: Tweak animation scale & settings 2026-02-17 22:07:36 -05:00
bbedward
6213267908 settings: guard internal writes from watcher 2026-02-17 22:03:57 -05:00
bbedward
d084114149 cc: fix plugin reloading in bar position changes 2026-02-17 17:25:19 -05:00
bbedward
f6d99eca0d popout: anchor height changing popout surfaces to top and bottom 2026-02-17 17:25:19 -05:00
bbedward
722eb3289e workspaces: fix named workspace icons 2026-02-17 17:25:19 -05:00
bbedward
b7f2bdcb2d dankinstall: no_anim on dms layers 2026-02-17 17:25:19 -05:00
bbedward
11c20db6e6 1.4.1 2026-02-17 14:08:15 -05:00
bbedward
8a4e3f8bb1 system updater: fix hide no update option 2026-02-17 14:08:04 -05:00
bbedward
bc8fe97c13 launcher: fix kb navigation not always showing last delegate in view 2026-02-17 14:08:04 -05:00
bbedward
47262155aa doctor: add qt6-imageformats check 2026-02-17 14:08:04 -05:00
207 changed files with 12292 additions and 5847 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

@@ -10,6 +10,7 @@ on:
options:
- dms
- dms-git
- dms-greeter
- all
default: "dms"
rebuild_release:
@@ -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
@@ -112,6 +128,10 @@ jobs:
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
@@ -144,6 +164,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 +203,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 +216,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
@@ -283,55 +284,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 +366,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 +375,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,6 +386,9 @@ jobs:
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
fi
UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
@@ -382,13 +400,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 +444,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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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.2/dms-qml.tar.gz</param>
<param name="filename">dms-qml.tar.gz</param>
</service>
</services>

View File

@@ -0,0 +1,6 @@
dms-greeter (1.4.2db8) unstable; urgency=medium
* Initial Debian OBS package
* Port from Ubuntu/Fedora packaging
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 21 Feb 2026 00:00: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

@@ -37,6 +37,7 @@ Recommends: quickshell-git
# Recommended system packages
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct
%description

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

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,13 @@ 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"
fi
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
@@ -444,6 +484,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 +510,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 +651,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 +738,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 +802,17 @@ 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"
fi
fi
@@ -839,8 +939,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 +988,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 +1021,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,7 @@
buildInputs =
with pkgs;
[
go_1_24
go_1_25
gopls
delve
go-tools
@@ -189,6 +189,7 @@
prek
uv # for prek
shellcheck
# Nix development tools
nixd

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

@@ -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: ({})
@@ -517,7 +518,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 +572,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 +588,7 @@ Singleton {
"widgetOutlineOpacity": 1.0,
"widgetOutlineThickness": 1,
"fontScale": 1.0,
"iconScale": 1.0,
"autoHide": false,
"autoHideDelay": 250,
"showOnWindowsOpen": false,
@@ -1025,6 +1031,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 +1250,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 +2597,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

@@ -961,7 +961,6 @@ Singleton {
}
if (!isGreeterMode) {
// Skip with matugen because, our script runner will do it.
if (!matugenAvailable) {
PortalService.setLightMode(light);
}
@@ -1170,21 +1169,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

@@ -341,7 +341,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 +395,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 +411,7 @@ var SPEC = {
widgetOutlineOpacity: 1.0,
widgetOutlineThickness: 1,
fontScale: 1.0,
iconScale: 1.0,
autoHide: false,
autoHideDelay: 250,
showOnWindowsOpen: false,

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();
@@ -420,8 +444,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.batteryPopoutLoader = batteryPopoutLoader;
}
BatteryPopout {
id: batteryPopout
onPopoutClosed: PopoutService.unloadBattery()
Component.onCompleted: {
PopoutService.batteryPopout = batteryPopout;
@@ -434,8 +463,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.layoutPopoutLoader = layoutPopoutLoader;
}
DWLLayoutPopout {
id: layoutPopout
onPopoutClosed: PopoutService.unloadLayoutPopout()
Component.onCompleted: {
PopoutService.layoutPopout = layoutPopout;
@@ -448,8 +482,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.vpnPopoutLoader = vpnPopoutLoader;
}
VpnPopout {
id: vpnPopout
onPopoutClosed: PopoutService.unloadVpn()
Component.onCompleted: {
PopoutService.vpnPopout = vpnPopout;
@@ -462,8 +501,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.processListPopoutLoader = processListPopoutLoader;
}
ProcessListPopout {
id: processListPopout
onPopoutClosed: PopoutService.unloadProcessListPopout()
Component.onCompleted: {
PopoutService.processListPopout = processListPopout;
@@ -506,8 +550,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.appDrawerLoader = appDrawerLoader;
}
AppDrawerPopout {
id: appDrawerPopout
onPopoutClosed: PopoutService.unloadAppDrawer()
Component.onCompleted: {
PopoutService.appDrawerPopout = appDrawerPopout;
@@ -539,8 +588,13 @@ Item {
active: false
Component.onCompleted: {
PopoutService.clipboardHistoryPopoutLoader = clipboardHistoryPopoutLoader;
}
ClipboardHistoryPopout {
id: clipboardHistoryPopout
onPopoutClosed: PopoutService.unloadClipboardHistoryPopout()
Component.onCompleted: {
PopoutService.clipboardHistoryPopout = clipboardHistoryPopout;
@@ -715,8 +769,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

@@ -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"),
@@ -681,24 +674,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();
@@ -931,13 +916,6 @@ Item {
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"));
}
function detectTrigger(query) {
if (!query || query.length === 0)
return {
@@ -1270,6 +1248,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 +1292,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 +1558,6 @@ Item {
case "file":
openFile(item.data?.path);
break;
case "calculator":
copyToClipboard(item.name);
break;
default:
return;
}
@@ -1616,25 +1612,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) {

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

@@ -102,7 +102,7 @@ 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 = "";
@@ -226,6 +226,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 +293,7 @@ Item {
PanelWindow {
id: launcherWindow
visible: root._windowEnabled && (!root.unloadContentOnClose || spotlightOpen || isClosing)
visible: root._windowEnabled && (spotlightOpen || isClosing)
color: "transparent"
exclusionMode: ExclusionMode.Ignore

View File

@@ -190,32 +190,6 @@ function transformPluginItem(item, pluginId, selectLabel) {
};
}
function createCalculatorItem(calc, query, copyLabel) {
return {
id: "calculator_result",
type: "calculator",
name: calc.displayResult,
subtitle: query + " =",
icon: "calculate",
iconType: "material",
section: "calculator",
data: {
expression: calc.expression,
result: calc.result
},
actions: [],
primaryAction: {
name: copyLabel,
icon: "content_copy",
action: "copy"
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
return {

View File

@@ -470,9 +470,6 @@ FocusScope {
onTextChanged: {
controller.setSearchQuery(text);
if (text.length === 0) {
controller.restorePreviousMode();
}
if (actionPanel.expanded) {
actionPanel.hide();
}

View File

@@ -81,6 +81,12 @@ function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, g
return bounds.start + newPosInSection;
}
var currentRow = Math.floor(posInSection / cols);
var lastRow = Math.floor((bounds.count - 1) / cols);
if (currentRow < lastRow) {
return bounds.start + bounds.count - 1;
}
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
return nextSection !== -1 ? nextSection : selectedFlatIndex;
}

View File

@@ -178,8 +178,6 @@ Rectangle {
if (!root.item)
return "";
switch (root.item.type) {
case "calculator":
return I18n.tr("Calc");
case "plugin":
return I18n.tr("Plugin");
case "file":

View File

@@ -130,24 +130,17 @@ Item {
if (!entry || entry.isHeader)
return;
var rowIndex = _flatIndexToRowMap[index];
if (rowIndex === undefined || rowIndex >= _cumulativeHeights.length)
return;
var row = _visualRows[rowIndex];
if (!row)
if (rowIndex === undefined)
return;
var rowY = _cumulativeHeights[rowIndex];
var rowHeight = row.height;
var scrollY = mainListView.contentY - mainListView.originY;
var viewHeight = mainListView.height;
var headerH = stickyHeader.height;
mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
if (rowY < scrollY + headerH) {
mainListView.contentY = Math.max(mainListView.originY, rowY - headerH + mainListView.originY);
return;
}
if (rowY + rowHeight > scrollY + viewHeight) {
mainListView.contentY = rowY + rowHeight - viewHeight + mainListView.originY;
if (stickyHeader.visible && rowIndex < _cumulativeHeights.length) {
var rowY = _cumulativeHeights[rowIndex];
var scrollY = mainListView.contentY - mainListView.originY;
if (rowY < scrollY + stickyHeader.height) {
mainListView.contentY = Math.max(mainListView.originY, rowY - stickyHeader.height + mainListView.originY);
}
}
}

View File

@@ -3,7 +3,7 @@
const Weights = {
exactMatch: 10000,
prefixMatch: 5000,
wordBoundary: 1000,
wordBoundary: 3000,
substring: 500,
fuzzy: 100,
frecency: 2000,
@@ -99,8 +99,8 @@ function getTimeBucketWeight(daysSinceUsed) {
function calculateTextScore(name, query) {
if (name === query) return Weights.exactMatch
if (name.startsWith(query)) return Weights.prefixMatch
if (name.includes(query)) return Weights.substring
if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
if (name.includes(query)) return Weights.substring
if (query.length >= 3) {
var fs = fuzzyScore(name, query)
@@ -140,7 +140,7 @@ function score(item, query, frecencyData) {
if (textScore === 0) return 0
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 10, Weights.frecency) : 0
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0
return textScore + usageBonus + typeBonus
}

View File

@@ -238,37 +238,37 @@ FocusScope {
property var quickAccessLocations: [
{
"name": "Home",
"name": I18n.tr("Home"),
"path": homeDir,
"icon": "home"
},
{
"name": "Documents",
"name": I18n.tr("Documents"),
"path": docsDir,
"icon": "description"
},
{
"name": "Downloads",
"name": I18n.tr("Downloads"),
"path": downloadDir,
"icon": "download"
},
{
"name": "Pictures",
"name": I18n.tr("Pictures"),
"path": picsDir,
"icon": "image"
},
{
"name": "Music",
"name": I18n.tr("Music"),
"path": musicDir,
"icon": "music_note"
},
{
"name": "Videos",
"name": I18n.tr("Videos"),
"path": videosDir,
"icon": "movie"
},
{
"name": "Desktop",
"name": I18n.tr("Desktop"),
"path": desktopDir,
"icon": "computer"
}

View File

@@ -19,7 +19,7 @@ StyledRect {
spacing: 4
StyledText {
text: "Quick Access"
text: I18n.tr("Quick Access")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium

View File

@@ -91,7 +91,12 @@ DankModal {
id: searchField
Layout.alignment: Qt.AlignRight
leftIconName: "search"
keyForwardTargets: [root.modalFocusScope]
onTextEdited: searchDebounce.restart()
Keys.onEscapePressed: event => {
root.close();
event.accepted = true;
}
}
}
@@ -118,6 +123,7 @@ DankModal {
function generateCategories(query) {
const lowerQuery = query ? query.toLowerCase().trim() : "";
const lowerQueryWords = query.split(/\s+/);
const processed = {};
for (const cat in rawBinds) {
@@ -130,10 +136,25 @@ DankModal {
const keyLower = bind.key.toLowerCase();
const descLower = bind.desc.toLowerCase();
const actionLower = bind.action.toLowerCase();
if (!(lowerQuery.length === 0 || keyLower.includes(lowerQuery) || descLower.includes(lowerQuery) || catLower.includes(lowerQuery) || actionLower.includes(lowerQuery)))
continue;
if (bind.hideOnOverlay)
continue;
let shouldContinue = false;
for (let j = 0; j < lowerQueryWords.length; j++) {
const word = lowerQueryWords[j];
if (!(
word.length === 0 ||
keyLower.includes(word) ||
descLower.includes(word) ||
catLower.includes(word) ||
actionLower.includes(word)
)) {
shouldContinue = true;
break;
}
}
if (shouldContinue)
continue;
if (bind.subcat) {
hasSubcats = true;

View File

@@ -83,9 +83,9 @@ FloatingWindow {
objectName: "processListModal"
title: I18n.tr("System Monitor", "sysmon window title")
minimumSize: Qt.size(750, 550)
implicitWidth: 1000
implicitHeight: 720
minimumSize: Qt.size(Math.min(Math.round(Theme.fontSizeMedium * 48), Screen.width), Math.min(Math.round(Theme.fontSizeMedium * 34), Screen.height))
implicitWidth: Math.round(Theme.fontSizeMedium * 71)
implicitHeight: Math.round(Theme.fontSizeMedium * 51)
color: Theme.surfaceContainer
visible: false
@@ -236,7 +236,7 @@ FloatingWindow {
Item {
Layout.fillWidth: true
Layout.preferredHeight: 48
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.4)
MouseArea {
anchors.fill: parent
@@ -293,10 +293,10 @@ FloatingWindow {
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 52
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.7)
Layout.leftMargin: Theme.spacingL
Layout.rightMargin: Theme.spacingL
spacing: Theme.spacingL
spacing: Theme.spacingM
Row {
spacing: 2
@@ -322,14 +322,15 @@ FloatingWindow {
]
Rectangle {
width: 120
height: 44
width: tabRowContent.implicitWidth + Theme.spacingM * 2
height: Math.round(Theme.fontSizeMedium * 3.1)
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
id: tabRowContent
anchors.centerIn: parent
spacing: Theme.spacingXS
@@ -373,11 +374,13 @@ FloatingWindow {
DankButtonGroup {
id: processFilterGroup
Layout.minimumWidth: implicitWidth + 8
model: [I18n.tr("All"), I18n.tr("User"), I18n.tr("System")]
currentIndex: 0
checkEnabled: false
buttonHeight: 36
buttonHeight: Math.round(Theme.fontSizeSmall * 2.6)
minButtonWidth: 0
buttonPadding: Theme.spacingS
textSize: Theme.fontSizeSmall
visible: currentTab === 0
onSelectionChanged: (index, selected) => {
if (!selected)
@@ -400,9 +403,9 @@ FloatingWindow {
DankTextField {
id: searchField
Layout.fillWidth: true
Layout.maximumWidth: 250
Layout.minimumWidth: 120
Layout.preferredHeight: 40
Layout.maximumWidth: Math.round(Theme.fontSizeMedium * 18)
Layout.minimumWidth: Theme.fontSizeMedium * 4
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 2.8)
placeholderText: I18n.tr("Search processes...", "process search placeholder")
leftIconName: "search"
showClearButton: true
@@ -470,7 +473,7 @@ FloatingWindow {
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 32
Layout.preferredHeight: Math.round(Theme.fontSizeSmall * 2.7)
Layout.leftMargin: Theme.spacingL
Layout.rightMargin: Theme.spacingL
Layout.bottomMargin: Theme.spacingM

View File

@@ -631,7 +631,7 @@ FloatingWindow {
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: "fixed 800"
placeholderText: "800"
backgroundColor: "transparent"
enabled: root.visible
}
@@ -658,7 +658,7 @@ FloatingWindow {
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: "fixed 600"
placeholderText: "600"
backgroundColor: "transparent"
enabled: root.visible
}

View File

@@ -8,10 +8,39 @@ DankPopout {
layerNamespace: "dms:app-launcher"
property string _pendingMode: ""
property string _pendingQuery: ""
function show() {
open();
}
function openWithMode(mode) {
_pendingMode = mode || "";
open();
}
function toggleWithMode(mode) {
if (shouldBeVisible) {
close();
return;
}
openWithMode(mode);
}
function openWithQuery(query) {
_pendingQuery = query || "";
open();
}
function toggleWithQuery(query) {
if (shouldBeVisible) {
close();
return;
}
openWithQuery(query);
}
popupWidth: 560
popupHeight: 640
triggerWidth: 40
@@ -30,20 +59,39 @@ DankPopout {
var lc = contentLoader.item?.launcherContent;
if (!lc)
return;
const query = _pendingQuery;
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
_pendingMode = "";
_pendingQuery = "";
if (lc.searchField) {
lc.searchField.text = "";
lc.searchField.text = query;
lc.searchField.forceActiveFocus();
}
if (lc.controller) {
lc.controller.searchMode = "apps";
lc.controller.searchMode = mode;
lc.controller.pluginFilter = "";
lc.controller.searchQuery = "";
lc.controller.performSearch();
if (query) {
lc.controller.setSearchQuery(query);
} else {
lc.controller.performSearch();
}
}
lc.resetScroll?.();
lc.actionPanel?.hide();
}
Connections {
target: contentLoader.item?.launcherContent?.controller ?? null
function onModeChanged(mode) {
if (contentLoader.item.launcherContent.controller.autoSwitchedToFiles)
return;
SessionData.setAppDrawerLastMode(mode);
}
}
content: Component {
Rectangle {
id: contentContainer

View File

@@ -85,12 +85,12 @@ Variants {
}
Component.onCompleted: {
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
if (!source) {
isInitialized = true;
return;
root._renderSettling = false;
}
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
setWallpaperImmediate(formattedSource);
isInitialized = true;
}
@@ -98,8 +98,25 @@ Variants {
property real transitionProgress: 0
readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false
property bool _renderSettling: true
property bool useNextForEffect: false
Connections {
target: currentWallpaper
function onStatusChanged() {
if (currentWallpaper.status === Image.Ready) {
root._renderSettling = true;
renderSettleTimer.restart();
}
}
}
Timer {
id: renderSettleTimer
interval: 100
onTriggered: root._renderSettling = false
}
onSourceChanged: {
if (!source || source.startsWith("#")) {
setWallpaperImmediate("");
@@ -124,6 +141,8 @@ Variants {
transitionAnimation.stop();
root.transitionProgress = 0.0;
root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = newSource;
nextWallpaper.source = "";
}

View File

@@ -15,18 +15,22 @@ Item {
property var pluginDetailInstance: null
property var widgetModel: null
property var collapseCallback: null
property real maxAvailableHeight: 9999
function getDetailHeight(section) {
const maxAvailable = parent ? parent.height - Theme.spacingS : 9999;
switch (true) {
case section === "wifi":
case section === "bluetooth":
case section === "builtin_vpn":
return Math.min(350, maxAvailable);
return Math.min(350, maxAvailableHeight);
case section.startsWith("brightnessSlider_"):
return Math.min(400, maxAvailable);
return Math.min(400, maxAvailableHeight);
case section.startsWith("plugin_"):
if (pluginDetailInstance?.ccDetailHeight)
return Math.min(pluginDetailInstance.ccDetailHeight, maxAvailableHeight);
return Math.min(250, maxAvailableHeight);
default:
return Math.min(250, maxAvailable);
return Math.min(250, maxAvailableHeight);
}
}

View File

@@ -31,11 +31,26 @@ Column {
spacing: editMode ? Theme.spacingL : Theme.spacingS
property real maxPopoutHeight: 9999
property var currentRowWidgets: []
property real currentRowWidth: 0
property int expandedRowIndex: -1
property var colorPickerModal: null
readonly property real _maxDetailHeight: {
const rows = layoutResult.rows;
let totalRowHeight = 0;
for (let i = 0; i < rows.length; i++) {
const sliderOnly = rows[i].every(w => {
const id = w.id || "";
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
});
totalRowHeight += sliderOnly ? 36 : 60;
}
const rowSpacing = Math.max(0, rows.length - 1) * spacing;
return Math.max(100, maxPopoutHeight - totalRowHeight - rowSpacing);
}
function calculateRowsAndWidgets() {
return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex);
}
@@ -163,6 +178,7 @@ Column {
DetailHost {
id: detailHost
width: parent.width
maxAvailableHeight: root._maxDetailHeight
height: active ? (getDetailHeight(root.expandedSection) + Theme.spacingS) : 0
property bool active: {
if (root.expandedSection === "")
@@ -945,22 +961,31 @@ Column {
}
}
Component.onCompleted: {
Qt.callLater(() => {
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
if (pluginComponent) {
const instance = pluginComponent.createObject(null, {
"pluginId": pluginId,
"pluginService": PluginService,
"visible": false,
"width": 0,
"height": 0
});
if (instance) {
pluginInstance = instance;
}
function tryCreatePluginInstance() {
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
if (!pluginComponent)
return false;
try {
const instance = pluginComponent.createObject(null, {
"pluginId": pluginId,
"pluginService": PluginService,
"visible": false,
"width": 0,
"height": 0
});
if (instance) {
pluginInstance = instance;
return true;
}
});
} catch (e) {
console.warn("DragDropGrid: stale plugin component for", pluginId, "- reloading");
PluginService.reloadPlugin(pluginId);
}
return false;
}
Component.onCompleted: {
Qt.callLater(() => tryCreatePluginInstance());
}
Connections {
@@ -970,6 +995,11 @@ Column {
pluginInstance.loadPluginData();
}
}
function onPluginLoaded(loadedPluginId) {
if (loadedPluginId !== pluginId || pluginInstance)
return;
Qt.callLater(() => tryCreatePluginInstance());
}
}
Component.onDestruction: {

View File

@@ -13,8 +13,8 @@ Row {
property Item popoutContent: null
signal addWidget(string widgetId)
signal resetToDefault()
signal clearAll()
signal resetToDefault
signal clearAll
height: 48
spacing: Theme.spacingS
@@ -28,7 +28,7 @@ Row {
y: parent ? Math.round((parent.height - height) / 2) : 0
width: 400
height: 300
modal: true
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
@@ -133,7 +133,7 @@ Row {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.addWidget(modelData.id)
root.addWidget(modelData.id);
}
}
}

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