1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-14 16:22:46 -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
323 changed files with 5803 additions and 59720 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }} token: ${{ steps.app_token.outputs.token }}
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install flatpak - name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak run: sudo apt update && sudo apt install -y flatpak
@@ -38,7 +38,7 @@ jobs:
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install flatpak - name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak run: sudo apt update && sudo apt install -y flatpak
@@ -21,7 +21,7 @@ jobs:
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: core/go.mod go-version-file: core/go.mod
+8 -8
View File
@@ -32,13 +32,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
@@ -106,7 +106,7 @@ jobs:
- name: Upload artifacts (${{ matrix.arch }}) - name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64' if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: core-assets-${{ matrix.arch }} name: core-assets-${{ matrix.arch }}
path: | path: |
@@ -120,7 +120,7 @@ jobs:
- name: Upload artifacts with completions - name: Upload artifacts with completions
if: matrix.arch == 'amd64' if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: core-assets-${{ matrix.arch }} name: core-assets-${{ matrix.arch }}
path: | path: |
@@ -147,7 +147,7 @@ jobs:
# private-key: ${{ secrets.APP_PRIVATE_KEY }} # private-key: ${{ secrets.APP_PRIVATE_KEY }}
# - name: Checkout # - name: Checkout
# uses: actions/checkout@v6 # uses: actions/checkout@v4
# with: # with:
# token: ${{ steps.app_token.outputs.token }} # token: ${{ steps.app_token.outputs.token }}
# fetch-depth: 0 # fetch-depth: 0
@@ -181,7 +181,7 @@ jobs:
TAG: ${{ inputs.tag }} TAG: ${{ inputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
@@ -192,12 +192,12 @@ jobs:
git checkout ${TAG} git checkout ${TAG}
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
pattern: core-assets-* pattern: core-assets-*
merge-multiple: true merge-multiple: true
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Determine version - name: Determine version
id: version id: version
@@ -134,7 +134,7 @@ jobs:
rpm -qpi "$SRPM" rpm -qpi "$SRPM"
- name: Upload SRPM artifact - name: Upload SRPM artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }} name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }} path: ${{ steps.build.outputs.srpm_path }}
+10 -12
View File
@@ -9,8 +9,8 @@ on:
type: choice type: choice
options: options:
- dms - dms
- dms-greeter
- dms-git - dms-git
- dms-greeter
- all - all
default: "dms" default: "dms"
rebuild_release: rebuild_release:
@@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -119,8 +119,9 @@ jobs:
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)" echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then elif [[ "$PKG" == "all" ]]; then
# Check each stable package and build list of those needing updates # Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=() PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
if check_dms_stable; then if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms") PACKAGES_TO_UPDATE+=("dms")
if [[ -n "$LATEST_TAG" ]]; then if [[ -n "$LATEST_TAG" ]]; then
@@ -139,7 +140,7 @@ jobs:
else else
echo "packages=" >> $GITHUB_OUTPUT echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ Both packages up to date" echo "✓ All packages up to date"
fi fi
elif [[ "$PKG" == "dms-git" ]]; then elif [[ "$PKG" == "dms-git" ]]; then
@@ -195,13 +196,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Wait before OBS upload
run: sleep 3
- name: Determine packages to update - name: Determine packages to update
id: packages id: packages
run: | run: |
@@ -247,7 +245,7 @@ jobs:
fi fi
- name: Update dms-git spec version - name: Update dms-git spec version
if: contains(steps.packages.outputs.packages, 'dms-git') if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: | run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD) COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD) COMMIT_COUNT=$(git rev-list --count HEAD)
@@ -268,7 +266,7 @@ jobs:
} > distro/opensuse/dms-git.spec } > distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version - name: Update Debian dms-git changelog version
if: contains(steps.packages.outputs.packages, 'dms-git') if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: | run: |
COMMIT_HASH=$(git rev-parse --short=8 HEAD) COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD) COMMIT_COUNT=$(git rev-list --count HEAD)
@@ -347,7 +345,7 @@ jobs:
done done
- name: Install Go - name: Install Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
@@ -391,7 +389,7 @@ jobs:
UPLOADED_PACKAGES=() UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=() SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check) # PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload # Loop through each package and upload
for PKG in $PACKAGES; do for PKG in $PACKAGES; do
echo "" echo ""
+9 -19
View File
@@ -4,15 +4,9 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
package: package:
description: "Package to upload" description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: true required: false
type: choice default: "dms-git"
options:
- dms
- dms-greeter
- dms-git
- all
default: "dms"
rebuild_release: rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)" description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false required: false
@@ -31,7 +25,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -145,7 +139,7 @@ jobs:
fi fi
else else
# Fallback # Fallback
echo "packages=dms" >> $GITHUB_OUTPUT echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
fi fi
@@ -157,12 +151,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache: false cache: false
@@ -215,7 +209,7 @@ jobs:
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE" echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi fi
# PACKAGES can be space-separated list (e.g., "dms-git dms dms-greeter" from "all" check) # PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload # Loop through each package and upload
for PKG in $PACKAGES; do for PKG in $PACKAGES; do
# Map package to PPA name # Map package to PPA name
@@ -242,11 +236,7 @@ jobs:
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE" echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ppa-upload.sh uploads to questing + resolute when series is omitted bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
if ! bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}; then
echo "::error::Upload failed for $PKG"
exit 1
fi
done done
- name: Summary - name: Summary
+4 -4
View File
@@ -24,7 +24,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }} token: ${{ steps.app_token.outputs.token }}
@@ -40,7 +40,7 @@ jobs:
echo "Build succeeded, no hash update needed" echo "Build succeeded, no hash update needed"
exit 0 exit 0
fi fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true) new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; } [ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix) current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; } [ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
@@ -59,8 +59,8 @@ jobs:
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0 git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin ${{ github.ref_name }} git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }} git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
else else
echo "No changes to flake.nix" echo "No changes to flake.nix"
fi fi
-8
View File
@@ -1,13 +1,5 @@
This file is more of a quick reference so I know what to account for before next releases. This file is more of a quick reference so I know what to account for before next releases.
# 1.5.0
- Overhauled shadows
- App ID changed to com.danklinux.dms - breaking for window rules
- Greeter stuff
- Terminal mux
- Locale overrides
- new neovim theming
# 1.4.0 # 1.4.0
- Overhauled system monitor, graphs, styling - Overhauled system monitor, graphs, styling
+1 -3
View File
@@ -86,9 +86,7 @@ touch .qmlls.ini
4. Restart dms to generate the `.qmlls.ini` file 4. Restart dms to generate the `.qmlls.ini` file
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`. 5. Make your changes, test, and open a pull request.
6. Make your changes, test, and open a pull request.
### I18n/Localization ### I18n/Localization
+2 -6
View File
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help .PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
all: build all: build
@@ -32,9 +32,6 @@ clean:
@$(MAKE) -C $(CORE_DIR) clean @$(MAKE) -C $(CORE_DIR) clean
@echo "Clean complete" @echo "Clean complete"
lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh
# Installation targets # Installation targets
install-bin: install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@@ -79,7 +76,7 @@ install-desktop:
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true @update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry installed" @echo "Desktop entry installed"
install: install-bin install-shell install-completions install-systemd install-icon install-desktop install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
@echo "" @echo ""
@echo "Installation complete!" @echo "Installation complete!"
@echo "" @echo ""
@@ -133,7 +130,6 @@ help:
@echo " all (default) - Build the DMS binary" @echo " all (default) - Build the DMS binary"
@echo " build - Same as 'all'" @echo " build - Same as 'all'"
@echo " clean - Clean build artifacts" @echo " clean - Clean build artifacts"
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
@echo "" @echo ""
@echo "Install:" @echo "Install:"
@echo " install - Build and install everything (requires sudo)" @echo " install - Build and install everything (requires sudo)"
-6
View File
@@ -28,12 +28,6 @@ packages:
outpkg: mocks_brightness outpkg: mocks_brightness
interfaces: interfaces:
DBusConn: DBusConn:
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
config:
dir: "internal/mocks/geolocation"
outpkg: mocks_geolocation
interfaces:
Client:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network: github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config: config:
dir: "internal/mocks/network" dir: "internal/mocks/network"
+4 -17
View File
@@ -1,26 +1,13 @@
repos: repos:
- repo: local - repo: https://github.com/golangci/golangci-lint
rev: v2.9.0
hooks: hooks:
- id: golangci-lint-fmt - id: golangci-lint-fmt
name: golangci-lint-fmt
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
language: system
require_serial: true require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-full - id: golangci-lint-full
name: golangci-lint-full
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-config-verify - id: golangci-lint-config-verify
name: golangci-lint-config-verify - repo: local
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify hooks:
language: system
files: \.golangci\.(?:yml|yaml|toml|json)
pass_filenames: false
- id: go-test - id: go-test
name: go test name: go test
entry: go test ./... entry: go test ./...
+3 -3
View File
@@ -63,19 +63,19 @@ endif
build-all: build dankinstall build-all: build dankinstall
install: install: build
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installation complete" @echo "Installation complete"
install-all: install-all: build-all
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete" @echo "Installation complete"
install-dankinstall: install-dankinstall: dankinstall
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete" @echo "Installation complete"
@@ -1,10 +0,0 @@
{
"policy_version": 1,
"blocked_commands": [
"greeter install",
"greeter enable",
"greeter uninstall",
"setup"
],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
}
+1 -11
View File
@@ -222,19 +222,16 @@ func init() {
func runClipCopy(cmd *cobra.Command, args []string) { func runClipCopy(cmd *cobra.Command, args []string) {
var data []byte var data []byte
copyFromStdin := false
switch { switch {
case len(args) > 0: case len(args) > 0:
data = []byte(args[0]) data = []byte(args[0])
case clipCopyDownload || clipCopyType == "__multi__": default:
var err error var err error
data, err = io.ReadAll(os.Stdin) data, err = io.ReadAll(os.Stdin)
if err != nil { if err != nil {
log.Fatalf("read stdin: %v", err) log.Fatalf("read stdin: %v", err)
} }
default:
copyFromStdin = true
} }
if clipCopyDownload { if clipCopyDownload {
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
return return
} }
if copyFromStdin {
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err)
}
return
}
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil { if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err) log.Fatalf("copy: %v", err)
} }
+3 -2
View File
@@ -64,8 +64,9 @@ var killCmd = &cobra.Command{
} }
var ipcCmd = &cobra.Command{ var ipcCmd = &cobra.Command{
Use: "ipc [target] [function] [args...]", Use: "ipc [target] [function] [args...]",
Short: "Send IPC commands to running DMS shell", Short: "Send IPC commands to running DMS shell",
PreRunE: findConfig,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args) _ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
+5 -5
View File
@@ -1079,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
if currentCategory != -1 { if currentCategory != -1 {
sb.WriteString("\n") sb.WriteString("\n")
} }
fmt.Fprintf(&sb, "**%s**\n", r.category.String()) sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
currentCategory = r.category currentCategory = r.category
} }
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message) sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
if doctorVerbose && r.details != "" { if doctorVerbose && r.details != "" {
fmt.Fprintf(&sb, " - %s\n", r.details) sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
} }
} }
@@ -1096,8 +1096,8 @@ func formatResultsPlain(results []checkResult) string {
} }
sb.WriteString("\n---\n") sb.WriteString("\n---\n")
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n", sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()) ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
return sb.String() return sb.String()
} }
File diff suppressed because it is too large Load Diff
+1 -5
View File
@@ -57,11 +57,10 @@ func init() {
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal") cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant") cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip") cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
} }
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion") matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting") matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
} }
func buildMatugenOptions(cmd *cobra.Command) matugen.Options { func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
@@ -78,7 +77,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal") syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark") terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
skipTemplates, _ := cmd.Flags().GetString("skip-templates") skipTemplates, _ := cmd.Flags().GetString("skip-templates")
contrast, _ := cmd.Flags().GetFloat64("contrast")
return matugen.Options{ return matugen.Options{
StateDir: stateDir, StateDir: stateDir,
@@ -89,7 +87,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
Mode: matugen.ColorMode(mode), Mode: matugen.ColorMode(mode),
IconTheme: iconTheme, IconTheme: iconTheme,
MatugenType: matugenType, MatugenType: matugenType,
Contrast: contrast,
RunUserTemplates: runUserTemplates, RunUserTemplates: runUserTemplates,
StockColors: stockColors, StockColors: stockColors,
SyncModeWithPortal: syncModeWithPortal, SyncModeWithPortal: syncModeWithPortal,
@@ -131,7 +128,6 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
"syncModeWithPortal": opts.SyncModeWithPortal, "syncModeWithPortal": opts.SyncModeWithPortal,
"terminalsAlwaysDark": opts.TerminalsAlwaysDark, "terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"skipTemplates": opts.SkipTemplates, "skipTemplates": opts.SkipTemplates,
"contrast": opts.Contrast,
"wait": wait, "wait": wait,
}, },
} }
-8
View File
@@ -22,8 +22,6 @@ var (
ssNoClipboard bool ssNoClipboard bool
ssNoFile bool ssNoFile bool
ssNoNotify bool ssNoNotify bool
ssNoConfirm bool
ssReset bool
ssStdout bool ssStdout bool
) )
@@ -52,10 +50,8 @@ Examples:
dms screenshot output -o DP-1 # Specific output dms screenshot output -o DP-1 # Specific output
dms screenshot window # Focused window (Hyprland) dms screenshot window # Focused window (Hyprland)
dms screenshot last # Last region (pre-selected) dms screenshot last # Last region (pre-selected)
dms screenshot --reset # Reset last region pre-selection
dms screenshot --no-clipboard # Save file only dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only dms screenshot --no-file # Clipboard only
dms screenshot --no-confirm # Region capture on mouse release
dms screenshot --cursor=on # Include cursor dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`, dms screenshot -f jpg -q 85 # JPEG with quality 85`,
} }
@@ -123,8 +119,6 @@ func init() {
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard") screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file") screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification") screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)") screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
screenshotCmd.AddCommand(ssRegionCmd) screenshotCmd.AddCommand(ssRegionCmd)
@@ -148,8 +142,6 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config.Clipboard = !ssNoClipboard config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify config.Notify = !ssNoNotify
config.NoConfirm = ssNoConfirm
config.Reset = ssReset
config.Stdout = ssStdout config.Stdout = ssStdout
if ssOutputDir != "" { if ssOutputDir != "" {
+3 -4
View File
@@ -16,10 +16,9 @@ import (
) )
var setupCmd = &cobra.Command{ var setupCmd = &cobra.Command{
Use: "setup", Use: "setup",
Short: "Deploy DMS configurations", Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts", Long: "Deploy compositor and terminal configurations with interactive prompts",
PersistentPreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil { if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err) log.Fatalf("Error during setup: %v", err)
-271
View File
@@ -1,271 +0,0 @@
package main
import (
"bufio"
_ "embed"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/spf13/cobra"
)
const (
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
)
var (
immutablePolicyOnce sync.Once
immutablePolicy immutableCommandPolicy
immutablePolicyErr error
)
//go:embed assets/cli-policy.default.json
var defaultCLIPolicyJSON []byte
type immutableCommandPolicy struct {
ImmutableSystem bool
ImmutableReason string
BlockedCommands []string
Message string
}
type cliPolicyFile struct {
PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"`
}
func normalizeCommandSpec(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.TrimPrefix(normalized, "dms ")
return strings.Join(strings.Fields(normalized), " ")
}
func normalizeBlockedCommands(raw []string) []string {
normalized := make([]string, 0, len(raw))
seen := make(map[string]bool)
for _, cmd := range raw {
spec := normalizeCommandSpec(cmd)
if spec == "" || seen[spec] {
continue
}
seen[spec] = true
normalized = append(normalized, spec)
}
return normalized
}
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
normalizedPath := normalizeCommandSpec(commandPath)
if normalizedPath == "" {
return false
}
for _, entry := range blocked {
spec := normalizeCommandSpec(entry)
if spec == "" {
continue
}
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
return true
}
}
return false
}
func loadPolicyFile(path string) (*cliPolicyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var policy cliPolicyFile
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
}
return &policy, nil
}
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
policyFile, err := loadPolicyFile(path)
if err != nil {
return err
}
if policyFile == nil {
return nil
}
if policyFile.ImmutableSystem != nil {
base.ImmutableSystem = *policyFile.ImmutableSystem
}
if policyFile.BlockedCommands != nil {
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
}
if policyFile.Message != nil {
msg := strings.TrimSpace(*policyFile.Message)
if msg != "" {
base.Message = msg
}
}
return nil
}
func readOSReleaseMap(path string) map[string]string {
values := make(map[string]string)
file, err := os.Open(path)
if err != nil {
return values
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToUpper(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
values[key] = strings.ToLower(value)
}
return values
}
func hasAnyToken(text string, tokens ...string) bool {
if text == "" {
return false
}
for _, token := range tokens {
if strings.Contains(text, token) {
return true
}
}
return false
}
func detectImmutableSystem() (bool, string) {
if _, err := os.Stat("/run/ostree-booted"); err == nil {
return true, "/run/ostree-booted is present"
}
osRelease := readOSReleaseMap("/etc/os-release")
if len(osRelease) == 0 {
return false, ""
}
id := osRelease["ID"]
idLike := osRelease["ID_LIKE"]
variantID := osRelease["VARIANT_ID"]
name := osRelease["NAME"]
prettyName := osRelease["PRETTY_NAME"]
immutableIDs := map[string]bool{
"bluefin": true,
"bazzite": true,
"silverblue": true,
"kinoite": true,
"sericea": true,
"onyx": true,
"aurora": true,
"fedora-iot": true,
"fedora-coreos": true,
}
if immutableIDs[id] {
return true, "os-release ID=" + id
}
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
if hasAnyToken(variantID, markers...) {
return true, "os-release VARIANT_ID=" + variantID
}
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
return true, "os-release ID_LIKE=" + idLike
}
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
return true, "os-release identifies an atomic/ostree variant"
}
return false, ""
}
func getImmutablePolicy() (*immutableCommandPolicy, error) {
immutablePolicyOnce.Do(func() {
detectedImmutable, reason := detectImmutableSystem()
immutablePolicy = immutableCommandPolicy{
ImmutableSystem: detectedImmutable,
ImmutableReason: reason,
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
}
var defaultPolicy cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return
}
if defaultPolicy.BlockedCommands != nil {
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
}
if defaultPolicy.Message != nil {
msg := strings.TrimSpace(*defaultPolicy.Message)
if msg != "" {
immutablePolicy.Message = msg
}
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
immutablePolicyErr = err
return
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
immutablePolicyErr = err
return
}
})
if immutablePolicyErr != nil {
return nil, immutablePolicyErr
}
return &immutablePolicy, nil
}
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
policy, err := getImmutablePolicy()
if err != nil {
return err
}
if !policy.ImmutableSystem {
return nil
}
commandPath := normalizeCommandSpec(cmd.CommandPath())
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
return nil
}
reason := ""
if policy.ImmutableReason != "" {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
}
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
}
+10 -1
View File
@@ -16,10 +16,19 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) // Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
+10 -1
View File
@@ -11,20 +11,29 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) // Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
} }
func main() { func main() {
// Block root
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
+3 -57
View File
@@ -192,9 +192,6 @@ func runShellInteractive(session bool) {
} }
} }
// ! TODO - remove when QS 0.3 is up and we can use the pragma
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
if isSessionManaged && hasSystemdRun() { if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope") cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
} }
@@ -435,9 +432,6 @@ func runShellDaemon(session bool) {
} }
} }
// ! TODO - remove when QS 0.3 is up and we can use the pragma
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
if isSessionManaged && hasSystemdRun() { if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope") cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
} }
@@ -622,43 +616,6 @@ func getShellIPCCompletions(args []string, _ string) []string {
return nil return nil
} }
func getFirstDMSPID() (int, bool) {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
continue
}
proc, err := os.FindProcess(pid)
if err != nil {
continue
}
if proc.Signal(syscall.Signal(0)) != nil {
continue
}
return pid, true
}
return 0, false
}
func runShellIPCCommand(args []string) { func runShellIPCCommand(args []string) {
if len(args) == 0 { if len(args) == 0 {
printIPCHelp() printIPCHelp()
@@ -670,21 +627,10 @@ func runShellIPCCommand(args []string) {
} }
cmdArgs := []string{"ipc"} cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() {
switch pid, ok := getFirstDMSPID(); { cmdArgs = append(cmdArgs, "--any-display")
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
log.Fatalf("Error finding config: %v", err)
}
// ! TODO - remove check when QS 0.3 is released
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
} }
cmdArgs = append(cmdArgs, "-p", configPath)
cmdArgs = append(cmdArgs, args...) cmdArgs = append(cmdArgs, args...)
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
+8
View File
@@ -7,6 +7,14 @@ import (
"strings" "strings"
) )
func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd)
if err != nil {
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
}
return path, nil
}
func isArchPackageInstalled(packageName string) bool { func isArchPackageInstalled(packageName string) bool {
cmd := exec.Command("pacman", "-Q", packageName) cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run() err := cmd.Run()
+1 -9
View File
@@ -1,8 +1,6 @@
module github.com/AvengeMedia/DankMaterialShell/core module github.com/AvengeMedia/DankMaterialShell/core
go 1.26.0 go 1.25.0
toolchain go1.26.1
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
@@ -18,8 +16,6 @@ require (
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.3.0
github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
@@ -36,19 +32,15 @@ require (
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.50.0 // indirect
) )
-12
View File
@@ -58,8 +58,6 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -77,8 +75,6 @@ github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -119,8 +115,6 @@ github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo= github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -148,12 +142,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+8 -92
View File
@@ -1,12 +1,10 @@
package clipboard package clipboard
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"syscall" "syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
@@ -14,37 +12,17 @@ import (
) )
func Copy(data []byte, mimeType string) error { func Copy(data []byte, mimeType string) error {
return CopyReader(bytes.NewReader(data), mimeType, false, false) return CopyOpts(data, mimeType, false, false)
} }
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
return copyServeWithWriter(func(writer io.Writer) error {
total := 0
for total < len(data) {
n, err := writer.Write(data[total:])
total += n
if err != nil {
return err
}
}
if total != len(data) {
return io.ErrShortWrite
}
return nil
}, mimeType, pasteOnce)
}
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if !foreground { if !foreground {
return copyFork(data, mimeType, pasteOnce) return copyFork(data, mimeType, pasteOnce)
} }
return copyServeReader(data, mimeType, pasteOnce) return copyServe(data, mimeType, pasteOnce)
} }
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error { func copyFork(data []byte, mimeType string, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground"} args := []string{os.Args[0], "cl", "copy", "--foreground"}
if pasteOnce { if pasteOnce {
args = append(args, "--paste-once") args = append(args, "--paste-once")
@@ -52,15 +30,11 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
args = append(args, "--type", mimeType) args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil cmd.Stdout = nil
cmd.Stderr = nil cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if stdinSource, ok := data.(*os.File); ok {
cmd.Stdin = stdinSource
return cmd.Start()
}
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdin pipe: %w", err) return fmt.Errorf("stdin pipe: %w", err)
@@ -70,66 +44,16 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
return fmt.Errorf("start: %w", err) return fmt.Errorf("start: %w", err)
} }
if _, err := io.Copy(stdin, data); err != nil { if _, err := stdin.Write(data); err != nil {
stdin.Close() stdin.Close()
return fmt.Errorf("write stdin: %w", err) return fmt.Errorf("write stdin: %w", err)
} }
if err := stdin.Close(); err != nil { stdin.Close()
return fmt.Errorf("close stdin: %w", err)
}
return nil return nil
} }
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error { func copyServe(data []byte, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
if cacheDir, err := os.UserCacheDir(); err == nil {
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
}
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
for _, dir := range preferredDirs {
if err := os.MkdirAll(dir, 0o700); err != nil {
continue
}
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
if err == nil {
return cachedData, nil
}
}
return os.CreateTemp("", "dms-clipboard-*")
}
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("") display, err := wlclient.Connect("")
if err != nil { if err != nil {
return fmt.Errorf("wayland connect: %w", err) return fmt.Errorf("wayland connect: %w", err)
@@ -215,18 +139,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
cancelled := make(chan struct{}) cancelled := make(chan struct{})
pasted := make(chan struct{}, 1) pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd) defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
if err := writeTo(file); err != nil { file.Write(data)
select {
case sendErr <- err:
default:
}
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -247,8 +165,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
select { select {
case <-cancelled: case <-cancelled:
return nil return nil
case err := <-sendErr:
return err
case <-pasted: case <-pasted:
if pasteOnce { if pasteOnce {
return nil return nil
-1
View File
@@ -252,7 +252,6 @@ window-rule {
// Open dms windows as floating by default // Open dms windows as floating by default
window-rule { window-rule {
match app-id=r#"org.quickshell$"# match app-id=r#"org.quickshell$"#
match app-id=r#"com.danklinux.dms$"#
open-floating true open-floating true
} }
debug { debug {
+62 -109
View File
@@ -135,42 +135,6 @@ func (a *ArchDistribution) packageInstalled(pkg string) bool {
return err == nil return err == nil
} }
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
data, err := os.ReadFile(srcinfoPath)
if err != nil {
return nil, nil, err
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
var pkg string
var target *[]string
switch {
case strings.HasPrefix(line, "makedepends = "):
pkg = strings.TrimPrefix(line, "makedepends = ")
target = &makedeps
case strings.HasPrefix(line, "depends = "):
pkg = strings.TrimPrefix(line, "depends = ")
target = &deps
default:
continue
}
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
pkg = pkg[:idx]
}
pkg = strings.TrimSpace(pkg)
if pkg != "" {
*target = append(*target, pkg)
}
}
return deps, makedeps, nil
}
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
return exec.Command("pacman", "-Si", pkg).Run() == nil
}
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
} }
@@ -476,10 +440,29 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", "))) a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
hasNiri := false hasNiri := false
hasQuickshell := false
for _, pkg := range packages { for _, pkg := range packages {
if pkg == "niri-git" { if pkg == "niri-git" {
hasNiri = true hasNiri = true
} }
if pkg == "quickshell" || pkg == "quickshell-git" {
hasQuickshell = true
}
}
// If quickshell is in the list, always reinstall google-breakpad first
if hasQuickshell {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.63,
Step: "Reinstalling google-breakpad for quickshell...",
IsComplete: false,
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
}
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
}
} }
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed // If niri is in the list, install makepkg-git-lfs-proto first if not already installed
@@ -560,16 +543,6 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
} }
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error { func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
}
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
if visited[pkg] {
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
return nil
}
visited[pkg] = true
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err) return fmt.Errorf("failed to get user home directory: %w", err)
@@ -643,8 +616,48 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err) return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
} }
srcinfoPath = filepath.Join(packageDir, ".SRCINFO") // Skip dependency installation for dms-shell-git and dms-shell-bin
if pkg == "dms-shell-bin" { // since we manually manage those dependencies
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
// Pre-install dependencies from .SRCINFO
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
IsComplete: false,
CommandInfo: "Installing package dependencies and makedepends",
}
// Install dependencies and makedepends explicitly
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
if [[ "%s" == *"quickshell"* ]]; then
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
fi
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi
`, srcinfoPath, pkg, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
}
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
if [ ! -z "$makedeps" ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
fi
`, srcinfoPath, sudoPassword))
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
}
} else {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress), Progress: startProgress + 0.35*(endProgress-startProgress),
@@ -652,66 +665,6 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
IsComplete: false, IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg), LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
} }
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
IsComplete: false,
CommandInfo: "Classifying dependencies as system or AUR",
}
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
if err != nil {
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
}
seen := make(map[string]bool)
var systemPkgs []string
var aurPkgs []string
for _, dep := range append(runtimeDeps, makeDeps...) {
if seen[dep] || a.packageInstalled(dep) {
continue
}
seen[dep] = true
if a.isInSystemRepo(dep) {
systemPkgs = append(systemPkgs, dep)
} else {
aurPkgs = append(aurPkgs, dep)
}
}
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.32*(endProgress-startProgress),
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
}
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
}
}
for _, aurDep := range aurPkgs {
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
}
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
startProgress+0.35*(endProgress-startProgress),
startProgress+0.39*(endProgress-startProgress),
visited,
); err != nil {
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
}
}
} }
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -724,7 +677,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm") buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
buildCmd.Dir = packageDir buildCmd.Dir = packageDir
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to build %s: %w", pkg, err) return fmt.Errorf("failed to build %s: %w", pkg, err)
+18 -69
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -91,27 +92,9 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
} }
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {
return debianPackageInstalledPrecisely(pkg) cmd := exec.Command("dpkg", "-l", pkg)
} err := cmd.Run()
return err == nil
func debianPackageInstalledPrecisely(pkg string) bool {
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
output, err := cmd.Output()
if err != nil {
return false
}
return strings.TrimSpace(string(output)) == "installed"
}
func debianRepoArchitecture(arch string) string {
switch arch {
case "amd64", "x86_64":
return "amd64"
case "arm64", "aarch64":
return "arm64"
default:
return arch
}
} }
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -211,12 +194,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: "Installing development dependencies...", Step: "Installing development dependencies...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev", CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
LogOutput: "Installing additional development tools", LogOutput: "Installing additional development tools",
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }
@@ -396,14 +379,6 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
return names return names
} }
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool) enabledRepos := make(map[string]bool)
@@ -461,7 +436,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
// Add repository // Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL) repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -507,46 +482,20 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
groups := orderedMinimalInstallGroups(packages) args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
totalGroups := len(groups) args = append(args, packages...)
groupIndex := 0 progressChan <- InstallProgressMsg{
installGroup := func(groupPackages []string, minimal bool) error { Phase: PhaseSystemPackages,
if len(groupPackages) == 0 { Progress: 0.40,
return nil Step: "Installing system packages...",
} IsComplete: false,
NeedsSudo: true,
groupIndex++ CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
startProgress := 0.40
endProgress := 0.60
if totalGroups > 1 {
if groupIndex == 1 {
endProgress = 0.50
} else {
startProgress = 0.50
}
}
args := d.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: startProgress,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
} }
for _, group := range groups { cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
if err := installGroup(group.packages, group.minimal); err != nil { return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
return err
}
}
return nil
} }
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
+42 -52
View File
@@ -484,7 +484,28 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60) args := []string{"dnf", "install", "-y"}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
} }
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -494,57 +515,26 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
}
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
args := []string{"dnf", "install", "-y"} args := []string{"dnf", "install", "-y"}
if minimal {
args = append(args, "--setopt=install_weak_deps=False") for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
} }
return append(args, packages...)
} args = append(args, packages...)
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { progressChan <- InstallProgressMsg{
groups := orderedMinimalInstallGroups(packages) Phase: PhaseAURPackages,
totalGroups := len(groups) Progress: 0.70,
Step: "Installing COPR packages...",
groupIndex := 0 IsComplete: false,
installGroup := func(groupPackages []string, minimal bool) error { NeedsSudo: true,
if len(groupPackages) == 0 { CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
return nil }
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
groupIndex++ return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := f.dnfInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
} }
-44
View File
@@ -1,44 +0,0 @@
package distros
type minimalInstallGroup struct {
packages []string
minimal bool
}
func shouldPreferMinimalInstall(pkg string) bool {
switch pkg {
case "niri", "niri-git":
return true
default:
return false
}
}
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
for _, pkg := range packages {
if shouldPreferMinimalInstall(pkg) {
minimal = append(minimal, pkg)
continue
}
normal = append(normal, pkg)
}
return normal, minimal
}
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
normal, minimal := splitMinimalInstallPackages(packages)
groups := make([]minimalInstallGroup, 0, 2)
if len(minimal) > 0 {
groups = append(groups, minimalInstallGroup{
packages: minimal,
minimal: true,
})
}
if len(normal) > 0 {
groups = append(groups, minimalInstallGroup{
packages: normal,
minimal: false,
})
}
return groups
}
+43 -164
View File
@@ -6,7 +6,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -30,8 +29,6 @@ type OpenSUSEDistribution struct {
config DistroConfig config DistroConfig
} }
const openSUSENiriWaylandServerPackage = "libwayland-server0"
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution { func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
base := NewBaseDistribution(logChan) base := NewBaseDistribution(logChan)
return &OpenSUSEDistribution{ return &OpenSUSEDistribution{
@@ -202,7 +199,35 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
} }
func (o *OpenSUSEDistribution) getPrerequisites() []string { func (o *OpenSUSEDistribution) getPrerequisites() []string {
return []string{} return []string{
"make",
"unzip",
"gcc",
"gcc-c++",
"cmake",
"ninja",
"pkgconf-pkg-config",
"git",
"qt6-base-devel",
"qt6-declarative-devel",
"qt6-declarative-private-devel",
"qt6-shadertools",
"qt6-shadertools-devel",
"qt6-wayland-devel",
"qt6-waylandclient-private-devel",
"spirv-tools-devel",
"cli11-devel",
"wayland-protocols-devel",
"libgbm-devel",
"libdrm-devel",
"pipewire-devel",
"jemalloc-devel",
"wayland-utils",
"Mesa-libGLESv3-devel",
"pam-devel",
"glib2-devel",
"polkit-devel",
}
} }
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -272,10 +297,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
LogOutput: "Starting prerequisite check...", LogOutput: "Starting prerequisite check...",
} }
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to disable install media repositories: %w", err)
}
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err) return fmt.Errorf("failed to install prerequisites: %w", err)
} }
@@ -306,7 +327,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
NeedsSudo: true, NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
} }
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil { if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install zypper packages: %w", err) return fmt.Errorf("failed to install zypper packages: %w", err)
} }
} }
@@ -321,7 +342,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
IsComplete: false, IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")), LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
} }
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil { if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err) return fmt.Errorf("failed to install OBS packages: %w", err)
} }
} }
@@ -411,32 +432,9 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
} }
} }
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
return systemPkgs, obsPkgs, manualPkgs, variantMap return systemPkgs, obsPkgs, manualPkgs, variantMap
} }
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
return nil
}
return []string{openSUSENiriWaylandServerPackage}
}
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
for _, pkg := range extraPkgs {
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
continue
}
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
systemPkgs = append(systemPkgs, pkg)
}
return systemPkgs
}
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string { func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages)) names := make([]string, len(packages))
for i, pkg := range packages { for i, pkg := range packages {
@@ -516,146 +514,27 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
return nil return nil
} }
func isOpenSUSEInstallMediaURI(uri string) bool { func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
return strings.HasPrefix(normalizedURI, "cd:/") ||
strings.HasPrefix(normalizedURI, "dvd:/") ||
strings.HasPrefix(normalizedURI, "hd:/") ||
strings.HasPrefix(normalizedURI, "iso:/")
}
func parseZypperInstallMediaAliases(output string) []string {
var aliases []string
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 7 {
continue
}
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
alias := parts[1]
enabled := strings.ToLower(parts[3])
uri := parts[len(parts)-1]
if alias == "" || strings.EqualFold(alias, "alias") {
continue
}
if enabled != "" && enabled != "yes" {
continue
}
if !isOpenSUSEInstallMediaURI(uri) {
continue
}
aliases = append(aliases, alias)
}
return aliases
}
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
output, err := listCmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
return fmt.Errorf("failed to list zypper repositories: %w", err)
}
aliases := parseZypperInstallMediaAliases(string(output))
if len(aliases) == 0 {
return nil
}
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.055,
Step: "Disabling install media repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
}
for _, alias := range aliases {
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
repoOutput, err := cmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
}
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
}
return nil
}
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
args := []string{"zypper", "install", "-y"}
if minimal {
args = append(args, "--no-recommends")
}
return append(args, packages...)
}
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
} }
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", "))) o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
groups := orderedMinimalInstallGroups(packages) args := []string{"zypper", "install", "-y"}
totalGroups := len(groups) args = append(args, packages...)
groupIndex := 0 progressChan <- InstallProgressMsg{
installGroup := func(groupPackages []string, minimal bool) error { Phase: PhaseSystemPackages,
if len(groupPackages) == 0 { Progress: 0.40,
return nil Step: "Installing system packages...",
} IsComplete: false,
NeedsSudo: true,
groupIndex++ CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := o.zypperInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
for _, group := range groups { cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
if err := installGroup(group.packages, group.minimal); err != nil { return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
return err
}
}
return nil
} }
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
+29 -51
View File
@@ -100,7 +100,9 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
} }
func (u *UbuntuDistribution) packageInstalled(pkg string) bool { func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
return debianPackageInstalledPrecisely(pkg) cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run()
return err == nil
} }
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -452,7 +454,21 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
} }
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
args := []string{"apt-get", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
} }
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -461,59 +477,21 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
} }
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", "))) u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
}
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string { args := []string{"apt-get", "install", "-y"}
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} args = append(args, packages...)
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { progressChan <- InstallProgressMsg{
groups := orderedMinimalInstallGroups(packages) Phase: PhaseAURPackages,
totalGroups := len(groups) Progress: 0.70,
Step: "Installing PPA packages...",
groupIndex := 0 IsComplete: false,
installGroup := func(groupPackages []string, minimal bool) error { NeedsSudo: true,
if len(groupPackages) == 0 { CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := u.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
for _, group := range groups { cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
if err := installGroup(group.packages, group.minimal); err != nil { return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
return err
}
}
return nil
} }
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
-42
View File
@@ -1,42 +0,0 @@
package geolocation
import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
func NewClient() Client {
geoclueClient, err := newGeoClueClient()
if err != nil {
log.Warnf("GeoClue2 unavailable: %v", err)
return newSeededIpClient()
}
loc, _ := geoclueClient.GetLocation()
if loc.Latitude != 0 || loc.Longitude != 0 {
log.Info("Using GeoClue2 location")
return geoclueClient
}
log.Info("GeoClue2 has no fix yet, seeding with IP location")
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("IP location seed failed: %v", err)
return geoclueClient
}
log.Info("Seeded GeoClue2 with IP location")
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
return geoclueClient
}
func newSeededIpClient() *IpClient {
client := newIpClient()
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("IP location also failed: %v", err)
return client
}
log.Info("Using IP location")
client.currLocation.Latitude = ipLoc.Latitude
client.currLocation.Longitude = ipLoc.Longitude
return client
}
-243
View File
@@ -1,243 +0,0 @@
package geolocation
import (
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
)
const (
dbusGeoClueService = "org.freedesktop.GeoClue2"
dbusGeoCluePath = "/org/freedesktop/GeoClue2"
dbusGeoClueInterface = dbusGeoClueService
dbusGeoClueManagerPath = dbusGeoCluePath + "/Manager"
dbusGeoClueManagerInterface = dbusGeoClueInterface + ".Manager"
dbusGeoClueManagerGetClient = dbusGeoClueManagerInterface + ".GetClient"
dbusGeoClueClientInterface = dbusGeoClueInterface + ".Client"
dbusGeoClueClientDesktopId = dbusGeoClueClientInterface + ".DesktopId"
dbusGeoClueClientTimeThreshold = dbusGeoClueClientInterface + ".TimeThreshold"
dbusGeoClueClientTimeStart = dbusGeoClueClientInterface + ".Start"
dbusGeoClueClientTimeStop = dbusGeoClueClientInterface + ".Stop"
dbusGeoClueClientLocationUpdated = dbusGeoClueClientInterface + ".LocationUpdated"
dbusGeoClueLocationInterface = dbusGeoClueInterface + ".Location"
dbusGeoClueLocationLatitude = dbusGeoClueLocationInterface + ".Latitude"
dbusGeoClueLocationLongitude = dbusGeoClueLocationInterface + ".Longitude"
)
type GeoClueClient struct {
currLocation *Location
locationMutex sync.RWMutex
dbusConn *dbus.Conn
clientPath dbus.ObjectPath
signals chan *dbus.Signal
stopChan chan struct{}
sigWG sync.WaitGroup
subscribers syncmap.Map[string, chan Location]
}
func newGeoClueClient() (*GeoClueClient, error) {
dbusConn, err := dbus.ConnectSystemBus()
if err != nil {
return nil, fmt.Errorf("system bus connection failed: %w", err)
}
c := &GeoClueClient{
dbusConn: dbusConn,
stopChan: make(chan struct{}),
signals: make(chan *dbus.Signal, 256),
currLocation: &Location{
Latitude: 0.0,
Longitude: 0.0,
},
}
if err := c.setupClient(); err != nil {
dbusConn.Close()
return nil, err
}
if err := c.startSignalPump(); err != nil {
return nil, err
}
return c, nil
}
func (c *GeoClueClient) Close() {
close(c.stopChan)
c.sigWG.Wait()
if c.signals != nil {
c.dbusConn.RemoveSignal(c.signals)
close(c.signals)
}
c.subscribers.Range(func(key string, ch chan Location) bool {
close(ch)
c.subscribers.Delete(key)
return true
})
if c.dbusConn != nil {
c.dbusConn.Close()
}
}
func (c *GeoClueClient) Subscribe(id string) chan Location {
ch := make(chan Location, 64)
c.subscribers.Store(id, ch)
return ch
}
func (c *GeoClueClient) Unsubscribe(id string) {
if ch, ok := c.subscribers.LoadAndDelete(id); ok {
close(ch)
}
}
func (c *GeoClueClient) setupClient() error {
managerObj := c.dbusConn.Object(dbusGeoClueService, dbusGeoClueManagerPath)
if err := managerObj.Call(dbusGeoClueManagerGetClient, 0).Store(&c.clientPath); err != nil {
return fmt.Errorf("failed to create GeoClue2 client: %w", err)
}
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
if err := clientObj.SetProperty(dbusGeoClueClientDesktopId, "dms"); err != nil {
return fmt.Errorf("failed to set desktop ID: %w", err)
}
if err := clientObj.SetProperty(dbusGeoClueClientTimeThreshold, uint(10)); err != nil {
return fmt.Errorf("failed to set time threshold: %w", err)
}
return nil
}
func (c *GeoClueClient) startSignalPump() error {
c.dbusConn.Signal(c.signals)
if err := c.dbusConn.AddMatchSignal(
dbus.WithMatchObjectPath(c.clientPath),
dbus.WithMatchInterface(dbusGeoClueClientInterface),
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
); err != nil {
return err
}
c.sigWG.Add(1)
go func() {
defer c.sigWG.Done()
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
clientObj.Call(dbusGeoClueClientTimeStart, 0)
defer clientObj.Call(dbusGeoClueClientTimeStop, 0)
for {
select {
case <-c.stopChan:
return
case sig, ok := <-c.signals:
if !ok {
return
}
if sig == nil {
continue
}
c.handleSignal(sig)
}
}
}()
return nil
}
func (c *GeoClueClient) handleSignal(sig *dbus.Signal) {
switch sig.Name {
case dbusGeoClueClientLocationUpdated:
if len(sig.Body) != 2 {
return
}
newLocationPath, ok := sig.Body[1].(dbus.ObjectPath)
if !ok {
return
}
if err := c.handleLocationUpdated(newLocationPath); err != nil {
log.Warn("GeoClue: Failed to handle location update: %v", err)
return
}
}
}
func (c *GeoClueClient) handleLocationUpdated(path dbus.ObjectPath) error {
locationObj := c.dbusConn.Object(dbusGeoClueService, path)
lat, err := locationObj.GetProperty(dbusGeoClueLocationLatitude)
if err != nil {
return err
}
long, err := locationObj.GetProperty(dbusGeoClueLocationLongitude)
if err != nil {
return err
}
c.locationMutex.Lock()
c.currLocation.Latitude = dbusutil.AsOr(lat, 0.0)
c.currLocation.Longitude = dbusutil.AsOr(long, 0.0)
c.locationMutex.Unlock()
c.notifySubscribers()
return nil
}
func (c *GeoClueClient) notifySubscribers() {
currentLocation, err := c.GetLocation()
if err != nil {
return
}
c.subscribers.Range(func(key string, ch chan Location) bool {
select {
case ch <- currentLocation:
default:
log.Warn("GeoClue: subscriber channel full, dropping update")
}
return true
})
}
func (c *GeoClueClient) SeedLocation(loc Location) {
c.locationMutex.Lock()
defer c.locationMutex.Unlock()
c.currLocation.Latitude = loc.Latitude
c.currLocation.Longitude = loc.Longitude
}
func (c *GeoClueClient) GetLocation() (Location, error) {
c.locationMutex.RLock()
defer c.locationMutex.RUnlock()
if c.currLocation == nil {
return Location{
Latitude: 0.0,
Longitude: 0.0,
}, nil
}
stateCopy := *c.currLocation
return stateCopy, nil
}
-91
View File
@@ -1,91 +0,0 @@
package geolocation
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type IpClient struct {
currLocation *Location
}
type ipLocationResult struct {
Location
City string
}
type ipAPIResponse struct {
Status string `json:"status"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
City string `json:"city"`
}
func newIpClient() *IpClient {
return &IpClient{
currLocation: &Location{},
}
}
func (c *IpClient) Subscribe(id string) chan Location {
ch := make(chan Location, 1)
if location, err := c.GetLocation(); err == nil {
ch <- location
}
return ch
}
func (c *IpClient) Unsubscribe(id string) {}
func (c *IpClient) Close() {}
func (c *IpClient) GetLocation() (Location, error) {
if c.currLocation.Latitude != 0 || c.currLocation.Longitude != 0 {
return *c.currLocation, nil
}
result, err := fetchIPLocation()
if err != nil {
return Location{}, err
}
c.currLocation.Latitude = result.Latitude
c.currLocation.Longitude = result.Longitude
return *c.currLocation, nil
}
func fetchIPLocation() (ipLocationResult, error) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("http://ip-api.com/json/")
if err != nil {
return ipLocationResult{}, fmt.Errorf("failed to fetch IP location: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ipLocationResult{}, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return ipLocationResult{}, fmt.Errorf("failed to read response: %w", err)
}
var data ipAPIResponse
if err := json.Unmarshal(body, &data); err != nil {
return ipLocationResult{}, fmt.Errorf("failed to parse response: %w", err)
}
if data.Status == "fail" || (data.Lat == 0 && data.Lon == 0) {
return ipLocationResult{}, fmt.Errorf("ip-api.com returned no location data")
}
return ipLocationResult{
Location: Location{Latitude: data.Lat, Longitude: data.Lon},
City: data.City,
}, nil
}
-15
View File
@@ -1,15 +0,0 @@
package geolocation
type Location struct {
Latitude float64
Longitude float64
}
type Client interface {
GetLocation() (Location, error)
Subscribe(id string) chan Location
Unsubscribe(id string)
Close()
}
@@ -1,91 +0,0 @@
# AppArmor profile for dms-greeter
#
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
# Manual edits will be overwritten on next sync.
#
# Mode: complain (denials are logged, nothing is blocked)
# To switch to enforce after validating with `aa-logprof`:
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
#
#include <tunables/global>
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# The launcher script itself
/usr/bin/dms-greeter r,
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
/var/cache/dms-greeter/ rw,
/var/cache/dms-greeter/** rwlk,
# DMS config — packaged path
/usr/share/quickshell/dms-greeter/ r,
/usr/share/quickshell/dms-greeter/** r,
/usr/share/quickshell/ r,
/usr/share/quickshell/** r,
# DMS config — system and user overrides
/etc/dms/ r,
/etc/dms/** r,
/usr/share/dms/ r,
/usr/share/dms/** r,
/home/*/.config/quickshell/ r,
/home/*/.config/quickshell/** r,
/root/.config/quickshell/ r,
/root/.config/quickshell/** r,
# greetd / PAM — read-only for session setup
/etc/greetd/ r,
/etc/greetd/** r,
/etc/pam.d/ r,
/etc/pam.d/** r,
/usr/lib/pam.d/ r,
/usr/lib/pam.d/** r,
# Compositor binaries — run unconfined so each compositor uses its own profile
/usr/bin/niri Ux,
/usr/bin/hyprland Ux,
/usr/bin/Hyprland Ux,
/usr/bin/sway Ux,
/usr/bin/labwc Ux,
/usr/bin/scroll Ux,
/usr/bin/miracle-wm Ux,
/usr/bin/mango Ux,
# Quickshell — run unconfined (has its own compositor profile on some distros)
/usr/bin/qs Ux,
/usr/bin/quickshell Ux,
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
/run/user/[0-9]*/ rw,
/run/user/[0-9]*/** rw,
# DRM / GPU devices (required for Wayland compositor startup)
/dev/dri/ r,
/dev/dri/* rw,
/dev/udmabuf rw,
# Input devices
/dev/input/ r,
/dev/input/* r,
# Systemd journal / logging
/run/systemd/journal/socket rw,
/dev/log rw,
# Shell helper binaries invoked by the launcher script
/usr/bin/env ix,
/usr/bin/mkdir ix,
/usr/bin/cat ix,
/usr/bin/grep ix,
/usr/bin/dirname ix,
/usr/bin/basename ix,
/usr/bin/command ix,
/bin/env ix,
/bin/mkdir ix,
# Signal management (compositor lifecycle)
signal (send, receive) set=("term", "int", "hup", "kill"),
}
File diff suppressed because it is too large Load Diff
-98
View File
@@ -1,98 +0,0 @@
package greeter
import (
"os"
"path/filepath"
"testing"
)
func writeTestJSON(t *testing.T, path string, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("failed to create parent dir for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("failed to write %s: %v", path, err)
}
}
func TestResolveGreeterThemeSyncState(t *testing.T) {
t.Parallel()
tests := []struct {
name string
settingsJSON string
sessionJSON string
wantSourcePath string
wantResolvedWallpaper string
wantDynamicOverrideUsed bool
}{
{
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": "Pictures/blue.jpg",
"matugenScheme": "scheme-tonal-spot",
"iconTheme": "Papirus"
}`,
sessionJSON: `{"isLightMode":true}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
wantDynamicOverrideUsed: true,
},
{
name: "dynamic theme without override uses desktop colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": ""
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "",
wantDynamicOverrideUsed: false,
},
{
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
settingsJSON: `{
"currentThemeName": "purple",
"greeterWallpaperPath": "/tmp/blue.jpg"
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "/tmp/blue.jpg",
wantDynamicOverrideUsed: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
}
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
}
wantResolvedWallpaper := tt.wantResolvedWallpaper
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
}
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
}
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
}
})
}
}
+2 -23
View File
@@ -71,7 +71,6 @@ var templateRegistry = []TemplateDef{
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true}, {ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode}, {ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs}, {ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
} }
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
@@ -99,9 +98,7 @@ type Options struct {
Mode ColorMode Mode ColorMode
IconTheme string IconTheme string
MatugenType string MatugenType string
Contrast float64
RunUserTemplates bool RunUserTemplates bool
ColorsOnly bool
StockColors string StockColors string
SyncModeWithPortal bool SyncModeWithPortal bool
TerminalsAlwaysDark bool TerminalsAlwaysDark bool
@@ -229,7 +226,6 @@ func buildOnce(opts *Options) (bool, error) {
log.Info("Running matugen color hex with stock color overrides") log.Info("Running matugen color hex with stock color overrides")
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()} args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
args = appendContrastArg(args, opts.Contrast)
args = append(args, importArgs...) args = append(args, importArgs...)
if err := runMatugen(args); err != nil { if err := runMatugen(args); err != nil {
return false, err return false, err
@@ -266,7 +262,6 @@ func buildOnce(opts *Options) (bool, error) {
args = []string{opts.Kind, opts.Value} args = []string{opts.Kind, opts.Value}
} }
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()) args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
args = appendContrastArg(args, opts.Contrast)
args = append(args, importArgs...) args = append(args, importArgs...)
if err := runMatugen(args); err != nil { if err := runMatugen(args); err != nil {
return false, err return false, err
@@ -278,10 +273,6 @@ func buildOnce(opts *Options) (bool, error) {
return false, nil return false, nil
} }
if opts.ColorsOnly {
return true, nil
}
if isDMSGTKActive(opts.ConfigDir) { if isDMSGTKActive(opts.ConfigDir) {
switch opts.Mode { switch opts.Mode {
case ColorModeLight: case ColorModeLight:
@@ -302,13 +293,6 @@ func buildOnce(opts *Options) (bool, error) {
return true, nil return true, nil
} }
func appendContrastArg(args []string, contrast float64) []string {
if contrast == 0 {
return args
}
return append(args, "--contrast", strconv.FormatFloat(contrast, 'f', -1, 64))
}
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error { func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml") userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
@@ -346,10 +330,6 @@ output_path = '%s'
`, opts.ShellDir, opts.ColorsOutput()) `, opts.ShellDir, opts.ColorsOutput())
if opts.ColorsOnly {
return nil
}
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
for _, tmpl := range templateRegistry { for _, tmpl := range templateRegistry {
if opts.ShouldSkipTemplate(tmpl.ID) { if opts.ShouldSkipTemplate(tmpl.ID) {
@@ -616,10 +596,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
matugenVersionOK = true matugenVersionOK = true
if matugenSupportsCOE { if matugenSupportsCOE {
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr) log.Infof("Matugen %s supports --continue-on-error", versionStr)
} }
if matugenIsV4 { if matugenIsV4 {
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr) log.Infof("Matugen %s: using v4 flags", versionStr)
} }
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
} }
@@ -697,7 +677,6 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
baseArgs = []string{opts.Kind, opts.Value} baseArgs = []string{opts.Kind, opts.Value}
} }
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run") baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
if flags.isV4 { if flags.isV4 {
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output") baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
} }
-49
View File
@@ -3,7 +3,6 @@ package matugen
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils" mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
@@ -393,51 +392,3 @@ func TestSubstituteVars(t *testing.T) {
}) })
} }
} }
func TestBuildMergedConfigColorsOnly(t *testing.T) {
tempDir := t.TempDir()
shellDir := filepath.Join(tempDir, "shell")
configsDir := filepath.Join(shellDir, "matugen", "configs")
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
baseConfig := "[config]\ncustom_keywords = []\n"
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
t.Fatalf("failed to write base config: %v", err)
}
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
if err != nil {
t.Fatalf("failed to create temp config: %v", err)
}
defer os.Remove(cfgFile.Name())
defer cfgFile.Close()
opts := &Options{
ShellDir: shellDir,
ConfigDir: filepath.Join(tempDir, "config"),
StateDir: filepath.Join(tempDir, "state"),
ColorsOnly: true,
}
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
t.Fatalf("buildMergedConfig failed: %v", err)
}
if err := cfgFile.Close(); err != nil {
t.Fatalf("failed to close merged config: %v", err)
}
output, err := os.ReadFile(cfgFile.Name())
if err != nil {
t.Fatalf("failed to read merged config: %v", err)
}
content := string(output)
assert.Contains(t, content, "[templates.dank]")
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
assert.NotContains(t, content, "[templates.gtk]")
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
}
@@ -1,203 +0,0 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks_geolocation
import (
geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
mock "github.com/stretchr/testify/mock"
)
// MockClient is an autogenerated mock type for the Client type
type MockClient struct {
mock.Mock
}
type MockClient_Expecter struct {
mock *mock.Mock
}
func (_m *MockClient) EXPECT() *MockClient_Expecter {
return &MockClient_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockClient) Close() {
_m.Called()
}
// MockClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockClient_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockClient_Expecter) Close() *MockClient_Close_Call {
return &MockClient_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockClient_Close_Call) Run(run func()) *MockClient_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockClient_Close_Call) Return() *MockClient_Close_Call {
_c.Call.Return()
return _c
}
func (_c *MockClient_Close_Call) RunAndReturn(run func()) *MockClient_Close_Call {
_c.Run(run)
return _c
}
// GetLocation provides a mock function with no fields
func (_m *MockClient) GetLocation() (geolocation.Location, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetLocation")
}
var r0 geolocation.Location
var r1 error
if rf, ok := ret.Get(0).(func() (geolocation.Location, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() geolocation.Location); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(geolocation.Location)
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockClient_GetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLocation'
type MockClient_GetLocation_Call struct {
*mock.Call
}
// GetLocation is a helper method to define mock.On call
func (_e *MockClient_Expecter) GetLocation() *MockClient_GetLocation_Call {
return &MockClient_GetLocation_Call{Call: _e.mock.On("GetLocation")}
}
func (_c *MockClient_GetLocation_Call) Run(run func()) *MockClient_GetLocation_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockClient_GetLocation_Call) Return(_a0 geolocation.Location, _a1 error) *MockClient_GetLocation_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClient_GetLocation_Call) RunAndReturn(run func() (geolocation.Location, error)) *MockClient_GetLocation_Call {
_c.Call.Return(run)
return _c
}
// Subscribe provides a mock function with given fields: id
func (_m *MockClient) Subscribe(id string) chan geolocation.Location {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for Subscribe")
}
var r0 chan geolocation.Location
if rf, ok := ret.Get(0).(func(string) chan geolocation.Location); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(chan geolocation.Location)
}
}
return r0
}
// MockClient_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
type MockClient_Subscribe_Call struct {
*mock.Call
}
// Subscribe is a helper method to define mock.On call
// - id string
func (_e *MockClient_Expecter) Subscribe(id interface{}) *MockClient_Subscribe_Call {
return &MockClient_Subscribe_Call{Call: _e.mock.On("Subscribe", id)}
}
func (_c *MockClient_Subscribe_Call) Run(run func(id string)) *MockClient_Subscribe_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockClient_Subscribe_Call) Return(_a0 chan geolocation.Location) *MockClient_Subscribe_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClient_Subscribe_Call) RunAndReturn(run func(string) chan geolocation.Location) *MockClient_Subscribe_Call {
_c.Call.Return(run)
return _c
}
// Unsubscribe provides a mock function with given fields: id
func (_m *MockClient) Unsubscribe(id string) {
_m.Called(id)
}
// MockClient_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
type MockClient_Unsubscribe_Call struct {
*mock.Call
}
// Unsubscribe is a helper method to define mock.On call
// - id string
func (_e *MockClient_Expecter) Unsubscribe(id interface{}) *MockClient_Unsubscribe_Call {
return &MockClient_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", id)}
}
func (_c *MockClient_Unsubscribe_Call) Run(run func(id string)) *MockClient_Unsubscribe_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockClient_Unsubscribe_Call) Return() *MockClient_Unsubscribe_Call {
_c.Call.Return()
return _c
}
func (_c *MockClient_Unsubscribe_Call) RunAndReturn(run func(string)) *MockClient_Unsubscribe_Call {
_c.Run(run)
return _c
}
// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockClient(t interface {
mock.TestingT
Cleanup(func())
}) *MockClient {
mock := &MockClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
@@ -1062,62 +1062,6 @@ func (_c *MockBackend_GetWiFiNetworkDetails_Call) RunAndReturn(run func(string)
return _c return _c
} }
// GetWiFiQRCodeContent provides a mock function with given fields: ssid
func (_m *MockBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
ret := _m.Called(ssid)
if len(ret) == 0 {
panic("no return value specified for GetWiFiQRCodeContent")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
return rf(ssid)
}
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(ssid)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(ssid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockBackend_GetWiFiQRCodeContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWiFiQRCodeContent'
type MockBackend_GetWiFiQRCodeContent_Call struct {
*mock.Call
}
// GetWiFiQRCodeContent is a helper method to define mock.On call
// - ssid string
func (_e *MockBackend_Expecter) GetWiFiQRCodeContent(ssid interface{}) *MockBackend_GetWiFiQRCodeContent_Call {
return &MockBackend_GetWiFiQRCodeContent_Call{Call: _e.mock.On("GetWiFiQRCodeContent", ssid)}
}
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Run(run func(ssid string)) *MockBackend_GetWiFiQRCodeContent_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Return(_a0 string, _a1 error) *MockBackend_GetWiFiQRCodeContent_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockBackend_GetWiFiQRCodeContent_Call) RunAndReturn(run func(string) (string, error)) *MockBackend_GetWiFiQRCodeContent_Call {
_c.Call.Return(run)
return _c
}
// GetWiredConnections provides a mock function with no fields // GetWiredConnections provides a mock function with no fields
func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) { func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) {
ret := _m.Called() ret := _m.Called()
+1 -6
View File
@@ -6,7 +6,6 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
@@ -60,11 +59,7 @@ func Send(n Notification) error {
hints := map[string]dbus.Variant{} hints := map[string]dbus.Variant{}
if n.FilePath != "" { if n.FilePath != "" {
imgPath := n.FilePath hints["image_path"] = dbus.MakeVariant(n.FilePath)
if !strings.HasPrefix(imgPath, "file://") {
imgPath = "file://" + imgPath
}
hints["image_path"] = dbus.MakeVariant(imgPath)
} }
obj := conn.Object(notifyDest, notifyPath) obj := conn.Object(notifyDest, notifyPath)
+1 -5
View File
@@ -113,11 +113,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
} }
func (r *RegionSelector) Run() (*CaptureResult, bool, error) { func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
if r.screenshoter != nil && r.screenshoter.config.Reset { r.preSelect = GetLastRegion()
r.preSelect = Region{}
} else {
r.preSelect = GetLastRegion()
}
if err := r.connect(); err != nil { if err := r.connect(); err != nil {
return nil, false, fmt.Errorf("wayland connect: %w", err) return nil, false, fmt.Errorf("wayland connect: %w", err)
-3
View File
@@ -114,9 +114,6 @@ func (r *RegionSelector) setupPointerHandlers() {
for _, os := range r.surfaces { for _, os := range r.surfaces {
r.redrawSurface(os) r.redrawSurface(os)
} }
if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection {
r.finishSelection()
}
} }
default: default:
r.cancelled = true r.cancelled = true
+1 -5
View File
@@ -138,13 +138,9 @@ func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uin
if !r.showCapturedCursor { if !r.showCapturedCursor {
cursorLabel = "show" cursorLabel = "show"
} }
captureKey := "Space/Enter"
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
captureKey = "Drag+Release"
}
items := []struct{ key, desc string }{ items := []struct{ key, desc string }{
{captureKey, "capture"}, {"Space/Enter", "capture"},
{"P", cursorLabel + " cursor"}, {"P", cursorLabel + " cursor"},
{"Esc", "cancel"}, {"Esc", "cancel"},
} }
-6
View File
@@ -106,12 +106,6 @@ func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
} }
func (s *Screenshoter) captureRegion() (*CaptureResult, error) { func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
if s.config.Reset {
if err := SaveLastRegion(Region{}); err != nil {
log.Debug("failed to reset last region", "err", err)
}
}
selector := NewRegionSelector(s) selector := NewRegionSelector(s)
result, cancelled, err := selector.Run() result, cancelled, err := selector.Run()
if err != nil { if err != nil {
-4
View File
@@ -52,8 +52,6 @@ type Config struct {
Mode Mode Mode Mode
OutputName string OutputName string
Cursor CursorMode Cursor CursorMode
NoConfirm bool
Reset bool
Format Format Format Format
Quality int Quality int
OutputDir string OutputDir string
@@ -68,8 +66,6 @@ func DefaultConfig() Config {
return Config{ return Config{
Mode: ModeRegion, Mode: ModeRegion,
Cursor: CursorOff, Cursor: CursorOff,
NoConfirm: false,
Reset: false,
Format: FormatPNG, Format: FormatPNG,
Quality: 90, Quality: 90,
OutputDir: "", OutputDir: "",
+10 -50
View File
@@ -6,20 +6,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/pilebones/go-udev/netlink" "github.com/pilebones/go-udev/netlink"
) )
const (
udevRecvBufSize = 8 * 1024 * 1024
udevMaxRetries = 5
udevBaseDelay = 2 * time.Second
udevMaxDelay = 60 * time.Second
)
type UdevMonitor struct { type UdevMonitor struct {
stop chan struct{} stop chan struct{}
rescanMutex sync.Mutex rescanMutex sync.Mutex
@@ -37,6 +29,13 @@ func NewUdevMonitor(manager *Manager) *UdevMonitor {
} }
func (m *UdevMonitor) run(manager *Manager) { func (m *UdevMonitor) run(manager *Manager) {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
log.Errorf("Failed to connect to udev netlink: %v", err)
return
}
defer conn.Close()
matcher := &netlink.RuleDefinitions{ matcher := &netlink.RuleDefinitions{
Rules: []netlink.RuleDefinition{ Rules: []netlink.RuleDefinition{
{Env: map[string]string{"SUBSYSTEM": "backlight"}}, {Env: map[string]string{"SUBSYSTEM": "backlight"}},
@@ -49,46 +48,6 @@ func (m *UdevMonitor) run(manager *Manager) {
return return
} }
failures := 0
for {
if err := m.monitorLoop(manager, matcher); err != nil {
log.Errorf("Udev monitor error: %v", err)
}
select {
case <-m.stop:
return
default:
}
failures++
if failures > udevMaxRetries {
log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries)
return
}
delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay)
log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries)
select {
case <-m.stop:
return
case <-time.After(delay):
}
}
}
func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
return err
}
defer conn.Close()
if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil {
log.Warnf("Failed to set udev socket receive buffer: %v", err)
}
events := make(chan netlink.UEvent) events := make(chan netlink.UEvent)
errs := make(chan error) errs := make(chan error)
conn.Monitor(events, errs, matcher) conn.Monitor(events, errs, matcher)
@@ -98,9 +57,10 @@ func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinit
for { for {
select { select {
case <-m.stop: case <-m.stop:
return nil return
case err := <-errs: case err := <-errs:
return err log.Errorf("Udev monitor error: %v", err)
return
case event := <-events: case event := <-events:
m.handleEvent(manager, event) m.handleEvent(manager, event)
} }
+1 -38
View File
@@ -2,10 +2,8 @@ package cups
import ( import (
"errors" "errors"
"fmt"
"net" "net"
"net/url" "net/url"
"os/exec"
"strings" "strings"
"time" "time"
@@ -277,42 +275,13 @@ func (m *Manager) GetClasses() ([]PrinterClass, error) {
return classes, nil return classes, nil
} }
func createPrinterViaLpadmin(name, deviceURI, ppd, information, location string) error {
args := []string{"-p", name, "-E", "-v", deviceURI, "-m", ppd}
if information != "" {
args = append(args, "-D", information)
}
if location != "" {
args = append(args, "-L", location)
}
out, err := exec.Command("lpadmin", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
func deletePrinterViaLpadmin(name string) error {
out, err := exec.Command("lpadmin", "-x", name).CombinedOutput()
if err != nil {
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error { func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error {
usedPkHelper := false usedPkHelper := false
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location) err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
if isAuthError(err) && m.pkHelper != nil { if isAuthError(err) && m.pkHelper != nil {
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil { if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil {
// pkHelper failed (e.g., no polkit agent), try lpadmin as last resort. return err
// lpadmin -E enables the printer, so no further setup needed.
if lpadminErr := createPrinterViaLpadmin(name, deviceURI, ppd, information, location); lpadminErr != nil {
return err
}
m.RefreshState()
return nil
} }
usedPkHelper = true usedPkHelper = true
} else if err != nil { } else if err != nil {
@@ -339,12 +308,6 @@ func (m *Manager) DeletePrinter(printerName string) error {
err := m.client.DeletePrinter(printerName) err := m.client.DeletePrinter(printerName)
if isAuthError(err) && m.pkHelper != nil { if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterDelete(printerName) err = m.pkHelper.PrinterDelete(printerName)
if err != nil {
// pkHelper failed, try lpadmin as last resort
if lpadminErr := deletePrinterViaLpadmin(printerName); lpadminErr == nil {
err = nil
}
}
} }
if err == nil { if err == nil {
m.RefreshState() m.RefreshState()
-21
View File
@@ -70,8 +70,6 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
handleRestartJob(conn, req, manager) handleRestartJob(conn, req, manager)
case "cups.holdJob": case "cups.holdJob":
handleHoldJob(conn, req, manager) handleHoldJob(conn, req, manager)
case "cups.testConnection":
handleTestConnection(conn, req, manager)
default: default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
} }
@@ -466,22 +464,3 @@ func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
} }
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
} }
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
host, err := params.StringNonEmpty(req.Params, "host")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
port := params.IntOpt(req.Params, "port", 631)
protocol := params.StringOpt(req.Params, "protocol", "ipp")
result, err := manager.TestRemotePrinter(host, port, protocol)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, result)
}
@@ -1,176 +0,0 @@
package cups
import (
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
)
var validProtocols = map[string]bool{
"ipp": true,
"ipps": true,
"lpd": true,
"socket": true,
}
func validateTestConnectionParams(host string, port int, protocol string) error {
if host == "" {
return errors.New("host is required")
}
if strings.ContainsAny(host, " \t\n\r/\\") {
return errors.New("host contains invalid characters")
}
if port < 1 || port > 65535 {
return errors.New("port must be between 1 and 65535")
}
if protocol != "" && !validProtocols[protocol] {
return errors.New("protocol must be one of: ipp, ipps, lpd, socket")
}
return nil
}
const probeTimeout = 10 * time.Second
func probeRemotePrinter(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
// Fast fail: TCP reachability check
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
if err != nil {
return &RemotePrinterInfo{
Reachable: false,
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
}, nil
}
conn.Close()
// Create a temporary IPP client pointing at the remote host.
// The TCP dial above provides fast-fail for unreachable hosts.
// The IPP adapter's ResponseHeaderTimeout (90s) bounds stalling servers.
client := ipp.NewIPPClient(host, port, "", "", useTLS)
// Try /ipp/print first (modern driverless printers), then / (legacy)
info, err := probeIPPEndpoint(client, host, port, useTLS, "/ipp/print")
if err != nil {
// If we got an auth error, the printer exists but requires credentials.
// Report it as reachable with the URI that triggered the auth challenge.
if isAuthError(err) {
proto := "ipp"
if useTLS {
proto = "ipps"
}
return &RemotePrinterInfo{
Reachable: true,
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
Info: "authentication required",
}, nil
}
info, err = probeIPPEndpoint(client, host, port, useTLS, "/")
}
if err != nil {
if isAuthError(err) {
proto := "ipp"
if useTLS {
proto = "ipps"
}
return &RemotePrinterInfo{
Reachable: true,
URI: fmt.Sprintf("%s://%s:%d/", proto, host, port),
Info: "authentication required",
}, nil
}
// TCP reachable but not an IPP printer
return &RemotePrinterInfo{
Reachable: true,
Error: fmt.Sprintf("host is reachable but does not appear to be an IPP printer: %s", err.Error()),
}, nil
}
return info, nil
}
func probeIPPEndpoint(client *ipp.IPPClient, host string, port int, useTLS bool, resourcePath string) (*RemotePrinterInfo, error) {
proto := "ipp"
if useTLS {
proto = "ipps"
}
printerURI := fmt.Sprintf("%s://%s:%d%s", proto, host, port, resourcePath)
httpProto := "http"
if useTLS {
httpProto = "https"
}
httpURL := fmt.Sprintf("%s://%s:%d%s", httpProto, host, port, resourcePath)
req := ipp.NewRequest(ipp.OperationGetPrinterAttributes, 1)
req.OperationAttributes[ipp.AttributePrinterURI] = printerURI
req.OperationAttributes[ipp.AttributeRequestedAttributes] = []string{
ipp.AttributePrinterName,
ipp.AttributePrinterMakeAndModel,
ipp.AttributePrinterState,
ipp.AttributePrinterInfo,
ipp.AttributePrinterUriSupported,
}
resp, err := client.SendRequest(httpURL, req, nil)
if err != nil {
return nil, err
}
if len(resp.PrinterAttributes) == 0 {
return nil, errors.New("no printer attributes returned")
}
attrs := resp.PrinterAttributes[0]
return &RemotePrinterInfo{
Reachable: true,
MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel),
Name: getStringAttr(attrs, ipp.AttributePrinterName),
Info: getStringAttr(attrs, ipp.AttributePrinterInfo),
State: parsePrinterState(attrs),
URI: printerURI,
}, nil
}
// TestRemotePrinter validates inputs and probes a remote printer via IPP.
// For lpd/socket protocols, only TCP reachability is tested.
func (m *Manager) TestRemotePrinter(host string, port int, protocol string) (*RemotePrinterInfo, error) {
if protocol == "" {
protocol = "ipp"
}
if err := validateTestConnectionParams(host, port, protocol); err != nil {
return nil, err
}
// For non-IPP protocols, only check TCP reachability
if protocol == "lpd" || protocol == "socket" {
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
if err != nil {
return &RemotePrinterInfo{
Reachable: false,
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
}, nil
}
conn.Close()
return &RemotePrinterInfo{
Reachable: true,
URI: fmt.Sprintf("%s://%s:%d", protocol, host, port),
}, nil
}
useTLS := protocol == "ipps"
probeFn := m.probeRemoteFn
if probeFn == nil {
probeFn = probeRemotePrinter
}
return probeFn(host, port, useTLS)
}
@@ -1,397 +0,0 @@
package cups
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/stretchr/testify/assert"
)
func TestValidateTestConnectionParams(t *testing.T) {
tests := []struct {
name string
host string
port int
protocol string
wantErr string
}{
{
name: "valid ipp",
host: "192.168.0.5",
port: 631,
protocol: "ipp",
wantErr: "",
},
{
name: "valid ipps",
host: "printer.local",
port: 443,
protocol: "ipps",
wantErr: "",
},
{
name: "valid lpd",
host: "10.0.0.1",
port: 515,
protocol: "lpd",
wantErr: "",
},
{
name: "valid socket",
host: "10.0.0.1",
port: 9100,
protocol: "socket",
wantErr: "",
},
{
name: "empty host",
host: "",
port: 631,
protocol: "ipp",
wantErr: "host is required",
},
{
name: "port too low",
host: "192.168.0.5",
port: 0,
protocol: "ipp",
wantErr: "port must be between 1 and 65535",
},
{
name: "port too high",
host: "192.168.0.5",
port: 70000,
protocol: "ipp",
wantErr: "port must be between 1 and 65535",
},
{
name: "invalid protocol",
host: "192.168.0.5",
port: 631,
protocol: "ftp",
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
},
{
name: "empty protocol treated as ipp",
host: "192.168.0.5",
port: 631,
protocol: "",
wantErr: "",
},
{
name: "host with slash",
host: "192.168.0.5/admin",
port: 631,
protocol: "ipp",
wantErr: "host contains invalid characters",
},
{
name: "host with space",
host: "192.168.0.5 ",
port: 631,
protocol: "ipp",
wantErr: "host contains invalid characters",
},
{
name: "host with newline",
host: "192.168.0.5\n",
port: 631,
protocol: "ipp",
wantErr: "host contains invalid characters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateTestConnectionParams(tt.host, tt.port, tt.protocol)
if tt.wantErr == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantErr)
}
})
}
}
func TestManager_TestRemotePrinter_Validation(t *testing.T) {
m := NewTestManager(nil, nil)
tests := []struct {
name string
host string
port int
protocol string
wantErr string
}{
{
name: "empty host returns error",
host: "",
port: 631,
protocol: "ipp",
wantErr: "host is required",
},
{
name: "invalid port returns error",
host: "192.168.0.5",
port: 0,
protocol: "ipp",
wantErr: "port must be between 1 and 65535",
},
{
name: "invalid protocol returns error",
host: "192.168.0.5",
port: 631,
protocol: "ftp",
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := m.TestRemotePrinter(tt.host, tt.port, tt.protocol)
assert.EqualError(t, err, tt.wantErr)
})
}
}
func TestManager_TestRemotePrinter_IPP(t *testing.T) {
tests := []struct {
name string
protocol string
probeRet *RemotePrinterInfo
probeErr error
wantTLS bool
wantReach bool
wantModel string
}{
{
name: "successful ipp probe",
protocol: "ipp",
probeRet: &RemotePrinterInfo{
Reachable: true,
MakeModel: "HP OfficeJet 8010",
Name: "OfficeJet",
State: "idle",
URI: "ipp://192.168.0.5:631/ipp/print",
},
wantTLS: false,
wantReach: true,
wantModel: "HP OfficeJet 8010",
},
{
name: "successful ipps probe",
protocol: "ipps",
probeRet: &RemotePrinterInfo{
Reachable: true,
MakeModel: "HP OfficeJet 8010",
URI: "ipps://192.168.0.5:631/ipp/print",
},
wantTLS: true,
wantReach: true,
wantModel: "HP OfficeJet 8010",
},
{
name: "unreachable host",
protocol: "ipp",
probeRet: &RemotePrinterInfo{
Reachable: false,
Error: "cannot reach 192.168.0.5:631: connection refused",
},
wantReach: false,
},
{
name: "empty protocol defaults to ipp",
protocol: "",
probeRet: &RemotePrinterInfo{
Reachable: true,
MakeModel: "Test Printer",
},
wantTLS: false,
wantReach: true,
wantModel: "Test Printer",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var capturedTLS bool
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
capturedTLS = useTLS
return tt.probeRet, tt.probeErr
}
result, err := m.TestRemotePrinter("192.168.0.5", 631, tt.protocol)
if tt.probeErr != nil {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantReach, result.Reachable)
assert.Equal(t, tt.wantModel, result.MakeModel)
assert.Equal(t, tt.wantTLS, capturedTLS)
})
}
}
func TestManager_TestRemotePrinter_AuthRequired(t *testing.T) {
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
// Simulate what happens when the printer returns HTTP 401
return probeRemotePrinterWithAuthError(host, port, useTLS)
}
result, err := m.TestRemotePrinter("192.168.0.107", 631, "ipp")
assert.NoError(t, err)
assert.True(t, result.Reachable)
assert.Equal(t, "authentication required", result.Info)
assert.Contains(t, result.URI, "ipp://192.168.0.107:631")
}
// probeRemotePrinterWithAuthError simulates a probe where the printer
// returns HTTP 401 on both endpoints.
func probeRemotePrinterWithAuthError(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
// This simulates what probeRemotePrinter does when both endpoints
// return auth errors. We test the auth detection logic directly.
err := ipp.HTTPError{Code: 401}
if isAuthError(err) {
proto := "ipp"
if useTLS {
proto = "ipps"
}
return &RemotePrinterInfo{
Reachable: true,
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
Info: "authentication required",
}, nil
}
return nil, err
}
func TestManager_TestRemotePrinter_NonIPPProtocol(t *testing.T) {
m := NewTestManager(nil, nil)
probeCalled := false
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
probeCalled = true
return nil, nil
}
// These will fail at TCP dial (no real server), but the important
// thing is that probeRemoteFn is NOT called for lpd/socket.
m.TestRemotePrinter("192.168.0.5", 9100, "socket")
assert.False(t, probeCalled, "probe function should not be called for socket protocol")
m.TestRemotePrinter("192.168.0.5", 515, "lpd")
assert.False(t, probeCalled, "probe function should not be called for lpd protocol")
}
func TestHandleTestConnection_Success(t *testing.T) {
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
return &RemotePrinterInfo{
Reachable: true,
MakeModel: "HP OfficeJet 8010",
Name: "OfficeJet",
State: "idle",
URI: "ipp://192.168.0.5:631/ipp/print",
}, nil
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
Method: "cups.testConnection",
Params: map[string]any{
"host": "192.168.0.5",
},
}
handleTestConnection(conn, req, m)
var resp models.Response[RemotePrinterInfo]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Reachable)
assert.Equal(t, "HP OfficeJet 8010", resp.Result.MakeModel)
}
func TestHandleTestConnection_MissingHost(t *testing.T) {
m := NewTestManager(nil, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
Method: "cups.testConnection",
Params: map[string]any{},
}
handleTestConnection(conn, req, m)
var resp models.Response[any]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.Nil(t, resp.Result)
assert.NotNil(t, resp.Error)
}
func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
assert.Equal(t, 9631, port)
assert.True(t, useTLS)
return &RemotePrinterInfo{Reachable: true, URI: "ipps://192.168.0.5:9631/ipp/print"}, nil
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
Method: "cups.testConnection",
Params: map[string]any{
"host": "192.168.0.5",
"port": float64(9631),
"protocol": "ipps",
},
}
handleTestConnection(conn, req, m)
var resp models.Response[RemotePrinterInfo]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Reachable)
}
func TestHandleRequest_TestConnection(t *testing.T) {
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
return &RemotePrinterInfo{Reachable: true}, nil
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
Method: "cups.testConnection",
Params: map[string]any{"host": "192.168.0.5"},
}
HandleRequest(conn, req, m)
var resp models.Response[RemotePrinterInfo]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Reachable)
}
-11
View File
@@ -55,16 +55,6 @@ type PPD struct {
Type string `json:"type"` Type string `json:"type"`
} }
type RemotePrinterInfo struct {
Reachable bool `json:"reachable"`
MakeModel string `json:"makeModel"`
Name string `json:"name"`
Info string `json:"info"`
State string `json:"state"`
URI string `json:"uri"`
Error string `json:"error,omitempty"`
}
type PrinterClass struct { type PrinterClass struct {
Name string `json:"name"` Name string `json:"name"`
URI string `json:"uri"` URI string `json:"uri"`
@@ -87,7 +77,6 @@ type Manager struct {
notifierWg sync.WaitGroup notifierWg sync.WaitGroup
lastNotifiedState *CUPSState lastNotifiedState *CUPSState
baseURL string baseURL string
probeRemoteFn func(host string, port int, useTLS bool) (*RemotePrinterInfo, error)
} }
type SubscriptionManagerInterface interface { type SubscriptionManagerInterface interface {
-61
View File
@@ -1,61 +0,0 @@
package location
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type LocationEvent struct {
Type string `json:"type"`
Data State `json:"data"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "location.getState":
handleGetState(conn, req, manager)
case "location.subscribe":
handleSubscribe(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
event := LocationEvent{
Type: "state_changed",
Data: initialState,
}
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
return
}
for state := range stateChan {
event := LocationEvent{
Type: "state_changed",
Data: state,
}
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
Result: &event,
}); err != nil {
return
}
}
}
-175
View File
@@ -1,175 +0,0 @@
package location
import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
func NewManager(client geolocation.Client) (*Manager, error) {
currLocation, err := client.GetLocation()
if err != nil {
log.Warnf("Failed to get initial location: %v", err)
}
m := &Manager{
client: client,
dirty: make(chan struct{}),
stopChan: make(chan struct{}),
state: &State{
Latitude: currLocation.Latitude,
Longitude: currLocation.Longitude,
},
}
if err := m.startSignalPump(); err != nil {
return nil, err
}
m.notifierWg.Add(1)
go m.notifier()
return m, nil
}
func (m *Manager) Close() {
close(m.stopChan)
m.notifierWg.Wait()
m.sigWG.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if ch, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch)
}
}
func (m *Manager) startSignalPump() error {
m.sigWG.Add(1)
go func() {
defer m.sigWG.Done()
subscription := m.client.Subscribe("locationManager")
defer m.client.Unsubscribe("locationManager")
for {
select {
case <-m.stopChan:
return
case location, ok := <-subscription:
if !ok {
return
}
m.handleLocationChange(location)
}
}
}()
return nil
}
func (m *Manager) handleLocationChange(location geolocation.Location) {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
m.state.Latitude = location.Latitude
m.state.Longitude = location.Longitude
m.notifySubscribers()
}
func (m *Manager) notifySubscribers() {
select {
case m.dirty <- struct{}{}:
default:
}
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{
Latitude: 0.0,
Longitude: 0.0,
}
}
stateCopy := *m.state
return stateCopy
}
func (m *Manager) notifier() {
defer m.notifierWg.Done()
const minGap = 200 * time.Millisecond
timer := time.NewTimer(minGap)
timer.Stop()
var pending bool
for {
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.dirty:
if pending {
continue
}
pending = true
timer.Reset(minGap)
case <-timer.C:
if !pending {
continue
}
currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
pending = false
continue
}
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- currentState:
default:
log.Warn("Location: subscriber channel full, dropping update")
}
return true
})
stateCopy := currentState
m.lastNotified = &stateCopy
pending = false
}
}
}
func stateChanged(old, new *State) bool {
if old == nil || new == nil {
return true
}
if old.Latitude != new.Latitude {
return true
}
if old.Longitude != new.Longitude {
return true
}
return false
}
-28
View File
@@ -1,28 +0,0 @@
package location
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type State struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
type Manager struct {
state *State
stateMutex sync.RWMutex
client geolocation.Client
stopChan chan struct{}
sigWG sync.WaitGroup
subscribers syncmap.Map[string, chan State]
dirty chan struct{}
notifierWg sync.WaitGroup
lastNotified *State
}
@@ -5,6 +5,5 @@ const (
dbusPath = "/org/freedesktop/login1" dbusPath = "/org/freedesktop/login1"
dbusManagerInterface = "org.freedesktop.login1.Manager" dbusManagerInterface = "org.freedesktop.login1.Manager"
dbusSessionInterface = "org.freedesktop.login1.Session" dbusSessionInterface = "org.freedesktop.login1.Session"
dbusUserInterface = "org.freedesktop.login1.User"
dbusPropsInterface = "org.freedesktop.DBus.Properties" dbusPropsInterface = "org.freedesktop.DBus.Properties"
) )
+9 -198
View File
@@ -17,8 +17,15 @@ func NewManager() (*Manager, error) {
return nil, fmt.Errorf("failed to connect to system bus: %w", err) return nil, fmt.Errorf("failed to connect to system bus: %w", err)
} }
sessionID := os.Getenv("XDG_SESSION_ID")
if sessionID == "" {
sessionID = "self"
}
m := &Manager{ m := &Manager{
state: &SessionState{}, state: &SessionState{
SessionID: sessionID,
},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
@@ -53,13 +60,12 @@ func (m *Manager) initialize() error {
m.initializeFallbackDelay() m.initializeFallbackDelay()
sessionID, sessionPath, err := m.discoverSession() sessionPath, err := m.getSession(m.state.SessionID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get session path: %w", err) return fmt.Errorf("failed to get session path: %w", err)
} }
m.stateMutex.Lock() m.stateMutex.Lock()
m.state.SessionID = sessionID
m.state.SessionPath = string(sessionPath) m.state.SessionPath = string(sessionPath)
m.sessionPath = sessionPath m.sessionPath = sessionPath
m.stateMutex.Unlock() m.stateMutex.Unlock()
@@ -73,41 +79,6 @@ func (m *Manager) initialize() error {
return nil return nil
} }
func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) {
// 1. Explicit XDG_SESSION_ID
if id := os.Getenv("XDG_SESSION_ID"); id != "" {
if path, err := m.getSession(id); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id)
return id, path, nil
}
}
// 2. PID-based lookup (works when caller is inside a session cgroup)
if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id)
return id, path, nil
}
// 3. User's primary display session (handles UWSM and similar)
if id, path, err := m.getUserDisplaySession(); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id)
return id, path, nil
}
// 4. Score all sessions for current UID
if id, path, err := m.findBestSession(); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id)
return id, path, nil
}
// 5. Last resort: "self"
path, err := m.getSession("self")
if err != nil {
return "", "", fmt.Errorf("%w", err)
}
return "self", path, nil
}
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) { func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
var out dbus.ObjectPath var out dbus.ObjectPath
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out) err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
@@ -117,166 +88,6 @@ func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
return out, nil return out, nil
} }
func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) {
var path dbus.ObjectPath
if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil {
return "", "", err
}
sessionObj := m.conn.Object(dbusDest, path)
var id dbus.Variant
if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil {
return "", "", err
}
return id.Value().(string), path, nil
}
func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) {
uid := uint32(os.Getuid())
var userPath dbus.ObjectPath
if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil {
return "", "", err
}
userObj := m.conn.Object(dbusDest, userPath)
var display dbus.Variant
if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil {
return "", "", err
}
pair, ok := display.Value().([]any)
if !ok || len(pair) < 2 {
return "", "", fmt.Errorf("unexpected Display format")
}
sessionID, _ := pair[0].(string)
sessionPath, _ := pair[1].(dbus.ObjectPath)
if sessionID == "" || sessionPath == "" {
return "", "", fmt.Errorf("empty Display session")
}
return sessionID, sessionPath, nil
}
type sessionCandidate struct {
id string
path dbus.ObjectPath
}
func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) {
// ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path]
var raw [][]any
if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil {
return "", "", err
}
uid := uint32(os.Getuid())
var candidates []sessionCandidate
for _, entry := range raw {
if len(entry) < 5 {
continue
}
entryUID, _ := entry[1].(uint32)
if entryUID != uid {
continue
}
id, _ := entry[0].(string)
path, _ := entry[4].(dbus.ObjectPath)
if id != "" && path != "" {
candidates = append(candidates, sessionCandidate{id: id, path: path})
}
}
if len(candidates) == 0 {
return "", "", fmt.Errorf("no sessions for uid %d", uid)
}
bestScore := -1
var best sessionCandidate
for _, c := range candidates {
score := m.scoreSession(c.path)
if score > bestScore {
bestScore = score
best = c
}
}
if bestScore < 0 {
return "", "", fmt.Errorf("no viable session found")
}
return best.id, best.path, nil
}
func (m *Manager) scoreSession(path dbus.ObjectPath) int {
obj := m.conn.Object(dbusDest, path)
var props map[string]dbus.Variant
if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil {
return -1
}
getStr := func(key string) string {
if v, ok := props[key]; ok {
if s, ok := v.Value().(string); ok {
return s
}
}
return ""
}
getBool := func(key string) bool {
if v, ok := props[key]; ok {
if b, ok := v.Value().(bool); ok {
return b
}
}
return false
}
getUint32 := func(key string) uint32 {
if v, ok := props[key]; ok {
if u, ok := v.Value().(uint32); ok {
return u
}
}
return 0
}
class := getStr("Class")
if class != "user" {
return -1
}
if getBool("Remote") {
return -1
}
score := 0
if getBool("Active") {
score += 100
}
switch getStr("Type") {
case "wayland", "x11":
score += 80
case "tty":
score += 10
}
if v, ok := props["Seat"]; ok {
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
if seat, ok := seatArr[0].(string); ok && seat != "" {
score += 40
if seat == "seat0" {
score += 10
}
}
}
}
if getUint32("VTNr") > 0 {
score += 20
}
return score
}
func (m *Manager) refreshSessionBinding() error { func (m *Manager) refreshSessionBinding() error {
if m.managerObj == nil || m.conn == nil { if m.managerObj == nil || m.conn == nil {
return fmt.Errorf("manager not fully initialized") return fmt.Errorf("manager not fully initialized")
-1
View File
@@ -29,7 +29,6 @@ func handleMatugenQueue(conn net.Conn, req models.Request) {
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false), SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false), TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
SkipTemplates: models.GetOr(req, "skipTemplates", ""), SkipTemplates: models.GetOr(req, "skipTemplates", ""),
Contrast: models.GetOr(req, "contrast", 0.0),
} }
wait := models.GetOr(req, "wait", true) wait := models.GetOr(req, "wait", true)
-1
View File
@@ -10,7 +10,6 @@ type Backend interface {
ScanWiFi() error ScanWiFi() error
ScanWiFiDevice(device string) error ScanWiFiDevice(device string) error
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
GetWiFiQRCodeContent(ssid string) (string, error)
GetWiFiDevices() []WiFiDevice GetWiFiDevices() []WiFiDevice
ConnectWiFi(req ConnectionRequest) error ConnectWiFi(req ConnectionRequest) error
@@ -111,10 +111,6 @@ func (b *HybridIwdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkI
return b.wifi.GetWiFiNetworkDetails(ssid) return b.wifi.GetWiFiNetworkDetails(ssid)
} }
func (b *HybridIwdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
return b.wifi.GetWiFiQRCodeContent(ssid)
}
func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error { func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
if err := b.wifi.ConnectWiFi(req); err != nil { if err := b.wifi.ConnectWiFi(req); err != nil {
return err return err
@@ -1,9 +1,6 @@
package network package network
import ( import "fmt"
"fmt"
"os"
)
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) { func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
return nil, fmt.Errorf("wired connections not supported by iwd") return nil, fmt.Errorf("wired connections not supported by iwd")
@@ -115,19 +112,3 @@ func (b *IWDBackend) getWiFiDevicesLocked() []WiFiDevice {
Networks: b.state.WiFiNetworks, Networks: b.state.WiFiNetworks,
}} }}
} }
func (b *IWDBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
path := iwdConfigPath(ssid)
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("no saved iwd config for `%s`: %w", ssid, err)
}
passphrase, err := parseIWDPassphrase(string(data))
if err != nil {
return "", fmt.Errorf("failed to read passphrase for `%s`: %w", ssid, err)
}
return FormatWiFiQRString("WPA", ssid, passphrase), nil
}
@@ -18,10 +18,6 @@ func (b *SystemdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInf
return nil, fmt.Errorf("WiFi details not supported by networkd backend") return nil, fmt.Errorf("WiFi details not supported by networkd backend")
} }
func (b *SystemdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
return "", fmt.Errorf("WiFi QR Code not supported by networkd backend")
}
func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error { func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
return fmt.Errorf("WiFi connect not supported by networkd backend") return fmt.Errorf("WiFi connect not supported by networkd backend")
} }
@@ -196,65 +196,6 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
}, nil }, nil
} }
func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
conn, err := b.findConnection(ssid)
if err != nil {
return "", fmt.Errorf("no saved connection for `%s`: %w", ssid, err)
}
connSettings, err := conn.GetSettings()
if err != nil {
return "", fmt.Errorf("failed to get settings for `%s`: %w", ssid, err)
}
secSettings, ok := connSettings["802-11-wireless-security"]
if !ok {
return "", fmt.Errorf("network `%s` has no security settings", ssid)
}
keyMgmt, ok := secSettings["key-mgmt"].(string)
if !ok {
return "", fmt.Errorf("failed to identify security type of network `%s`", ssid)
}
var securityType string
switch keyMgmt {
case "none":
authAlg, _ := secSettings["auth-alg"].(string)
switch authAlg {
case "open":
securityType = "nopass"
default:
securityType = "WEP"
}
case "ieee8021x":
securityType = "WEP"
default:
securityType = "WPA"
}
if securityType != "WPA" {
return "", fmt.Errorf("QR code generation only supports WPA connections, `%s` uses %s", ssid, securityType)
}
secrets, err := conn.GetSecrets("802-11-wireless-security")
if err != nil {
return "", fmt.Errorf("failed to retrieve connection secrets for `%s`: %w", ssid, err)
}
secSecrets, ok := secrets["802-11-wireless-security"]
if !ok {
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
}
psk, ok := secSecrets["psk"].(string)
if !ok {
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
}
return FormatWiFiQRString(securityType, ssid, psk), nil
}
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error { func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
devInfo, err := b.getWifiDeviceForConnection(req.Device) devInfo, err := b.getWifiDeviceForConnection(req.Device)
if err != nil { if err != nil {
+2 -9
View File
@@ -1,7 +1,6 @@
package network package network
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -29,13 +28,7 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
func TestDetectNetworkStack_Integration(t *testing.T) { func TestDetectNetworkStack_Integration(t *testing.T) {
result, err := DetectNetworkStack() result, err := DetectNetworkStack()
if err != nil && strings.Contains(err.Error(), "connect system bus") {
t.Skipf("system D-Bus unavailable: %v", err)
}
assert.NoError(t, err) assert.NoError(t, err)
if assert.NotNil(t, result) { assert.NotNil(t, result)
assert.NotEmpty(t, result.ChosenReason) assert.NotEmpty(t, result.ChosenReason)
}
} }
-41
View File
@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
@@ -41,10 +40,6 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
handleSetPreference(conn, req, manager) handleSetPreference(conn, req, manager)
case "network.info": case "network.info":
handleGetNetworkInfo(conn, req, manager) handleGetNetworkInfo(conn, req, manager)
case "network.qrcode":
handleGetNetworkQRCode(conn, req, manager)
case "network.delete-qrcode":
handleDeleteQRCode(conn, req, manager)
case "network.ethernet.info": case "network.ethernet.info":
handleGetWiredNetworkInfo(conn, req, manager) handleGetWiredNetworkInfo(conn, req, manager)
case "network.subscribe": case "network.subscribe":
@@ -325,42 +320,6 @@ func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, network) models.Respond(conn, req.ID, network)
} }
func handleGetNetworkQRCode(conn net.Conn, req models.Request, manager *Manager) {
ssid, err := params.String(req.Params, "ssid")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
content, err := manager.GetNetworkQRCode(ssid)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, content)
}
func handleDeleteQRCode(conn net.Conn, req models.Request, _ *Manager) {
path, err := params.String(req.Params, "path")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if !isValidQRCodePath(path) {
models.RespondError(conn, req.ID, "invalid QR code path")
return
}
if err := os.Remove(path); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "QR code file deleted"})
}
func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) { func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
uuid, err := params.String(req.Params, "uuid") uuid, err := params.String(req.Params, "uuid")
if err != nil { if err != nil {
-39
View File
@@ -6,8 +6,6 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard"
) )
func NewManager() (*Manager, error) { func NewManager() (*Manager, error) {
@@ -440,43 +438,6 @@ func (m *Manager) GetNetworkInfoDetailed(ssid string) (*NetworkInfoResponse, err
return m.backend.GetWiFiNetworkDetails(ssid) return m.backend.GetWiFiNetworkDetails(ssid)
} }
func (m *Manager) GetNetworkQRCode(ssid string) ([2]string, error) {
content, err := m.backend.GetWiFiQRCodeContent(ssid)
if err != nil {
return [2]string{}, err
}
qrc, err := qrcode.New(content)
if err != nil {
return [2]string{}, fmt.Errorf("failed to create QR code for `%s`: %w", ssid, err)
}
pathThemed, pathNormal := qrCodePaths(ssid)
wThemed, err := standard.New(
pathThemed,
standard.WithBuiltinImageEncoder(standard.PNG_FORMAT),
standard.WithBgTransparent(),
standard.WithFgColorRGBHex("#ffffff"),
)
if err != nil {
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
}
if err := qrc.Save(wThemed); err != nil {
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
}
wNormal, err := standard.New(pathNormal, standard.WithBuiltinImageEncoder(standard.PNG_FORMAT))
if err != nil {
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
}
if err := qrc.Save(wNormal); err != nil {
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
}
return [2]string{pathThemed, pathNormal}, nil
}
func (m *Manager) ToggleWiFi() error { func (m *Manager) ToggleWiFi() error {
enabled, err := m.backend.GetWiFiEnabled() enabled, err := m.backend.GetWiFiEnabled()
if err != nil { if err != nil {
@@ -1,59 +0,0 @@
package network
import (
"fmt"
"path/filepath"
"regexp"
"strings"
)
const qrCodeTmpPrefix = "/tmp/dank-wifi-qrcode-"
func FormatWiFiQRString(securityType, ssid, password string) string {
return fmt.Sprintf("WIFI:T:%s;S:%s;P:%s;;", securityType, ssid, password)
}
func qrCodePaths(ssid string) (themed, normal string) {
safe := sanitizeSSIDForPath(ssid)
themed = fmt.Sprintf("%s%s-themed.png", qrCodeTmpPrefix, safe)
normal = fmt.Sprintf("%s%s-normal.png", qrCodeTmpPrefix, safe)
return
}
func isValidQRCodePath(path string) bool {
clean := filepath.Clean(path)
return strings.HasPrefix(clean, qrCodeTmpPrefix) && strings.HasSuffix(clean, ".png")
}
var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
func sanitizeSSIDForPath(ssid string) string {
return safePathChar.ReplaceAllString(ssid, "_")
}
var iwdVerbatimSSID = regexp.MustCompile(`^[a-zA-Z0-9 _-]+$`)
func iwdConfigPath(ssid string) string {
switch {
case iwdVerbatimSSID.MatchString(ssid):
return fmt.Sprintf("/var/lib/iwd/%s.psk", ssid)
default:
return fmt.Sprintf("/var/lib/iwd/=%x.psk", []byte(ssid))
}
}
func parseIWDPassphrase(data string) (string, error) {
inSecurity := false
for _, line := range strings.Split(data, "\n") {
line = strings.TrimSpace(line)
switch {
case line == "[Security]":
inSecurity = true
case strings.HasPrefix(line, "["):
inSecurity = false
case inSecurity && strings.HasPrefix(line, "Passphrase="):
return strings.TrimPrefix(line, "Passphrase="), nil
}
}
return "", fmt.Errorf("no passphrase found in iwd config")
}
-10
View File
@@ -15,7 +15,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
@@ -193,15 +192,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
if strings.HasPrefix(req.Method, "location.") {
if locationManager == nil {
models.RespondError(conn, req.ID, "location manager not initialized")
return
}
location.HandleRequest(conn, req, locationManager)
return
}
switch req.Method { switch req.Method {
case "ping": case "ping":
models.Respond(conn, req.ID, "pong") models.Respond(conn, req.ID, "pong")
+13 -61
View File
@@ -14,7 +14,6 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
@@ -26,7 +25,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
@@ -72,8 +70,6 @@ var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager var themeModeManager *thememode.Manager
var locationManager *location.Manager
var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
@@ -394,19 +390,6 @@ func InitializeThemeModeManager() error {
return nil return nil
} }
func InitializeLocationManager(geoClient geolocation.Client) error {
manager, err := location.NewManager(geoClient)
if err != nil {
log.Warnf("Failed to initialize location manager: %v", err)
return err
}
locationManager = manager
log.Info("Location manager initialized")
return nil
}
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
@@ -554,10 +537,6 @@ func getServerInfo() ServerInfo {
caps = append(caps, "theme.auto") caps = append(caps, "theme.auto")
} }
if locationManager != nil {
caps = append(caps, "location")
}
if dbusManager != nil { if dbusManager != nil {
caps = append(caps, "dbus") caps = append(caps, "dbus")
} }
@@ -1328,12 +1307,6 @@ func cleanupManagers() {
if wlContext != nil { if wlContext != nil {
wlContext.Close() wlContext.Close()
} }
if locationManager != nil {
locationManager.Close()
}
if geoClientInstance != nil {
geoClientInstance.Close()
}
} }
func Start(printDocs bool) error { func Start(printDocs bool) error {
@@ -1515,9 +1488,6 @@ func Start(printDocs bool) error {
log.Info(" clipboard.getConfig - Get clipboard configuration") log.Info(" clipboard.getConfig - Get clipboard configuration")
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)") log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)") log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
log.Info("Location:")
log.Info(" location.getState - Get current location state")
log.Info(" location.subscribe - Subscribe to location changes (streaming)")
log.Info("") log.Info("")
} }
log.Info("Initializing managers...") log.Info("Initializing managers...")
@@ -1597,37 +1567,6 @@ func Start(printDocs bool) error {
log.Warnf("Wayland manager unavailable: %v", err) log.Warnf("Wayland manager unavailable: %v", err)
} }
if err := InitializeThemeModeManager(); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
}
go func() {
geoClient := geolocation.NewClient()
geoClientInstance = geoClient
if waylandManager != nil {
waylandManager.SetGeoClient(geoClient)
}
if themeModeManager != nil {
themeModeManager.SetGeoClient(geoClient)
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
}()
go func() { go func() {
if err := InitializeBluezManager(); err != nil { if err := InitializeBluezManager(); err != nil {
log.Warnf("Bluez manager unavailable: %v", err) log.Warnf("Bluez manager unavailable: %v", err)
@@ -1656,6 +1595,19 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err) log.Debugf("WlrOutput manager unavailable: %v", err)
} }
if err := InitializeThemeModeManager(); err != nil {
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) fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil { if wlrOutputManager != nil {
go func() { go func() {
+4 -14
View File
@@ -5,7 +5,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
@@ -33,8 +32,6 @@ type Manager struct {
cachedIPLat *float64 cachedIPLat *float64
cachedIPLon *float64 cachedIPLon *float64
geoClient geolocation.Client
stopChan chan struct{} stopChan chan struct{}
updateTrigger chan struct{} updateTrigger chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
@@ -314,10 +311,6 @@ func (m *Manager) getConfig() Config {
return m.config return m.config
} }
func (m *Manager) SetGeoClient(client geolocation.Client) {
m.geoClient = client
}
func (m *Manager) getLocation(config Config) (*float64, *float64) { func (m *Manager) getLocation(config Config) (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil { if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude return config.Latitude, config.Longitude
@@ -325,9 +318,6 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
if !config.UseIPLocation { if !config.UseIPLocation {
return nil, nil return nil, nil
} }
if m.geoClient == nil {
return nil, nil
}
m.locationMutex.RLock() m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil { if m.cachedIPLat != nil && m.cachedIPLon != nil {
@@ -337,17 +327,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
} }
m.locationMutex.RUnlock() m.locationMutex.RUnlock()
location, err := m.geoClient.GetLocation() lat, lon, err := wayland.FetchIPLocation()
if err != nil { if err != nil {
return nil, nil return nil, nil
} }
m.locationMutex.Lock() m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude m.cachedIPLat = lat
m.cachedIPLon = &location.Longitude m.cachedIPLon = lon
m.locationMutex.Unlock() m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon return lat, lon
} }
func statesEqual(a, b *State) bool { func statesEqual(a, b *State) bool {
+17 -27
View File
@@ -13,7 +13,6 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
) )
@@ -421,10 +420,6 @@ func (m *Manager) recalcSchedule(now time.Time) {
} }
} }
func (m *Manager) SetGeoClient(client geolocation.Client) {
m.geoClient = client
}
func (m *Manager) getLocation() (*float64, *float64) { func (m *Manager) getLocation() (*float64, *float64) {
m.configMutex.RLock() m.configMutex.RLock()
config := m.config config := m.config
@@ -433,31 +428,26 @@ func (m *Manager) getLocation() (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil { if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude return config.Latitude, config.Longitude
} }
if !config.UseIPLocation { if config.UseIPLocation {
return nil, nil m.locationMutex.RLock()
} if m.cachedIPLat != nil && m.cachedIPLon != nil {
if m.geoClient == nil { lat, lon := m.cachedIPLat, m.cachedIPLon
return nil, nil m.locationMutex.RUnlock()
} return lat, lon
}
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
m.locationMutex.RUnlock() m.locationMutex.RUnlock()
lat, lon, err := FetchIPLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = lat
m.cachedIPLon = lon
m.locationMutex.Unlock()
return lat, lon return lat, lon
} }
m.locationMutex.RUnlock() return nil, nil
location, err := m.geoClient.GetLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon
} }
func (m *Manager) hasValidSchedule() bool { func (m *Manager) hasValidSchedule() bool {
-3
View File
@@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
@@ -98,8 +97,6 @@ type Manager struct {
dbusConn *dbus.Conn dbusConn *dbus.Conn
dbusSignal chan *dbus.Signal dbusSignal chan *dbus.Signal
geoClient geolocation.Client
lastAppliedTemp int lastAppliedTemp int
lastAppliedGamma float64 lastAppliedGamma float64
} }
+4 -17
View File
@@ -2,10 +2,10 @@ package wlcontext
import ( import (
"fmt" "fmt"
"golang.org/x/sys/unix"
"os" "os"
"sync" "sync"
"time"
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -123,9 +123,6 @@ func (sc *SharedContext) eventDispatcher() {
{Fd: int32(sc.wakeR), Events: unix.POLLIN}, {Fd: int32(sc.wakeR), Events: unix.POLLIN},
} }
consecutiveErrors := 0
const maxConsecutiveErrors = 20
for { for {
sc.drainCmdQueue() sc.drainCmdQueue()
@@ -156,19 +153,9 @@ func (sc *SharedContext) eventDispatcher() {
} }
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) { if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
consecutiveErrors++ log.Errorf("Wayland connection error: %v", err)
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err) return
if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
return
}
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
continue
} }
consecutiveErrors = 0
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
cmdq: make(chan cmd, 512), cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
fatalError: make(chan error, 1), fatalError: make(chan error, 1),
+1 -1
View File
@@ -40,7 +40,7 @@ func (m Model) viewDeployingConfigs() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Setting up configuration files...") status := m.styles.Normal.Render("Setting up configuration files...")
fmt.Fprintf(&b, "%s %s", spinner, status) b.WriteString(fmt.Sprintf("%s %s", spinner, status))
b.WriteString("\n\n") b.WriteString("\n\n")
// Show progress information // Show progress information
+1 -1
View File
@@ -23,7 +23,7 @@ func (m Model) viewDetectingDeps() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...") status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
fmt.Fprintf(&b, "%s %s", spinner, status) b.WriteString(fmt.Sprintf("%s %s", spinner, status))
return b.String() return b.String()
} }
+2 -2
View File
@@ -52,7 +52,7 @@ func (m Model) viewInstallingPackages() string {
if !m.packageProgress.isComplete { if !m.packageProgress.isComplete {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render(m.packageProgress.step) status := m.styles.Normal.Render(m.packageProgress.step)
fmt.Fprintf(&b, "%s %s", spinner, status) b.WriteString(fmt.Sprintf("%s %s", spinner, status))
b.WriteString("\n\n") b.WriteString("\n\n")
// Show progress bar // Show progress bar
@@ -387,7 +387,7 @@ func (m Model) viewDebugLogs() string {
for i := startIdx; i < len(allLogs); i++ { for i := startIdx; i < len(allLogs); i++ {
if allLogs[i] != "" { if allLogs[i] != "" {
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i]) b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i]))
} }
} }
+1 -1
View File
@@ -75,7 +75,7 @@ func (m Model) viewFingerprintAuth() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Waiting for fingerprint...") status := m.styles.Normal.Render("Waiting for fingerprint...")
fmt.Fprintf(&b, "%s %s", spinner, status) b.WriteString(fmt.Sprintf("%s %s", spinner, status))
} }
return b.String() return b.String()
+3 -3
View File
@@ -132,9 +132,9 @@ func (m Model) viewWelcome() string {
contentStyle = contentStyle.Bold(true) contentStyle = contentStyle.Bold(true)
} }
fmt.Fprintf(&b, " %s %s\n", b.WriteString(fmt.Sprintf(" %s %s\n",
prefixStyle.Render(prefix), prefixStyle.Render(prefix),
contentStyle.Render(content)) contentStyle.Render(content)))
} }
b.WriteString("\n") b.WriteString("\n")
@@ -158,7 +158,7 @@ func (m Model) viewWelcome() string {
} else if m.isLoading { } else if m.isLoading {
spinner := m.spinner.View() spinner := m.spinner.View()
loading := m.styles.Normal.Render("Detecting system...") loading := m.styles.Normal.Render("Detecting system...")
fmt.Fprintf(&b, "%s %s\n\n", spinner, loading) b.WriteString(fmt.Sprintf("%s %s\n\n", spinner, loading))
} }
// Footer with better visual separation // Footer with better visual separation
+5 -5
View File
@@ -27,12 +27,12 @@ override_dh_auto_build:
# Verify core directory exists (native package format has source at root) # Verify core directory exists (native package format has source at root)
test -d core || (echo "ERROR: core directory not found!" && exit 1) test -d core || (echo "ERROR: core directory not found!" && exit 1)
# Pin go.mod and vendor/modules.txt to the installed Go toolchain version # Patch go.mod to use Go 1.24 base version (Debian 13 has 1.23.x, may vary)
GO_INSTALLED=$$(go version | grep -oP 'go\K[0-9]+\.[0-9]+'); \ sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
sed -i "s/^go [0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$$/go $${GO_INSTALLED}/" core/go.mod; \
sed -i "s/^\(## explicit; go \)[0-9]\+\.[0-9]\+\(\.[0-9]*\)\?$$/\1$${GO_INSTALLED}/" core/vendor/modules.txt
# Build dms-cli (single shell to preserve variables; arch: Debian amd64/arm64 -> Makefile amd64/arm64) # Build dms-cli from source using vendored dependencies
# Extract version info and build in single shell to preserve variables
# Architecture mapping: Debian amd64/arm64 -> Makefile amd64/arm64
VERSION="$(UPSTREAM_VERSION)"; \ VERSION="$(UPSTREAM_VERSION)"; \
COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown"); \ COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown"); \
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \ if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
+1 -1
View File
@@ -3,7 +3,7 @@
<service name="download_url"> <service name="download_url">
<param name="protocol">https</param> <param name="protocol">https</param>
<param name="host">github.com</param> <param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.3/dms-qml.tar.gz</param> <param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.2/dms-qml.tar.gz</param>
<param name="filename">dms-qml.tar.gz</param> <param name="filename">dms-qml.tar.gz</param>
</service> </service>
</services> </services>
+4 -3
View File
@@ -1,5 +1,6 @@
dms-greeter (1.4.3db1) unstable; urgency=medium dms-greeter (1.4.2db8) unstable; urgency=medium
* Update to v1.4.3 stable release * Initial Debian OBS package
* Port from Ubuntu/Fedora packaging
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 25 Feb 2026 02:40:00 +0000 -- Avenge Media <AvengeMedia.US@gmail.com> Sat, 21 Feb 2026 00:00:00 +0000
+1 -1
View File
@@ -13,7 +13,7 @@ Architecture: any
Depends: ${misc:Depends}, Depends: ${misc:Depends},
greetd, greetd,
quickshell-git | quickshell quickshell-git | quickshell
Suggests: niri | hyprland | sway Recommends: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3 DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors. inspired greeter interface built with Quickshell for Wayland compositors.
+1 -1
View File
@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms Package: dms
Architecture: amd64 arm64 Architecture: amd64
Depends: ${misc:Depends}, Depends: ${misc:Depends},
quickshell | quickshell-git, quickshell | quickshell-git,
accountsservice, accountsservice,
@@ -1,3 +1,2 @@
dms-distropkg-amd64.gz dms-distropkg-amd64.gz
dms-distropkg-arm64.gz
dms-source.tar.gz dms-source.tar.gz
-1
View File
@@ -1,5 +1,4 @@
# Include files that are normally excluded by .gitignore # Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad # These are needed for the build process on Launchpad
tar-ignore = !dms-distropkg-amd64.gz tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-distropkg-arm64.gz
tar-ignore = !dms-source.tar.gz tar-ignore = !dms-source.tar.gz
+3 -26
View File
@@ -3,7 +3,6 @@
%global debug_package %{nil} %global debug_package %{nil}
%global version {{{ git_repo_version }}} %global version {{{ git_repo_version }}}
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors %global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
%global go_toolchain_version 1.26.1
Name: dms Name: dms
Epoch: 2 Epoch: 2
@@ -15,12 +14,12 @@ License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell URL: https://github.com/AvengeMedia/DankMaterialShell
VCS: {{{ git_repo_vcs }}} VCS: {{{ git_repo_vcs }}}
Source0: {{{ git_repo_pack }}} Source0: {{{ git_repo_pack }}}
Source1: https://go.dev/dl/go%{go_toolchain_version}.linux-amd64.tar.gz
Source2: https://go.dev/dl/go%{go_toolchain_version}.linux-arm64.tar.gz
BuildRequires: git-core BuildRequires: git-core
BuildRequires: gzip BuildRequires: gzip
BuildRequires: golang >= 1.24
BuildRequires: make BuildRequires: make
BuildRequires: wget
BuildRequires: systemd-rpm-macros BuildRequires: systemd-rpm-macros
# Core requirements # Core requirements
@@ -29,7 +28,7 @@ Requires: accountsservice
Requires: dms-cli = %{epoch}:%{version}-%{release} Requires: dms-cli = %{epoch}:%{version}-%{release}
Requires: dgop Requires: dgop
# Core utilities (Recommended for DMS functionality) # Core utilities (Highly recommended for DMS functionality)
Recommends: cava Recommends: cava
Recommends: danksearch Recommends: danksearch
Recommends: matugen Recommends: matugen
@@ -67,28 +66,6 @@ Provides native DBus bindings, NetworkManager integration, and system utilities.
VERSION="%{version}" VERSION="%{version}"
COMMIT=$(echo "%{version}" | grep -oP '[a-f0-9]{7,}' | head -n1 || echo "unknown") COMMIT=$(echo "%{version}" | grep -oP '[a-f0-9]{7,}' | head -n1 || echo "unknown")
# Use pinned bundled Go toolchain (deterministic across chroots)
case "%{_arch}" in
x86_64)
GO_TARBALL="%{_sourcedir}/go%{go_toolchain_version}.linux-amd64.tar.gz"
;;
aarch64)
GO_TARBALL="%{_sourcedir}/go%{go_toolchain_version}.linux-arm64.tar.gz"
;;
*)
echo "Unsupported architecture for bundled Go: %{_arch}"
exit 1
;;
esac
rm -rf .go
tar -xzf "$GO_TARBALL"
mv go .go
export GOROOT="$PWD/.go"
export PATH="$GOROOT/bin:$PATH"
export GOTOOLCHAIN=local
go version
cd core cd core
make dist VERSION="$VERSION" COMMIT="$COMMIT" make dist VERSION="$VERSION" COMMIT="$COMMIT"
+2 -1
View File
@@ -2,6 +2,7 @@
config, config,
lib, lib,
pkgs, pkgs,
dmsPkgs,
... ...
}: }:
let let
@@ -9,7 +10,7 @@ let
in in
{ {
packages = [ packages = [
cfg.package dmsPkgs.dms-shell
] ]
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package ++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
++ lib.optionals cfg.enableVPN [ ++ lib.optionals cfg.enableVPN [
+3 -22
View File
@@ -8,7 +8,6 @@
let let
inherit (lib) types; inherit (lib) types;
cfg = config.programs.dank-material-shell.greeter; cfg = config.programs.dank-material-shell.greeter;
cfgDms = config.programs.dank-material-shell;
inherit (config.services.greetd.settings.default_session) user; inherit (config.services.greetd.settings.default_session) user;
@@ -24,20 +23,19 @@ let
lib.makeBinPath [ lib.makeBinPath [
cfg.quickshell.package cfg.quickshell.package
compositorPackage compositorPackage
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
] ]
} }
${ ${
lib.escapeShellArgs ( lib.escapeShellArgs (
[ [
"sh" "sh"
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter" "${../../quickshell/Modules/Greetd/assets/dms-greeter}"
"--cache-dir" "--cache-dir"
cacheDir cacheDir
"--command" "--command"
cfg.compositor.name cfg.compositor.name
"-p" "-p"
"${cfg.package}/share/quickshell/dms" "${dmsPkgs.dms-shell}/share/quickshell/dms"
] ]
++ lib.optionals (cfg.compositor.customConfig != "") [ ++ lib.optionals (cfg.compositor.customConfig != "") [
"-C" "-C"
@@ -67,21 +65,6 @@ in
options.programs.dank-material-shell.greeter = { options.programs.dank-material-shell.greeter = {
enable = lib.mkEnableOption "DankMaterialShell greeter"; enable = lib.mkEnableOption "DankMaterialShell greeter";
package = lib.mkOption {
type = types.package;
default = if cfgDms.enable or false then cfgDms.package else dmsPkgs.dms-shell;
defaultText = lib.literalExpression ''
if config.programs.dank-material-shell.enable
then config.programs.dank-material-shell.package
else built from source;
'';
description = ''
The DankMaterialShell package to use for the greeter.
Defaults to the package from `programs.dank-material-shell` if it is enabled,
otherwise defaults to building from source.
'';
};
compositor.name = lib.mkOption { compositor.name = lib.mkOption {
type = types.enum [ type = types.enum [
"niri" "niri"
@@ -196,9 +179,7 @@ in
fi fi
if [ -f settings.json ]; then if [ -f settings.json ]; then
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)" if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
cp "$theme_file" custom-theme.json
mv settings.json settings.orig.json mv settings.json settings.orig.json
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json ${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
fi fi
+3 -1
View File
@@ -2,6 +2,7 @@
config, config,
pkgs, pkgs,
lib, lib,
dmsPkgs,
... ...
}@args: }@args:
let let
@@ -12,6 +13,7 @@ let
config config
pkgs pkgs
lib lib
dmsPkgs
; ;
}; };
hasPluginSettings = lib.any (plugin: plugin.settings != { }) ( hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
@@ -94,7 +96,7 @@ in
}; };
Service = { Service = {
ExecStart = lib.getExe cfg.package + " run --session"; ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
Restart = "on-failure"; Restart = "on-failure";
}; };
+26 -23
View File
@@ -2,9 +2,11 @@
config, config,
lib, lib,
... ...
}: let }:
let
cfg = config.programs.dank-material-shell; cfg = config.programs.dank-material-shell;
in { in
{
imports = [ imports = [
./dms-rename.nix ./dms-rename.nix
]; ];
@@ -14,11 +16,9 @@ in {
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds"; enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup"; enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
includes = { includes = {
enable = enable = (lib.mkEnableOption "includes for niri-flake") // {
(lib.mkEnableOption "includes for niri-flake") default = true;
// { };
default = true;
};
override = lib.mkOption { override = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
description = '' description = ''
@@ -44,10 +44,8 @@ in {
"alttab" "alttab"
"binds" "binds"
"colors" "colors"
"cursor"
"layout" "layout"
"outputs" "outputs"
"windowrules"
"wpblur" "wpblur"
]; ];
example = [ example = [
@@ -72,21 +70,24 @@ in {
let let
cfg' = cfg.niri.includes; cfg' = cfg.niri.includes;
withOriginalConfig = dmsFiles: withOriginalConfig =
if cfg'.override dmsFiles:
then [cfg'.originalFileName] ++ dmsFiles if cfg'.override then
else dmsFiles ++ [cfg'.originalFileName]; [ cfg'.originalFileName ] ++ dmsFiles
else
dmsFiles ++ [ cfg'.originalFileName ];
fixes = map (fix: "\n${fix}") ( fixes = map (fix: "\n${fix}") (
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable) lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
# kdl # kdl
'' ''
// Border fix // Border fix
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details // See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
layout { border { on; }; } layout { border { on; }; }
'' ''
); );
in { in
{
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl"; niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
niri-config-dms = { niri-config-dms = {
target = "niri/config.kdl"; target = "niri/config.kdl";
@@ -103,9 +104,11 @@ in {
programs.niri.settings = lib.mkMerge [ programs.niri.settings = lib.mkMerge [
(lib.mkIf cfg.niri.enableKeybinds { (lib.mkIf cfg.niri.enableKeybinds {
binds = with config.lib.niri.actions; let binds =
dms-ipc = spawn "dms" "ipc"; with config.lib.niri.actions;
in let
dms-ipc = spawn "dms" "ipc";
in
{ {
"Mod+Space" = { "Mod+Space" = {
action = dms-ipc "spotlight" "toggle"; action = dms-ipc "spotlight" "toggle";

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