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

Compare commits

...

67 Commits

Author SHA1 Message Date
purian23 0e0a8ae0fb feat(dankdash): add native DankConnect integration 2026-07-25 20:16:29 -04:00
dms-ci[bot] 8bb7396362 nix: sync flake.lock to dank-qml-common submodule 2026-07-24 23:36:44 +00:00
purian23 62648ed75a Bump dank-qml-common: revert CachingImage cache change 2026-07-24 19:36:08 -04:00
bbedward fa629fbf99 dock: use required properties in dock item delegate, fixes context injection on Qt 6.8 (#2926) 2026-07-24 15:45:57 -04:00
ArijanJ 7222e072af osd: click to hide some osds (#2927) 2026-07-24 15:19:47 -04:00
bbedward f57f254855 notification: clip region on animation exit
related #2917
port 1.5
2026-07-24 14:35:14 -04:00
bbedward fa094db127 lock: add completed guard in fade to dpms window
related #2885
2026-07-24 14:12:26 -04:00
bbedward 77a357109a bluetooth: add soft rfkill unblock, wait for adapter to become available
instead of forfeiting subscription

fixes #2922
fixes #1537
2026-07-24 11:28:24 -04:00
dms-ci[bot] 295657492d nix: update vendorHash for go.mod changes 2026-07-24 14:52:12 +00:00
bbedward 2bafc21229 gamma: replace login1 scheduler re-calculation with suspend-aware
boottimer
2026-07-24 10:49:37 -04:00
bbedward ea03fb2788 cups: fix initialization race
port 1.5
2026-07-24 10:42:27 -04:00
bbedward 0815e48465 dock: limit input mask to body, disable hover headroom unless enabled
fixes #2923
2026-07-24 10:31:51 -04:00
bbedward 42060366cb network: fix networkPreference reverting from eth to wifi
fixes #2919
port 1.5
2026-07-24 10:10:59 -04:00
bbedward 8af71036cc frame: merge bars with frame window and get things on Top layer by
default
2026-07-23 14:10:17 -04:00
bbedward 5854327c28 settings: fix close IPC
fixes #2912
port 1.5
2026-07-23 14:09:59 -04:00
euletheia 99f452c999 feat(Themes): add support for GTK3 dynamic theming (#2523)
* feat(Themes): add support for GTK3 dynamic theming

Instead of overriding named colors of all GTK3 themes via the config
directory file, only override named colors for each variant of the
adw-gtk3 theme (light and dark), and let GTK3 apps change themes
depending on the value of org.gnome.desktop.interface color-scheme.

* fix(Themes): apply requested changes

- remove gtk3 override if it's a symlink
- remove call to gtk.sh with old arg 'assets-only'
- append key to dconf path to respect dconf write signature

* refactor(Themes): separate 'include'/'apply' logic for GTK3 dynamic theming

* revert(Themes): reset timer between color-scheme refresh

- rename DMS_ENABLE_GTK4_REFRESH to DMS_ENABLE_GTK_REFRESH
- revert a3fba8

* feat(Themes): add 'remove' mode to gtk.sh

Allow to remove all inclusions and overrides for GTK.

* fix(Themes): backup adw-gtk3 original css in 'apply' (not 'include')

* style(Themes): rename argument 'include' to 'patch' for clarity
2026-07-23 13:31:39 -04:00
bbedward d818067bee calendar/dcal: fix flatpak socket detection
port 1.5
2026-07-23 10:14:26 -04:00
Huỳnh Thiện Lộc 5538913320 feat(settings): add option to suppress config reload toast (#2730) (#2915)
* feat(settings): add option to suppress config reload toast (#2730)

Add a toggle in Settings → Compositor Layout to disable the 'config reloaded' toast for Niri and MangoWC compositors. Session setting showConfigReloadToast defaults to true.

* chore: update settings search index for config reload toast

* refactor(settings): rename 'Compositor Notifications' to 'Compositor Toasts'

* fix(settings): reuse existing I18n.tr('Notifications') translation
2026-07-23 10:12:39 -04:00
Thomas Kroll 8188ced955 Add option to ignore app-requested notification timeout (#2909)
* Add option to ignore app-requested notification timeout

Notifications may include an expireTimeout hint that currently takes
precedence over the configured Low/Normal/Critical durations, so
popups can disappear earlier (or later) than expected with no
interaction involved. Add notificationIgnoreAppTimeout (default off,
matching the notification spec) to let users force their own
durations regardless of what the app requests.

* Update settings search index for new timeout override toggle
2026-07-23 08:32:14 -04:00
Huỳnh Thiện Lộc 75eac83b47 feat(settings): add Mouse & Touchpad settings tab for Niri (#2905)
* feat(settings): add Mouse & Trackpad settings tab

* style(settings): clarify touchpad natural scroll description

* chore(settings): update settings search index

* refactor(settings): unify Mouse & Trackpad terminology to Mouse & Touchpad

* feat(setup): deploy default dms/input.kdl on setup

* style(niri): restore include section comment in base config template

* refactor(niri): remove redundant input block from base config template

* refactor(niri): restore keyboard and non-mouse input config to base template

* feat(settings): add touchpad disable-on-external-mouse toggle

* feat(settings): add left-handed, scroll-factor and dwt options

* feat(settings): add middle-emulation, scroll-method, tap-and-drag and drag-lock options

* refactor(settings): re-order mouse and touchpad UI options logically
2026-07-22 13:28:40 -04:00
bbedward ce548cb29c update CONTRIBUTING for submodule recurse option recommendation 2026-07-22 12:32:59 -04:00
bbedward 61510c2186 theme: support for QT_PLATFORMTHEME=kde 2026-07-22 12:20:27 -04:00
Thomas Kroll 237fb57d5e fix(lock): hold sleep inhibitor until compositor confirms lock (#2907)
lock-before-suspend released the logind sleep delay inhibitor as soon as Qt
reported the lock rendered (afterAnimating/afterRendering). That signal fires
before the compositor has committed/presented the ext-session-lock surface, so
the machine could freeze ~1 frame later with the desktop still the last
presented frame. On resume the desktop was briefly visible before the lock
appeared (both suspend and hibernate).

Gate lockerReady on WlSessionLock.secure (the ext-session-lock `locked` event),
so the inhibitor is held until the compositor confirms the session is locked and
the desktop is hidden. LockSurface passes the WlSessionLock down as sessionLock;
the readiness check returns early until secure, and re-fires on secureChanged.
The Go-side fallback timer in loginctl monitor remains as the backstop.

Reproduced on niri / s2idle with 3 outputs: debug log showed lockerReady sent
18 ms before `PM: suspend entry`; with this change the desktop no longer flashes
on resume.

Signed-off-by: Thomas Kroll <99196436+tkroll-ionos@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:24:25 -04:00
bbedward fe561f2b5d location: prevent calls when weather auto-locate is disabled
port 1.5
2026-07-21 23:35:33 -04:00
purian23 1c01254e9b fix(void): update working templates on dms-git
Port 1.5
2026-07-21 19:28:26 -04:00
bbedward 958672badd lock/pam: attempt to fix fprintd retry spam after resume 2026-07-21 17:53:58 -04:00
bbedward 93c3030a1e popout: fix lifecycle of blur with various popouts
fixes #2901
port 1.5
2026-07-21 17:46:43 -04:00
dms-ci[bot] 53c729f685 nix: update vendorHash for go.mod changes 2026-07-21 16:51:00 +00:00
bbedward e0480bf383 core: bump dankgo 2026-07-21 12:48:32 -04:00
bbedward e63d210358 wallpaper: use filter chips for random transitions 2026-07-21 11:27:49 -04:00
bbedward 30d9a34011 lock: avoid lock surface churn
related #2885
port 1.5
2026-07-21 10:50:39 -04:00
bbedward f0bdaa7f72 compositor/toplevels: filter on screen for generic compositors, without
workspace distinction

fixes #2890
port 1.5
2026-07-21 10:26:58 -04:00
bbedward fa6c36ad57 themes: more detailed WCAG breakdowns in browser 2026-07-20 20:21:00 -04:00
bbedward 5de0339506 i18n: add Ukrainian 2026-07-20 19:50:47 -04:00
bbedward 814871d982 window-rules/niri: make floating a tri-state 2026-07-20 18:00:56 -04:00
bbedward 68e60f036d polkit: fix DMS_DISABLE_POLKIT regression from qs 0.2 deprecation commit
fixes #2897
port 1.5
2026-07-20 16:07:41 -04:00
bbedward d185135dd7 theme: add WCAG data 2026-07-20 15:59:31 -04:00
bbedward 70ceee139b audio: revert wpctl workaround
related #2899
port 1.5
2026-07-20 15:19:07 -04:00
bbedward e58b41498c matugen/vesktop: more contrast fixes 2026-07-20 14:31:27 -04:00
bbedward 116cef0b3a lock: fix dpms fade surface getting stuck after unlock
port 1.5
2026-07-20 10:39:44 -04:00
Ron Harel dc75f1f01d feat(network): initial WiFi hotspot support (#2825)
* Add hotspot contract layer with capability-gated dispatch.

Establish the interface and manager plumbing for hotspot support so backends can opt in without expanding the core Backend contract.
Only backends that implement HotspotBackend get hotspot state propagated; others are forced to unsupported regardless of what they self-report.

* Add IPC handlers and API docs for hotspot actions.

Wire up configure/ start/ stop hotspot through the request router so the QML service layer can drive hotspot operations.
Bump API version to 27 and document the capability-gating contract clients should follow.

* Implement NetworkManager hotspot backend.

Implement HotspotBackend on NetworkManagerBackend with DMS-owned profile management, AP capability detection, and band validation.
Device resolution is deferred to StartHotspot when no device is specified, so profiles survive hardware changes.

* Isolate client Wi-Fi state from AP-mode connections.

Filter AP-mode profiles and access points out of all client Wi-Fi paths so the DMS hotspot (and user-created APs) never appear as saved networks, visible networks, or connected state.
This protects existing client behavior before hotspot controls are exposed in the UI.

* Prefer idle radios for automatic hotspot device selection.

* Add hotspot properties and methods to QML service layer.

Expose hotspot state and actions through DMSNetworkService, NetworkService, and LegacyNetworkService.
Capability is gated on API version and backend-reported support, not backend name checks, and stays stable when Wi-Fi radio is disabled.

* Add hotspot controls to Settings and Control Center.

Settings shows a hotspot setup card as a sibling in the Wi-Fi tab with SSID, password, device, band, save, and start/ stop controls.
Control Center shows a compact row that toggles a configured hotspot or routes to Settings for initial setup.
Both stay visible when Wi-Fi is disabled, explaining the requirement instead of hiding.

* Add translator context to hotspot strings.
2026-07-20 09:29:54 -04:00
Legend-017 367ad5f69a Fix(spotlight): improve file search query restoration and mode switching (#2887)
* fix(spotlight): fix empty results on restoring last file search query

- When "remember last query" is enabled and the last query is file search, it shows "no results" until the query is manually modified in full launcher mode.

* fix(spotlight): properly restore last selected mode(tab) when enabled in full launcher mode

* fix(spotlight): allow file trigger in plugins mode(tab)

* fix(spotlight): restore previous mode when clearing '/' file search prefix

When the '/' prefix is removed from the query, the controller now correctly reverts to the previous mode instead of remaining stuck in "files" mode.

* fix(spotlight): code review fix
2026-07-20 09:27:58 -04:00
bbedward 8577c6b815 matugen/vesktop/vencord/equibop: more contrast fixes 2026-07-19 19:22:57 -04:00
bbedward 3d9374d2b2 matugen/vesktop/vencord/equibop: minor contrast improvements 2026-07-19 19:08:41 -04:00
bbedward e300b14dde core: some fixes for embedded distribution, VERSION file, matugen
scripts, and embed scripts
2026-07-19 19:03:10 -04:00
bbedward dc4916b9e0 greeter: more repairs for standalone package, scrap LegacyNetworkService 2026-07-19 17:05:38 -04:00
bbedward fb65a23fac core: remove unused functions after migration 2026-07-19 16:52:54 -04:00
bbedward a5b5ed7e51 gitmodules: set branch=master in .gitmodules 2026-07-19 16:48:44 -04:00
bbedward dfbbfe1793 greeter: replace commands with dms-greeter 2026-07-19 16:43:43 -04:00
bbedward a69acf171a greeter: migrated dms-greeter to github/AvengeMedia/dank-greeter 2026-07-19 16:37:54 -04:00
bbedward 1402d2312a systray: fix menu positioning on vbars
fixes #2818
port 1.5
2026-07-19 16:37:23 -04:00
bbedward d003683689 matugen/vesktop: tweak some colors of unread indicators to follow
primary theme
2026-07-19 14:26:30 -04:00
bbedward 57fd39d84d matugen: add config dirs to detection of presence
related #2894
port 1.5
2026-07-19 12:43:04 -04:00
bbedward 6239e5a8e4 mango: suppress excessive reload messages
fixes #2848
port 1.5
2026-07-19 11:47:39 -04:00
bbedward 4c59d61f89 matugen/vesktop/vencord: replace midnight template with a new more
modest template
2026-07-19 11:46:45 -04:00
bbedward 2a8b88c1f0 matugen: remove padding 0 from gtk light
fixes #2884
port 1.5
2026-07-19 11:37:42 -04:00
bbedward dbaf2e922c theme: put full theme catalog in neovim dropdown
fixes #2892
port 1.5
2026-07-19 11:20:45 -04:00
bbedward eb330f62e1 dash: fix color tokens for placeholder in calendar, and weather failed
fixes #2891
port 1.5
2026-07-19 11:17:47 -04:00
bbedward 90291bd627 freebsd: add initial support for running on FreeBSD, excluding Bluez,
ppd, and some things. Add a simple wpa_supplicant backend
2026-07-19 10:31:48 -04:00
bbedward 069df80b22 core: exclude qmlls.ini from sync-shell 2026-07-18 15:42:21 -04:00
bbedward 2846363f55 powermenu: don't require key release for actions other than suspend and
hibernate

port 1.5
2026-07-18 15:30:31 -04:00
bbedward 0cdb065739 core: migrate to dankgo shared go modules, embed quickshell in DMS
binary to mount at runtime, -c or DMS_SHELL_DIR overrides required to
explicitly override embedded configuration
2026-07-18 14:51:52 -04:00
purian23 2114ece0df feat(bluetooth): built in audio codec support - another dep bites the dust
- Updated the codec logic to utilize WirePlumber, bye pactl
- Improved UI to display media and call codec sections
2026-07-18 14:16:49 -04:00
bbedward 4a90bd3fb1 matugen/neovim: respect terminalsAlwaysDark
port 1.5
2026-07-18 11:52:21 -04:00
bbedward 698b66cf91 lock: add resilience for when the lock surface is rejected
related #2778
related #2841

port 1.5
2026-07-18 11:50:31 -04:00
bbedward 8fc38de0a1 packaging: update fedora dms-git spec 2026-07-18 11:11:18 -04:00
bbedward 235f0668b8 Squashed commit of the following:
commit 990d86d481
Author: bbedward <bbedward@gmail.com>
Date:   Sat Jul 18 10:43:22 2026 -0400

    flake: update-common

commit 526cb157fd
Author: bbedward <bbedward@gmail.com>
Date:   Thu Jul 16 17:56:40 2026 -0400

    i18n: update sync scrirpt for dank-qml-common

commit 92ba96d9f9
Author: bbedward <bbedward@gmail.com>
Date:   Thu Jul 16 09:24:37 2026 -0400

    qs: integrate with dank-qml-common
2026-07-18 10:44:31 -04:00
493 changed files with 28882 additions and 42323 deletions
+56
View File
@@ -0,0 +1,56 @@
name: Sync flake.lock to dank-qml-common
on:
workflow_dispatch:
push:
paths:
- "dank-qml-common"
branches:
- master
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Point flake input at the submodule commit
run: |
set -euo pipefail
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only)
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])")
[ "$submodule_rev" = "$flake_rev" ] && { echo "flake.lock already matches $submodule_rev"; exit 0; }
nix flake lock --override-input dank-qml-common "github:AvengeMedia/dank-qml-common/$submodule_rev"
- name: Commit and push flake.lock update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if git diff --quiet flake.lock; then
echo "No changes to flake.lock"
exit 0
fi
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.lock
git commit -m "nix: sync flake.lock to dank-qml-common submodule"
git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
+34 -1
View File
@@ -27,6 +27,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
@@ -48,8 +50,39 @@ jobs:
- name: Build dms
run: go build -v ./cmd/dms
- name: Build dms (embedded shell)
run: make build
- name: Build dms (distropkg)
run: go build -v -tags distro_binary ./cmd/dms
run: go build -v -tags 'distro_binary withshell' ./cmd/dms
- name: Build dankinstall
run: go build -v ./cmd/dankinstall
build-freebsd:
runs-on: ubuntu-latest
defaults:
run:
working-directory: core
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
- name: Build all packages
env:
GOOS: freebsd
CGO_ENABLED: 0
run: go build -v ./...
- name: Build dms (distropkg, embedded shell)
run: |
make sync-shell
GOOS=freebsd CGO_ENABLED=0 go build -v -tags 'distro_binary withshell' ./cmd/dms
-2
View File
@@ -33,8 +33,6 @@ jobs:
- name: Run NixOS service start test
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
- name: Run greeter niri test
run: nix build .#nixosTests.x86_64-linux.greeter-niri-module -L
- name: Run home-manager module test
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
+2
View File
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
+94 -46
View File
@@ -27,7 +27,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [amd64, arm64]
include:
- { goos: linux, arch: amd64 }
- { goos: linux, arch: arm64 }
- { goos: freebsd, arch: amd64 }
- { goos: freebsd, arch: arm64 }
defaults:
run:
@@ -35,6 +39,8 @@ jobs:
env:
TAG: ${{ inputs.tag }}
# linux assets keep their historical arch-only names
ASSET: ${{ matrix.goos == 'linux' && matrix.arch || format('{0}-{1}', matrix.goos, matrix.arch) }}
steps:
- name: Checkout
@@ -42,6 +48,7 @@ jobs:
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
@@ -49,6 +56,7 @@ jobs:
go-version-file: ./core/go.mod
- name: Format check
if: matrix.goos == 'linux'
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
@@ -57,38 +65,43 @@ jobs:
fi
- name: Run tests
if: matrix.goos == 'linux'
run: go test -v ./...
- name: Build dankinstall (${{ matrix.arch }})
- name: Build dankinstall (${{ env.ASSET }})
if: matrix.goos == 'linux'
env:
GOOS: linux
GOOS: ${{ matrix.goos }}
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ matrix.arch }}
-o ../../dankinstall-${{ env.ASSET }}
cd ../..
gzip -9 -k dankinstall-${{ matrix.arch }}
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
gzip -9 -k dankinstall-${{ env.ASSET }}
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
- name: Build dms (${{ matrix.arch }})
- name: Sync embedded shell
run: make sync-shell
- name: Build dms (${{ env.ASSET }})
env:
GOOS: linux
GOOS: ${{ matrix.goos }}
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ matrix.arch }}
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ env.ASSET }}
cd ../..
gzip -9 -k dms-${{ matrix.arch }}
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
gzip -9 -k dms-${{ env.ASSET }}
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
- name: Generate shell completions
if: matrix.arch == 'amd64'
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
run: |
set -eux
chmod +x dms-amd64
@@ -96,46 +109,58 @@ jobs:
./dms-amd64 completion fish > completion.fish
./dms-amd64 completion zsh > completion.zsh
- name: Build dms-distropkg (${{ matrix.arch }})
- name: Build dms-distropkg (${{ env.ASSET }})
env:
GOOS: linux
GOOS: ${{ matrix.goos }}
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ matrix.arch }}
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ env.ASSET }}
cd ../..
gzip -9 -k dms-distropkg-${{ matrix.arch }}
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
gzip -9 -k dms-distropkg-${{ env.ASSET }}
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
- name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64'
- name: Upload artifacts (${{ env.ASSET }})
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ matrix.arch }}
name: core-assets-${{ env.ASSET }}
path: |
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
core/dankinstall-${{ env.ASSET }}.gz
core/dankinstall-${{ env.ASSET }}.gz.sha256
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts (${{ env.ASSET }}, no installer)
if: matrix.goos != 'linux'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ env.ASSET }}
path: |
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts with completions
if: matrix.arch == 'amd64'
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ matrix.arch }}
name: core-assets-${{ env.ASSET }}
path: |
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
core/dankinstall-${{ env.ASSET }}.gz
core/dankinstall-${{ env.ASSET }}.gz.sha256
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
core/completion.bash
core/completion.fish
core/completion.zsh
@@ -191,11 +216,13 @@ jobs:
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
submodules: recursive
- name: Fetch updated tag after version bump
run: |
git fetch origin --force tag ${TAG}
git checkout ${TAG}
git submodule update --init --recursive
- name: Set up Go
uses: actions/setup-go@v6
@@ -242,16 +269,23 @@ jobs:
### Complete Packages
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
- **`dms-full-freebsd-amd64.tar.gz`** - Complete package for FreeBSD x86_64 systems
- **`dms-full-freebsd-arm64.tar.gz`** - Complete package for FreeBSD ARM64 systems
### Individual Components
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
- **`dms-cli-freebsd-amd64.gz`** - DMS CLI binary for FreeBSD x86_64 systems
- **`dms-cli-freebsd-arm64.gz`** - DMS CLI binary for FreeBSD ARM64 systems
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dms-distropkg-freebsd-amd64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD x86_64 systems
- **`dms-distropkg-freebsd-arm64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD ARM64 systems
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
- **`dms-qml.tar.gz`** - QML source code only
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging)
### Checksums
- **`*.sha256`** - SHA256 checksums for verifying download integrity
@@ -299,6 +333,22 @@ jobs:
# Copy completions
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Replace the DankCommon symlink with real submodule content for packaging
rm quickshell/DankCommon
cp -r dank-qml-common/DankCommon quickshell/DankCommon
# Create full source tarball (GitHub tag archives never contain submodule content)
VERSION_NUM=${TAG#v}
tar --exclude='.git' \
--exclude='.github' \
--exclude='dank-qml-common' \
--exclude='_release_assets' \
--exclude='_core_assets' \
--exclude='RELEASE_BODY.md' \
--transform "s,^\.,DankMaterialShell-${VERSION_NUM},S" \
-czf _release_assets/dms-source.tar.gz .
(cd _release_assets && sha256sum dms-source.tar.gz > dms-source.tar.gz.sha256)
# Create QML source package (exclude build artifacts and git files)
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
@@ -315,8 +365,8 @@ jobs:
# Generate checksum for QML package
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
# Create full packages for each architecture
for arch in amd64 arm64; do
# Create full packages for each os/architecture
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
mkdir -p _temp_full/dms
mkdir -p _temp_full/bin
mkdir -p _temp_full/completions
@@ -359,18 +409,16 @@ jobs:
## Installation Steps
1. **Install quickshell assets:**
```bash
mkdir -p ~/.config/quickshell
cp -r dms ~/.config/quickshell/
```
The Quickshell UI is embedded in the `dms` binary. The bundled `dms/`
tree is optional — pass it with `-c` or `DMS_SHELL_DIR` to run a
modified copy instead of the embedded UI.
2. **Install the DMS CLI binaries:**
1. **Install the DMS CLI binaries:**
```bash
sudo install -m 755 bin/dms /usr/local/bin/dms
```
3. **Install shell completions (optional):**
2. **Install shell completions (optional):**
```bash
# Bash
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
@@ -382,7 +430,7 @@ jobs:
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
```
4. **Start the shell:**
3. **Start the shell:**
```bash
dms run
```
+2 -10
View File
@@ -4,14 +4,12 @@ on:
workflow_dispatch:
inputs:
package:
description: 'Package to build (dms, dms-greeter, or both)'
description: 'Package to build'
required: false
default: 'dms'
type: choice
options:
- dms
- dms-greeter
- both
version:
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
required: false
@@ -31,11 +29,7 @@ jobs:
id: set-packages
run: |
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
if [ "$PACKAGE_INPUT" = "both" ]; then
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
else
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
fi
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
build-and-upload:
needs: determine-packages
@@ -163,8 +157,6 @@ jobs:
PACKAGE="${{ matrix.package }}"
if [ "$PACKAGE" = "dms" ]; then
COPR_PROJECT="avengemedia/dms"
elif [ "$PACKAGE" = "dms-greeter" ]; then
COPR_PROJECT="avengemedia/danklinux"
else
echo "❌ Unknown package: $PACKAGE"
exit 1
+11 -57
View File
@@ -9,7 +9,6 @@ on:
type: choice
options:
- dms
- dms-greeter
- dms-git
- all
default: "dms"
@@ -36,6 +35,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Check for updates
id: check
@@ -74,27 +74,12 @@ jobs:
fi
}
# Helper function to check dms-greeter stable tag
check_dms_greeter_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
# Run from tag with no package specified - update both stable packages
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
# Run from tag with no package specified - update stable package
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
@@ -120,7 +105,7 @@ jobs:
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each stable package and build list of those needing updates
# Check the stable package and build list of those needing updates
PACKAGES_TO_UPDATE=()
if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms")
@@ -128,10 +113,6 @@ jobs:
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
fi
if check_dms_greeter_stable; then
PACKAGES_TO_UPDATE+=("dms-greeter")
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -140,7 +121,7 @@ jobs:
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ Both packages up to date"
echo "✓ All packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
@@ -164,18 +145,6 @@ jobs:
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_dms_greeter_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
@@ -199,6 +168,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Wait before OBS upload
run: sleep 3
@@ -219,9 +189,9 @@ jobs:
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch
# Determine version for dms stable and dms-greeter using the API
# Determine version for dms stable using the API
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
@@ -287,7 +257,7 @@ jobs:
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog"
- name: Update stable version (dms + dms-greeter)
- name: Update stable version (dms)
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
@@ -323,25 +293,12 @@ jobs:
fi
fi
# Update dms-greeter changelog when dms-greeter is in the upload list
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
{
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-greeter/debian/changelog"
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
fi
# Update Debian _service files for packages in upload list (download_url paths)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms, dms-greeter stable)
# Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
@@ -402,7 +359,7 @@ jobs:
UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
# PACKAGES can be a space-separated list (from the "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
echo ""
@@ -505,9 +462,6 @@ jobs:
dms-git)
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
+2 -3
View File
@@ -9,7 +9,6 @@ on:
type: choice
options:
- dms
- dms-greeter
- dms-git
- all
default: "dms"
@@ -36,6 +35,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install dependencies
run: |
@@ -94,6 +94,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
@@ -133,7 +134,6 @@ jobs:
case "$PACKAGE" in
dms) PPA_NAME="dms" ;;
dms-git) PPA_NAME="dms-git" ;;
dms-greeter) PPA_NAME="danklinux" ;;
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
esac
@@ -148,4 +148,3 @@ jobs:
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
echo "- **DankLinux PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> "$GITHUB_STEP_SUMMARY"
+7 -34
View File
@@ -27,11 +27,6 @@ on:
type: boolean
required: false
default: true
build_greeter:
description: 'Build stable dms-greeter package'
type: boolean
required: false
default: true
permissions:
contents: read
@@ -83,6 +78,7 @@ jobs:
with:
ref: ${{ github.event.release.tag_name || (github.event.inputs.version && (startsWith(github.event.inputs.version, 'v') && github.event.inputs.version || format('v{0}', github.event.inputs.version))) || github.ref }}
fetch-depth: 0
submodules: recursive
- name: Record checked-out source commit
run: echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
@@ -105,7 +101,6 @@ jobs:
- name: Inject templates
run: |
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-greeter void-packages/srcpkgs/
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
@@ -179,19 +174,16 @@ jobs:
mkdir -p r2-repo/current
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}"
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
if [ "${{ github.event_name }}" = "schedule" ]; then
BUILD_DMS="false"
BUILD_GREETER="false"
BUILD_GIT="true"
fi
if [ "${{ github.event_name }}" = "release" ]; then
BUILD_DMS="true"
BUILD_GREETER="true"
BUILD_GIT="false"
fi
@@ -216,23 +208,20 @@ jobs:
echo "=== Starting Builds ==="
echo "DMS stable build enabled: $BUILD_DMS"
echo "Greeter stable build enabled: $BUILD_GREETER"
echo "Git build enabled: $BUILD_GIT"
echo "Force rebuild: $FORCE_REBUILD"
cd void-packages
if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then
echo "🔧 Updating stable templates for $ARCHIVE_TAG"
if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then
echo "🔧 Updating stable template for $ARCHIVE_TAG"
TARBALL="$(mktemp)"
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/archive/refs/tags/${ARCHIVE_TAG}.tar.gz"
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
rm -f "$TARBALL"
for pkg in dms dms-greeter; do
sed -i "s/^version=.*/version=${RELEASE_VER}/" "srcpkgs/${pkg}/template"
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" "srcpkgs/${pkg}/template"
done
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
fi
# 1. Build dms-git (development package)
@@ -246,7 +235,7 @@ jobs:
# into $wrksrc (create_wrksrc=yes handles the rest).
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
mkdir -p "$SRC_CACHE"
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
--exclude=void-packages \
--exclude=r2-repo \
--exclude=.git \
@@ -288,22 +277,6 @@ jobs:
fi
fi
# 3. Build stable dms-greeter package
if [ "$BUILD_GREETER" = "true" ]; then
GREETER_VER=$(grep -E '^version=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
GREETER_REV=$(grep -E '^revision=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
EXPECTED_GREETER_FILE="dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps"
if [ -f "../r2-repo/current/$EXPECTED_GREETER_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_GREETER_FILE already exists, skipping build."
else
echo "🔨 Compiling dms-greeter ($GREETER_VER)..."
./xbps-src pkg dms-greeter
rm -f "../r2-repo/current/${EXPECTED_GREETER_FILE}"
cp -L hostdir/binpkgs/dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps ../r2-repo/current/
fi
fi
- name: Index and sign repository
run: |
cd r2-repo/current
+2
View File
@@ -129,3 +129,5 @@ distro/void/masterdir*/
# Often gets built
core/dms
core/internal/shellembed/dist
+4
View File
@@ -0,0 +1,4 @@
[submodule "dank-qml-common"]
path = dank-qml-common
url = https://github.com/AvengeMedia/dank-qml-common.git
branch = master
+8
View File
@@ -1,5 +1,13 @@
This file is more of a quick reference so I know what to account for before next releases.
# Next
- Go core migrated to dankgo (shared log/paths/errdefs/ipc/shellapp)
- Quickshell UI embedded in the dms binary, -c / DMS_SHELL_DIR override - breaking for setups relying on ~/.config/quickshell/dms auto-discovery (XDG search removed)
- release binaries + all -git packages build with withshell; sockets/pidfiles/env names unchanged, IPC APIVersion 28 wire-compatible
- -git packages (AUR dms-shell-git, fedora, opensuse, void, debian, ubuntu) no longer install /usr/share/quickshell/dms; dms greeter install falls back to the embedded UI when the greeter package is present
- ping over the socket returns {"pong":true} instead of "pong"
- startup: embedded UI resolution is keyed by build-time .dankrev (no per-start content hashing)
# 1.5.0
- Overhauled shadows
- App ID changed to com.danklinux.dms - breaking for window rules
+57
View File
@@ -6,6 +6,20 @@ To contribute fork this repository, make your changes, and open a pull request.
## Setup
Clone with submodules — the shared widget library ([dank-qml-common](https://github.com/AvengeMedia/dank-qml-common)) is vendored at `dank-qml-common/` and symlinked into `quickshell/DankCommon`:
```bash
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git
# or, in an existing clone:
git submodule update --init
```
To have `git pull` keep the submodule in sync automatically (moving it to the commit this repo points at, no separate `git submodule update` step), set:
```bash
git config submodule.recurse true
```
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
```bash
@@ -28,6 +42,47 @@ This will provide:
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
## Building and running
The Quickshell UI is embedded into the `dms` binary at build time. `make build` copies `quickshell/` into `core/internal/shellembed/dist/` (generated, never committed) and compiles with the `withshell` tag. `make dev` builds without the tag — that binary carries no UI and requires an explicit config dir.
```bash
make build # embedded binary at core/bin/dms
make dev # untagged development build
make run # dev build, then launch against the live quickshell/ tree
```
The UI config dir resolves in order: `-c <dir>`, `DMS_SHELL_DIR`, the dir a running instance is using, then the embedded UI. Each candidate must contain `shell.qml`. `make run` uses `-c $(pwd)/quickshell`, so QML edits hot-reload from the working tree.
The Go core depends on [dankgo](https://github.com/AvengeMedia/dankgo) for logging, XDG paths, the IPC transport, and the quickshell process lifecycle. To develop against a local dankgo checkout, create a gitignored `go.work` at the repo root:
```
go 1.26.1
use (
./core
../dankgo
)
```
## Shared widgets (dank-qml-common)
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
2. Commit and PR those changes in the `dank-qml-common` repo: `cd dank-qml-common && git switch -c my-change`, push, open the PR there.
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
The submodule URL in `.gitmodules` is HTTPS so CI and anonymous clones keep working. To push over SSH instead of being prompted for credentials, add a push rewrite to your git config — fetches stay HTTPS, pushes use SSH:
```bash
git config --global url."git@github.com:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/"
```
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
## VSCode Setup
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
@@ -104,6 +159,8 @@ Text {
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
+19 -2
View File
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets
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 dev run 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
all: build
@@ -27,6 +27,12 @@ build:
@$(MAKE) -C $(CORE_DIR) build
@echo "Build complete"
dev:
@$(MAKE) -C $(CORE_DIR) dev
run: dev
@$(BUILD_DIR)/$(BINARY_NAME) run -c $(CURDIR)/$(SHELL_DIR)
clean:
@echo "Cleaning build artifacts..."
@$(MAKE) -C $(CORE_DIR) clean
@@ -35,6 +41,12 @@ clean:
lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh
# Pull the latest dank-qml-common and pin it everywhere it is consumed
# (submodule pointer + nix flake input). Commit both in one change.
update-common:
git submodule update --remote --merge dank-qml-common
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
# Installation targets
install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@@ -43,8 +55,9 @@ install-bin:
install-shell:
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
@test -e $(SHELL_DIR)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
@mkdir -p $(SHELL_INSTALL_DIR)
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@echo "Shell files installed"
@@ -59,6 +72,9 @@ install-completions:
@echo "Shell completions installed"
install-systemd:
ifneq ($(shell uname),Linux)
@echo "Skipping systemd user service (non-Linux); start the shell from your compositor config with 'dms run'"
else
@echo "Installing systemd user service..."
@mkdir -p $(SYSTEMD_USER_DIR)
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
@@ -66,6 +82,7 @@ install-systemd:
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
endif
install-icon:
@echo "Installing icon..."
+12 -1
View File
@@ -98,7 +98,7 @@ Notification center with grouping, rich text support, and keyboard navigation.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
**Plugin System**
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
@@ -132,6 +132,17 @@ dms plugins search # Browse plugin registry
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
## Dank Projects
DMS is one piece of the suite. The rest lives in its own repos:
- [dank-greeter](https://github.com/AvengeMedia/dank-greeter) - greetd login screen with the Dank Material aesthetic. The Greeter tab in DMS settings is the front-end for it.
- [dankcalendar](https://github.com/AvengeMedia/dankcalendar) - Local, Google, Microsoft, and CalDAV calendars for the dank desktop.
- [dgop](https://github.com/AvengeMedia/dgop) - System monitoring CLI and API that powers the process list and dashboard widgets.
- [dsearch](https://github.com/AvengeMedia/danksearch) - Fast file search that powers file results in the launcher.
- [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
- [dankgo](https://github.com/AvengeMedia/dankgo) - Common Go modules behind the single binary apps.
## Development
See component-specific documentation:
+50 -26
View File
@@ -3,6 +3,8 @@ BINARY_NAME_INSTALL=dankinstall
SOURCE_DIR=cmd/dms
SOURCE_DIR_INSTALL=cmd/dankinstall
BUILD_DIR=bin
SHELL_SRC=../quickshell
EMBED_DIR=internal/shellembed/dist
PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin
@@ -21,17 +23,46 @@ BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUI
# Architecture to build for dist target (amd64, arm64, or all)
ARCH ?= all
# Target OSes for dist builds
DIST_OSES ?= linux freebsd
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
ifeq ($(ARCH),all)
DIST_ARCHS = amd64 arm64
else
DIST_ARCHS = $(ARCH)
endif
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
# Default target
all: build
# Copy the quickshell UI into the embed dir (gitignored) so tagged builds
# can bake it into the binary. Dev-only files are stripped; scripts/ is kept
# minus its dev entries, since Theme.qml and BluetoothService.qml run
# gtk.sh/qt.sh/bluez-card-profile.lua out of the resolved shell dir.
# tar -h dereferences the DankCommon submodule symlink; go:embed rejects
# symlinks. .qmlls.ini is excluded at copy time: it's a symlink into the
# quickshell runtime VFS and dereferencing it fails whenever the shell isn't
# running.
sync-shell:
@test -e $(SHELL_SRC)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
@rm -rf $(EMBED_DIR)
@mkdir -p $(EMBED_DIR)
@tar -C $(SHELL_SRC) --exclude=.qmlls.ini -chf - . | tar -C $(EMBED_DIR) -xf -
@rm -rf $(EMBED_DIR)/.git* $(EMBED_DIR)/.github
@find $(EMBED_DIR) -type d \( -name .claude -o -name .vscode \) -prune -exec rm -rf {} +
@rm -f $(EMBED_DIR)/AGENTS.md $(EMBED_DIR)/qmlformat-all.sh
@rm -f $(EMBED_DIR)/scripts/i18nsync.py $(EMBED_DIR)/scripts/build-vscode-vsix.sh $(EMBED_DIR)/scripts/qmllint-entrypoints.sh
@rm -f $(EMBED_DIR)/scripts/spam-notifications.sh $(EMBED_DIR)/scripts/verify-notifications.sh
@rm -f $(EMBED_DIR)/translations/*.py $(EMBED_DIR)/translations/WORKFLOW.md
@cd $(EMBED_DIR) && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum | sha256sum | cut -c1-16 > .dankrev
# Build the main binary (dms)
build:
build: sync-shell
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
CGO_ENABLED=0 $(GO) build -tags withshell $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
dankinstall:
@@ -40,26 +71,18 @@ dankinstall:
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
dist:
ifeq ($(ARCH),all)
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
# Build distro binaries (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair
dist: sync-shell
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..."
@mkdir -p $(BUILD_DIR)
@echo "Building for linux/amd64..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
@echo "Building for linux/arm64..."
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
@echo "Distribution builds complete:"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
else
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
@mkdir -p $(BUILD_DIR)
@echo "Building for linux/$(ARCH)..."
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
@echo "Distribution build complete:"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
endif
@for os in $(DIST_OSES); do \
for arch in $(DIST_ARCHS); do \
echo "Building for $$os/$$arch..."; \
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch ./$(SOURCE_DIR) || exit 1; \
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \
done; \
done
@echo "Distribution builds complete"
build-all: build dankinstall
@@ -99,7 +122,7 @@ uninstall-dankinstall:
clean:
@echo "Cleaning build artifacts..."
@rm -rf $(BUILD_DIR)
@rm -rf $(BUILD_DIR) $(EMBED_DIR)
@echo "Clean complete"
test:
@@ -141,10 +164,11 @@ print-version:
help:
@echo "Available targets:"
@echo " all - Build the main binary (dms) (default)"
@echo " build - Build the main binary (dms)"
@echo " build - Build the main binary (dms) with the embedded UI"
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
@echo " dankinstall - Build dankinstall binary"
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
@echo " dist - Build dms for linux/freebsd amd64/arm64 (no update/greeter)"
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow"
@echo " build-all - Build both binaries"
@echo " install - Install dms to $(INSTALL_DIR)"
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
+3 -3
View File
@@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication.
- `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages)
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
- `dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
### Color Picker
@@ -109,7 +109,7 @@ make test # Run tests
**Distribution build:**
```bash
make dist # Build without update/greeter features
make dist # Build without update features
```
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
@@ -170,7 +170,7 @@ sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t ki
| `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
| `--yes` | `-y` | Required for headless mode confirms installation without interactive prompts |
| `--yes` | `-y` | Required for headless mode - confirms installation without interactive prompts |
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
+30 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
@@ -15,7 +16,7 @@ import (
var authCmd = &cobra.Command{
Use: "auth",
Short: "Manage DMS authentication sync",
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
Long: "Manage PAM/authentication setup for the DMS lock screen",
}
var authSyncCmd = &cobra.Command{
@@ -211,3 +212,31 @@ func syncAuthInTerminal(nonInteractive bool) error {
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
return runCommandInTerminal(shellCmd)
}
func runCommandInTerminal(shellCmd string) error {
terminals := []struct {
name string
args []string
}{
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
{"kitty", []string{"bash", "-c", shellCmd}},
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
}
for _, t := range terminals {
if _, err := exec.LookPath(t.name); err != nil {
continue
}
cmd := exec.Command(t.name, t.args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
}
+30 -78
View File
@@ -6,10 +6,10 @@ import (
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
"github.com/spf13/cobra"
)
@@ -19,63 +19,6 @@ var versionCmd = &cobra.Command{
Run: runVersion,
}
var runCmd = &cobra.Command{
Use: "run",
Short: "Launch quickshell with DMS configuration",
Long: "Launch quickshell with DMS configuration (qs -c dms)",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
daemon, _ := cmd.Flags().GetBool("daemon")
session, _ := cmd.Flags().GetBool("session")
if v, _ := cmd.Flags().GetString("log-level"); v != "" {
if err := os.Setenv("DMS_LOG_LEVEL", v); err != nil {
log.Fatalf("Failed to set DMS_LOG_LEVEL: %v", err)
}
}
if v, _ := cmd.Flags().GetString("log-file"); v != "" {
if err := os.Setenv("DMS_LOG_FILE", v); err != nil {
log.Fatalf("Failed to set DMS_LOG_FILE: %v", err)
}
}
log.ApplyEnvOverrides()
config.CleanupStrayHyprlandConfFile(log.Infof)
if daemon {
runShellDaemon(session)
} else {
runShellInteractive(session)
}
},
}
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Restart quickshell with DMS configuration",
Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
restartShell()
},
}
var restartDetachedCmd = &cobra.Command{
Use: "restart-detached <pid>",
Hidden: true,
Args: cobra.ExactArgs(1),
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
runDetachedRestart(args[0])
},
}
var killCmd = &cobra.Command{
Use: "kill",
Short: "Kill running DMS shell processes",
Long: "Kill all running quickshell processes with DMS configuration",
Run: func(cmd *cobra.Command, args []string) {
killShell()
},
}
var ipcCmd = &cobra.Command{
Use: "ipc",
Short: "Send IPC commands to running DMS shell",
@@ -264,29 +207,41 @@ func formatVersion(version string) string {
return fmt.Sprintf("dms %s", version)
}
var baseVersionRe = regexp.MustCompile(`^([\d.]+)`)
// Installed UI trees, for builds without an embedded UI.
var shellVersionPaths = []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
}
func getBaseVersion() string {
paths := []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
if ver := parseBaseVersion(shellembed.Version()); ver != "" {
return ver
}
for _, path := range paths {
if content, err := os.ReadFile(path); err == nil {
ver := strings.TrimSpace(string(content))
ver = strings.TrimPrefix(ver, "v")
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
if matches := re.FindStringSubmatch(ver); matches != nil {
return matches[1]
}
}
for _, path := range shellVersionPaths {
content, err := os.ReadFile(path)
if err != nil {
continue
}
if ver := parseBaseVersion(string(content)); ver != "" {
return ver
}
}
// Fallback
return "1.0.2"
}
func parseBaseVersion(raw string) string {
matches := baseVersionRe.FindStringSubmatch(strings.TrimPrefix(strings.TrimSpace(raw), "v"))
if matches == nil {
return ""
}
return matches[1]
}
func startDebugServer() error {
server.CLIVersion = Version
return server.Start(true)
@@ -743,12 +698,9 @@ func checkAllPluginsCLI() error {
}
func getCommonCommands() []*cobra.Command {
return []*cobra.Command{
commands := shellApp.Commands()
return append(commands, []*cobra.Command{
versionCmd,
runCmd,
restartCmd,
restartDetachedCmd,
killCmd,
ipcCmd,
debugSrvCmd,
pluginsCmd,
@@ -775,5 +727,5 @@ func getCommonCommands() []*cobra.Command {
trashCmd,
systemCmd,
switchUserCmd,
}
}...)
}
+1
View File
@@ -40,6 +40,7 @@ var resolveIncludeCmd = &cobra.Command{
"layout.kdl",
"outputs.kdl",
"binds.kdl",
"input.kdl",
"cursor.conf",
"layout.conf",
"outputs.conf",
+8 -6
View File
@@ -401,12 +401,12 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
}
func getDMSShellVersion() (version, path string) {
if err := findConfig(nil, nil); err == nil && configPath != "" {
versionFile := filepath.Join(configPath, "VERSION")
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
versionFile := filepath.Join(shellApp.ConfigPath(), "VERSION")
if data, err := os.ReadFile(versionFile); err == nil {
return strings.TrimSpace(string(data)), configPath
return strings.TrimSpace(string(data)), shellApp.ConfigPath()
}
return "installed", configPath
return "installed", shellApp.ConfigPath()
}
if dmsPath, err := config.LocateDMSConfig(); err == nil {
@@ -450,8 +450,8 @@ func checkDMSInstallation() []checkResult {
var results []checkResult
dmsPath := ""
if err := findConfig(nil, nil); err == nil && configPath != "" {
dmsPath = configPath
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
dmsPath = shellApp.ConfigPath()
} else if path, err := config.LocateDMSConfig(); err == nil {
dmsPath = path
}
@@ -833,6 +833,8 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
return "systemd-networkd"
case network.BackendConnMan:
return "ConnMan"
case network.BackendWpaSupplicant:
return "wpa_supplicant"
default:
return ""
}
+22 -3
View File
@@ -26,7 +26,7 @@ var updateCmd = &cobra.Command{
Use: "update",
Short: "Update DankMaterialShell to the latest version",
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
PreRunE: findConfig,
PreRunE: shellApp.ResolveConfig,
Run: func(cmd *cobra.Command, args []string) {
runUpdate()
},
@@ -98,7 +98,7 @@ func runUpdate() {
}
log.Info("Update complete! Restarting DMS...")
restartShell()
shellApp.Restart()
}
func updateArchLinux() error {
@@ -250,6 +250,10 @@ func updateOtherDistros() error {
return fmt.Errorf("failed to fetch changes: %w", err)
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
if currentTag != "" {
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
latestTagOutput, err := latestTagCmd.Output()
@@ -291,6 +295,10 @@ func updateOtherDistros() error {
return fmt.Errorf("update cancelled")
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
return nil
}
@@ -320,10 +328,21 @@ func updateOtherDistros() error {
return fmt.Errorf("update cancelled")
}
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
fmt.Println("\nUpdate complete!")
return nil
}
func updateSubmodules() error {
submoduleCmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
submoduleCmd.Stdout = os.Stdout
submoduleCmd.Stderr = os.Stderr
return submoduleCmd.Run()
}
func offerReclone(dmsPath string) bool {
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
reader := bufio.NewReader(os.Stdin)
@@ -342,7 +361,7 @@ func offerReclone(dmsPath string) bool {
}
fmt.Println("Cloning fresh copy...")
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
if err := cloneCmd.Run(); err != nil {
File diff suppressed because it is too large Load Diff
-150
View File
@@ -1,150 +0,0 @@
package main
import (
"errors"
"reflect"
"strings"
"testing"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
"github.com/spf13/cobra"
)
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
origGreeterConfigSyncFn := greeterConfigSyncFn
origSharedAuthSyncFn := sharedAuthSyncFn
t.Cleanup(func() {
greeterConfigSyncFn = origGreeterConfigSyncFn
sharedAuthSyncFn = origSharedAuthSyncFn
})
var calls []string
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
if dmsPath != "/tmp/dms" {
t.Fatalf("unexpected dmsPath %q", dmsPath)
}
if compositor != "niri" {
t.Fatalf("unexpected compositor %q", compositor)
}
if sudoPassword != "" {
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
}
calls = append(calls, "configs")
return nil
}
var gotOptions sharedpam.SyncAuthOptions
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
if sudoPassword != "" {
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
}
gotOptions = options
calls = append(calls, "auth")
return nil
}
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
ForceGreeterAuth: true,
}, func() {
calls = append(calls, "before-auth")
})
if err != nil {
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
}
wantCalls := []string{"configs", "before-auth", "auth"}
if !reflect.DeepEqual(calls, wantCalls) {
t.Fatalf("call order = %v, want %v", calls, wantCalls)
}
if !gotOptions.ForceGreeterAuth {
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
}
}
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
origGreeterConfigSyncFn := greeterConfigSyncFn
origSharedAuthSyncFn := sharedAuthSyncFn
t.Cleanup(func() {
greeterConfigSyncFn = origGreeterConfigSyncFn
sharedAuthSyncFn = origSharedAuthSyncFn
})
greeterConfigSyncFn = func(string, string, func(string), string) error {
return errors.New("config sync failed")
}
authCalled := false
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
authCalled = true
return nil
}
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
if err == nil || err.Error() != "config sync failed" {
t.Fatalf("expected config sync error, got %v", err)
}
if authCalled {
t.Fatal("expected auth sync not to run after config sync failure")
}
}
func TestGreeterStatusStateDirUsesNixOSDefault(t *testing.T) {
if got := greeterStatusStateDir("", true); got != nixOSGreeterStateDir {
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, nixOSGreeterStateDir)
}
}
func TestGreeterStatusStateDirHonorsExplicitOverrideOnNixOS(t *testing.T) {
command := "dms-greeter --cache-dir /srv/dms-greeter --command niri"
if got := greeterStatusStateDir(command, true); got != "/srv/dms-greeter" {
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, "/srv/dms-greeter")
}
}
func TestExtractGreeterWrapperFromEnvCommand(t *testing.T) {
command := "env LIBSEAT_BACKEND=logind DMS_VOID=1 /usr/bin/dms-greeter --command niri"
if got := extractGreeterWrapperFromCommand(command); got != "/usr/bin/dms-greeter" {
t.Fatalf("extractGreeterWrapperFromCommand() = %q, want %q", got, "/usr/bin/dms-greeter")
}
}
func TestRejectNixOSGreeterMutationBlocksImperativeCommands(t *testing.T) {
origGreeterIsNixOSFn := greeterIsNixOSFn
greeterIsNixOSFn = func() bool { return true }
t.Cleanup(func() {
greeterIsNixOSFn = origGreeterIsNixOSFn
})
for _, commandName := range []string{"install", "enable", "sync", "uninstall"} {
t.Run(commandName, func(t *testing.T) {
root := &cobra.Command{Use: "dms"}
greeterCommand := &cobra.Command{Use: "greeter"}
mutationCommand := &cobra.Command{Use: commandName}
root.AddCommand(greeterCommand)
greeterCommand.AddCommand(mutationCommand)
err := rejectNixOSGreeterMutation(mutationCommand)
if err == nil {
t.Fatalf("expected NixOS greeter %s to be rejected", commandName)
}
if !strings.Contains(err.Error(), "dms greeter "+commandName+" is disabled on NixOS") {
t.Fatalf("unexpected error: %v", err)
}
if strings.Contains(err.Error(), "/var/cache/dms-greeter") {
t.Fatalf("NixOS remediation should not recommend the non-NixOS cache path: %v", err)
}
})
}
}
func TestRejectNixOSGreeterMutationAllowsOtherDistros(t *testing.T) {
origGreeterIsNixOSFn := greeterIsNixOSFn
greeterIsNixOSFn = func() bool { return false }
t.Cleanup(func() {
greeterIsNixOSFn = origGreeterIsNixOSFn
})
if err := rejectNixOSGreeterMutation(&cobra.Command{Use: "sync"}); err != nil {
t.Fatalf("expected non-NixOS greeter command to be allowed, got %v", err)
}
}
+1 -60
View File
@@ -1,19 +1,9 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var customConfigPath string
var configPath string
var rootCmd = &cobra.Command{
Use: "dms",
Short: "dms CLI",
@@ -21,54 +11,5 @@ var rootCmd = &cobra.Command{
}
func init() {
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
}
func findConfig(cmd *cobra.Command, args []string) error {
if customConfigPath != "" {
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
shellPath := filepath.Join(customConfigPath, "shell.qml")
info, statErr := os.Stat(shellPath)
if statErr == nil && !info.IsDir() {
configPath = customConfigPath
log.Debug("Using config from: %s", configPath)
return nil
}
if statErr != nil {
return fmt.Errorf("custom config path error: %w", statErr)
}
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
}
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
if len(getAllDMSPIDs()) == 0 {
os.Remove(configStateFile)
} else {
statePath := strings.TrimSpace(string(data))
shellPath := filepath.Join(statePath, "shell.qml")
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
log.Debug("Using config from active session state file: %s", statePath)
configPath = statePath
log.Debug("Using config from: %s", configPath)
return nil
}
os.Remove(configStateFile)
}
}
log.Debug("No custom path or active session, searching default XDG locations...")
var err error
configPath, err = config.LocateDMSConfig()
if err != nil {
return err
}
log.Debug("Using config from: %s", configPath)
return nil
rootCmd.PersistentFlags().StringVarP(shellApp.CustomConfigVar(), "config", "c", "", "Path to a UI config dir (containing shell.qml) to use instead of the embedded UI (env: DMS_SHELL_DIR)")
}
+3 -3
View File
@@ -231,16 +231,16 @@ func setPopoutScreenshotMode(begin bool) {
fn = "begin"
}
cmdArgs := []string{"ipc"}
if pid, ok := getFirstDMSPID(); ok {
if pid, ok := shellApp.SessionPID(); ok {
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
} else {
if err := findConfig(nil, nil); err != nil {
if err := shellApp.ResolveConfig(nil, nil); err != nil {
return
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
}
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
_ = exec.Command("qs", cmdArgs...).Run()
+23 -6
View File
@@ -11,7 +11,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
@@ -144,6 +143,10 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
niriFile: "alttab.kdl",
niriContent: func(_ string) string { return config.NiriAlttabConfig },
},
"input": {
niriFile: "input.kdl",
niriContent: func(_ string) string { return config.NiriInputConfig },
},
"outputs": {
niriFile: "outputs.kdl",
hyprFile: "outputs.lua",
@@ -205,7 +208,12 @@ func detectTerminal() (string, error) {
}
func detectCompositorForSetup() (string, error) {
compositors := greeter.DetectCompositors()
var compositors []string
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
if utils.CommandExists(candidate) {
compositors = append(compositors, candidate)
}
}
switch len(compositors) {
case 0:
@@ -214,11 +222,20 @@ func detectCompositorForSetup() (string, error) {
return strings.ToLower(compositors[0]), nil
}
selected, err := greeter.PromptCompositorChoice(compositors)
if err != nil {
return "", err
fmt.Println("Multiple compositors detected:")
for i, compositor := range compositors {
fmt.Printf("%d) %s\n", i+1, compositor)
}
return strings.ToLower(selected), nil
fmt.Printf("\nChoice (1-%d): ", len(compositors))
var response string
fmt.Scanln(&response)
choice := 0
fmt.Sscanf(strings.TrimSpace(response), "%d", &choice)
if choice < 1 || choice > len(compositors) {
return "", fmt.Errorf("invalid choice")
}
return strings.ToLower(compositors[choice-1]), nil
}
func runSetupDmsConfig(name string) error {
-137
View File
@@ -1,137 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
// runit (Void Linux) service helpers. Services live in /etc/sv and are "enabled"
// by symlinking them into the /var/service supervision dir, so the greeter
// commands branch on isRunit() instead of shelling systemctl.
const (
runitSvDir = "/etc/sv"
runitServiceDir = "/var/service"
)
// isRunit reports whether this system is supervised by runit (Void Linux).
func isRunit() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func runitServiceInstalled(name string) bool {
fi, err := os.Stat(runitSvDir + "/" + name)
return err == nil && fi.IsDir()
}
func runitServiceEnabled(name string) bool {
_, err := os.Lstat(runitServiceDir + "/" + name)
return err == nil
}
// enableRunitService links a service into /var/service (idempotent).
func enableRunitService(name string) error {
if !runitServiceInstalled(name) {
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
}
if runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), "", "ln", "-sf",
runitSvDir+"/"+name, runitServiceDir+"/"+name)
}
// disableRunitService removes a service's supervision symlink.
func disableRunitService(name string) error {
if !runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), "", "rm", "-f",
runitServiceDir+"/"+name)
}
// ensureRunitSeat sets up the seat access a Wayland greeter needs on runit (the
// equivalent of logind on systemd): enables seatd and adds the greeter user to
// the seat/video/input groups. Failures are reported but non-fatal.
func ensureRunitSeat(greeterUser string) {
if runitServiceInstalled("seatd") {
if err := enableRunitService("seatd"); err != nil {
fmt.Printf(" ⚠ could not enable seatd: %v\n", err)
} else {
fmt.Println(" ✓ seatd enabled")
}
} else {
fmt.Println(" ⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
}
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
fmt.Printf(" ⚠ could not add %s to seat groups: %v\n", greeterUser, err)
} else {
fmt.Printf(" ✓ %s added to seat groups (_seatd, video, input)\n", greeterUser)
}
}
// ensureVoidLogindGreeter configures the elogind-backed greeter on Void.
func ensureVoidLogindGreeter(greeterUser string) {
for _, service := range []string{"dbus", "elogind"} {
if err := enableRunitService(service); err != nil {
fmt.Printf(" ⚠ could not enable %s: %v\n", service, err)
} else {
fmt.Printf(" ✓ %s enabled\n", service)
}
}
greeter.EnsureVoidGreetdRunScript(func(msg string) { fmt.Println(" " + msg) }, "")
if runitServiceEnabled("seatd") {
if err := disableRunitService("seatd"); err != nil {
fmt.Printf(" ⚠ could not disable seatd: %v\n", err)
} else {
fmt.Println(" ✓ seatd disabled (elogind manages the seat)")
}
}
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "video,input", greeterUser); err != nil {
fmt.Printf(" ⚠ could not add %s to video/input groups: %v\n", greeterUser, err)
} else {
fmt.Printf(" ✓ %s added to video/input groups (elogind manages the seat)\n", greeterUser)
}
}
// ensureGreetdPamRundir provides XDG_RUNTIME_DIR to runit greeter sessions.
func ensureGreetdPamRundir() {
const pamPath = "/etc/pam.d/greetd"
data, err := os.ReadFile(pamPath)
if err != nil {
fmt.Printf(" ⚠ could not read %s: %v\n", pamPath, err)
return
}
if strings.Contains(string(data), "pam_rundir") {
return
}
line := "session optional pam_rundir.so"
if err := privesc.Run(context.Background(), "", "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
fmt.Printf(" ⚠ could not add pam_rundir to %s: %v\n", pamPath, err)
return
}
fmt.Println(" ✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
}
// startGreeterHint returns the init-appropriate "start greetd now" command.
func startGreeterHint() string {
if isRunit() {
return " sudo sv up greetd"
}
return " sudo systemctl start greetd"
}
-8
View File
@@ -12,14 +12,6 @@ import (
var Version = "dev"
func init() {
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("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd)
-8
View File
@@ -12,14 +12,6 @@ import (
var Version = "dev"
func init() {
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("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
+2 -5
View File
@@ -101,11 +101,8 @@ func getServerSocketPath() string {
runtimeDir = os.TempDir()
}
if parentPID, ok := sessionParentPID(os.Getenv("WAYLAND_DISPLAY")); ok {
sessionSock := filepath.Join(runtimeDir, fmt.Sprintf("danklinux-%d.sock", parentPID))
if _, err := os.Stat(sessionSock); err == nil {
return sessionSock
}
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
return sessionSock
}
entries, err := os.ReadDir(runtimeDir)
+6 -696
View File
@@ -4,591 +4,20 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
)
type ipcTargets map[string]map[string][]string
// getProcessExitCode returns the exit code from a ProcessState.
// For normal exits, returns the exit code directly.
// For signal termination, returns 128 + signal number (Unix convention).
func getProcessExitCode(state *os.ProcessState) int {
if state == nil {
return 1
}
if code := state.ExitCode(); code != -1 {
return code
}
// Process was killed by signal - extract signal number
if status, ok := state.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
return 128 + int(status.Signal())
}
}
return 1
}
var isSessionManaged bool
func execDetachedRestart(targetPID int) {
selfPath, err := os.Executable()
if err != nil {
return
}
cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
cmd.Start()
}
func runDetachedRestart(targetPIDStr string) {
targetPID, err := strconv.Atoi(targetPIDStr)
if err != nil {
return
}
time.Sleep(200 * time.Millisecond)
proc, err := os.FindProcess(targetPID)
if err == nil {
proc.Signal(syscall.SIGTERM)
}
time.Sleep(500 * time.Millisecond)
killShell()
runShellDaemon(false)
}
func getRuntimeDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
return runtime
}
return os.TempDir()
}
func appendLogEnv(env []string) []string {
if v := os.Getenv("DMS_LOG_LEVEL"); v != "" {
env = append(env, "DMS_LOG_LEVEL="+v)
}
if v := os.Getenv("DMS_LOG_FILE"); v != "" {
env = append(env, "DMS_LOG_FILE="+v)
}
return env
}
func withDMSExecutable(env []string) []string {
selfPath, err := os.Executable()
if err != nil {
return env
}
return append(env, "DMS_EXECUTABLE="+selfPath)
}
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
}
func getSessionFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.session", os.Getpid()))
}
func writePIDFile(childPID int) error {
pidFile := getPIDFilePath()
if display := os.Getenv("WAYLAND_DISPLAY"); display != "" {
if err := os.WriteFile(getSessionFilePath(), []byte(display), 0o644); err != nil {
log.Warnf("Failed to write session file: %v", err)
}
}
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
}
func removePIDFile() {
os.Remove(getPIDFilePath())
os.Remove(getSessionFilePath())
}
func getAllDMSPIDs() []int {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var pids []int
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
pidFile := filepath.Join(dir, entry.Name())
data, err := os.ReadFile(pidFile)
if err != nil {
continue
}
childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
os.Remove(pidFile)
continue
}
proc, err := os.FindProcess(childPID)
if err != nil {
os.Remove(pidFile)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
os.Remove(pidFile)
continue
}
pids = append(pids, childPID)
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
if parentProc, err := os.FindProcess(parentPID); err == nil {
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
pids = append(pids, parentPID)
}
}
}
}
return pids
}
func runShellInteractive(session bool) {
isSessionManaged = session
go printASCII()
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
errChan := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
}()
ensureFontCache()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = withDMSExecutable(append(os.Environ(), "DMS_SOCKET="+socketPath))
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
cmd.Env = append(cmd.Env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
cmd.Env = appendLogEnv(cmd.Env)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
tracker := &stderrTracker{parent: os.Stderr}
cmd.Stderr = tracker
startTime := time.Now()
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting quickshell: %v", err)
}
// Write PID file for the quickshell child process
if err := writePIDFile(cmd.Process.Pid); err != nil {
log.Warnf("Failed to write PID file: %v", err)
}
defer removePIDFile()
defer func() {
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
if err := cmd.Wait(); err != nil {
errChan <- fmt.Errorf("quickshell exited: %w", err)
} else {
errChan <- fmt.Errorf("quickshell exited")
}
}()
for {
select {
case sig := <-sigChan:
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
return
}
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
case <-time.After(500 * time.Millisecond):
}
log.Infof("\nReceived signal %v, shutting down...", sig)
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
return
case err := <-errChan:
log.Error(err)
cancel()
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
}
}
}
func restartShell() {
pids := getAllDMSPIDs()
if len(pids) == 0 {
log.Info("No running DMS shell instances found. Starting daemon...")
runShellDaemon(false)
return
}
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("Error finding process %d: %v", pid, err)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
if err := proc.Signal(syscall.SIGUSR1); err != nil {
log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
} else {
log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
}
}
}
func killShell() {
pids := getAllDMSPIDs()
if len(pids) == 0 {
log.Info("No running DMS shell instances found.")
return
}
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("Error finding process %d: %v", pid, err)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
if err := proc.Kill(); err != nil {
log.Errorf("Error killing process %d: %v", pid, err)
} else {
log.Infof("Killed DMS process with PID %d", pid)
}
}
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") {
continue
}
if strings.HasSuffix(entry.Name(), ".pid") || strings.HasSuffix(entry.Name(), ".session") {
os.Remove(filepath.Join(dir, entry.Name()))
}
}
}
func runShellDaemon(session bool) {
isSessionManaged = session
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
if !isDaemonChild {
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
cmd.Env = os.Environ()
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
return
}
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
errChan := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
}()
ensureFontCache()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = withDMSExecutable(append(os.Environ(), "DMS_SOCKET="+socketPath))
if os.Getenv("QT_LOGGING_RULES") == "" {
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
}
// ! 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() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
}
}
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
}
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
cmd.Env = append(cmd.Env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
cmd.Env = appendLogEnv(cmd.Env)
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
log.Fatalf("Error opening /dev/null: %v", err)
}
defer devNull.Close()
cmd.Stdin = devNull
cmd.Stdout = devNull
tracker := &stderrTracker{parent: devNull}
cmd.Stderr = tracker
startTime := time.Now()
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
// Write PID file for the quickshell child process
if err := writePIDFile(cmd.Process.Pid); err != nil {
log.Warnf("Failed to write PID file: %v", err)
}
defer removePIDFile()
defer func() {
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
if err := cmd.Wait(); err != nil {
errChan <- fmt.Errorf("quickshell exited: %w", err)
} else {
errChan <- fmt.Errorf("quickshell exited")
}
}()
for {
select {
case sig := <-sigChan:
if sig == syscall.SIGUSR1 {
if isSessionManaged {
log.Infof("Received SIGUSR1, exiting for systemd restart...")
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
os.Exit(1)
}
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
return
}
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
select {
case <-errChan:
cancel()
os.Remove(socketPath)
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
case <-time.After(500 * time.Millisecond):
}
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
return
case <-errChan:
cancel()
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
}
}
}
var qsHasAnyDisplay = sync.OnceValue(func() bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
@@ -632,17 +61,17 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
func buildQsIPCBaseArgs() ([]string, error) {
cmdArgs := []string{"ipc"}
switch pid, ok := getSessionDMSPID(); {
switch pid, ok := shellApp.SessionPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
if err := shellApp.ResolveConfig(nil, nil); err != nil {
return nil, err
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
}
return cmdArgs, nil
}
@@ -699,101 +128,6 @@ func getShellIPCCompletions(args []string, _ string) []string {
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 sessionParentPID(display string) (int, bool) {
if display == "" {
return 0, false
}
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
for _, entry := range entries {
name := entry.Name()
if !strings.HasPrefix(name, "danklinux-") || !strings.HasSuffix(name, ".session") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, name))
if err != nil || strings.TrimSpace(string(data)) != display {
continue
}
parentStr := strings.TrimSuffix(strings.TrimPrefix(name, "danklinux-"), ".session")
parentPID, err := strconv.Atoi(parentStr)
if err != nil {
continue
}
return parentPID, true
}
return 0, false
}
func getSessionDMSPID() (int, bool) {
parentPID, ok := sessionParentPID(os.Getenv("WAYLAND_DISPLAY"))
if !ok {
return getFirstDMSPID()
}
data, err := os.ReadFile(filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", parentPID)))
if err != nil {
return getFirstDMSPID()
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
return getFirstDMSPID()
}
proc, err := os.FindProcess(pid)
if err != nil || proc.Signal(syscall.Signal(0)) != nil {
return getFirstDMSPID()
}
return pid, true
}
func runShellIPCCommand(args []string) {
if len(args) == 0 {
printIPCHelp()
@@ -946,36 +280,12 @@ func rebuildFontCache() {
}
}
type stderrTracker struct {
mu sync.Mutex
buf strings.Builder
parent io.Writer
}
func (s *stderrTracker) Write(p []byte) (n int, err error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.buf.Len() < 8192 {
s.buf.Write(p)
}
if s.parent != nil {
return s.parent.Write(p)
}
return len(p), nil
}
func (s *stderrTracker) String() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.buf.String()
}
// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
func logStartupFailure(startTime time.Time, exitCode int, tracker *stderrTracker) {
if time.Since(startTime) >= 5*time.Second || exitCode == 0 || exitCode > 128 {
func logStartupFailure(exitCode int, uptime time.Duration, stderrTail string) {
if uptime >= 5*time.Second || exitCode == 0 || exitCode > 128 {
return
}
if containsFontCrashSignature(tracker.String()) {
if containsFontCrashSignature(stderrTail) {
log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
} else {
log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"context"
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
"github.com/AvengeMedia/dankgo/shellapp"
)
var shellApp = shellapp.New(shellapp.Config{
ID: "danklinux",
EnvPrefix: "DMS",
QSAppID: "com.danklinux.dms",
Version: Version,
Embedded: embeddedShell{},
Boot: bootBackend,
PreLaunch: preLaunch,
ExtraEnv: dmsExtraEnv,
OnUIExit: logStartupFailure,
})
type embeddedShell struct{}
func (embeddedShell) Available() bool { return shellembed.Available() }
func (embeddedShell) Extract(baseDir string) (string, error) { return shellembed.Extract(baseDir) }
func (embeddedShell) Prune(baseDir, keep string) { shellembed.Prune(baseDir, keep) }
type dmsBackend struct {
srv *server.Server
done chan error
}
func (b *dmsBackend) SocketPath() string { return b.srv.SocketPath() }
func (b *dmsBackend) Close() { b.srv.Close() }
func (b *dmsBackend) Done() <-chan error { return b.done }
func bootBackend(ctx context.Context) (shellapp.Backend, error) {
config.CleanupStrayHyprlandConfFile(log.Infof)
server.CLIVersion = Version
srv := server.New()
if err := srv.Listen(); err != nil {
return nil, err
}
backend := &dmsBackend{srv: srv, done: make(chan error, 1)}
go func() {
defer func() {
if r := recover(); r != nil {
backend.done <- fmt.Errorf("server panic: %v", r)
}
}()
backend.done <- srv.Serve(false)
}()
return backend, nil
}
func preLaunch() {
go printASCII()
ensureFontCache()
}
func dmsExtraEnv(string) []string {
var env []string
if selfPath, err := os.Executable(); err == nil {
env = append(env, "DMS_EXECUTABLE="+selfPath)
}
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
return env
}
-61
View File
@@ -1,9 +1,7 @@
package main
import (
"fmt"
"os/exec"
"slices"
"strings"
)
@@ -28,62 +26,3 @@ func isArchPackageInstalled(packageName string) bool {
err := cmd.Run()
return err == nil
}
type systemdServiceState struct {
Name string
EnabledState string
NeedsDisable bool
Exists bool
}
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
cmd := exec.Command("systemctl", "is-enabled", serviceName)
output, err := cmd.Output()
stateStr := strings.TrimSpace(string(output))
if err != nil {
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
isKnownState := slices.Contains(knownStates, stateStr)
if !isKnownState {
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
}
}
shouldDisable := false
switch stateStr {
case "enabled", "enabled-runtime", "static", "indirect", "alias":
shouldDisable = true
case "disabled", "masked", "masked-runtime", "not-found":
shouldDisable = false
default:
shouldDisable = true
}
return stateStr, shouldDisable, nil
}
func getSystemdServiceState(serviceName string) (*systemdServiceState, error) {
state := &systemdServiceState{
Name: serviceName,
Exists: false,
}
enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName)
if err != nil {
return nil, fmt.Errorf("failed to check enabled state: %w", err)
}
state.EnabledState = enabledState
state.NeedsDisable = needsDisable
if enabledState == "not-found" {
state.Exists = false
return state, nil
}
state.Exists = true
return state, nil
}
+27 -27
View File
@@ -1,14 +1,13 @@
module github.com/AvengeMedia/DankMaterialShell/core
go 1.26.1
go 1.26.4
require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.24.1
github.com/alecthomas/chroma/v2 v2.27.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0
github.com/fsnotify/fsnotify v1.10.1
github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847
@@ -18,12 +17,12 @@ require (
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.8.2
github.com/yuin/goldmark v1.8.4
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3
go.etcd.io/bbolt v1.5.0
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
golang.org/x/image v0.39.0
tailscale.com v1.96.5
golang.org/x/image v0.44.0
tailscale.com v1.100.0
)
require (
@@ -31,28 +30,28 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/charmbracelet/log v1.0.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/cloudflare/circl v1.6.4 // indirect
github.com/coder/websocket v1.8.15 // indirect
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect
github.com/dlclark/regexp2/v2 v2.5.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 // indirect
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 // indirect
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 // indirect
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mdlayher/netlink v1.11.1 // indirect
github.com/mdlayher/socket v0.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/mdlayher/netlink v1.11.2 // indirect
github.com/mdlayher/socket v0.6.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -62,14 +61,15 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
)
require (
github.com/AvengeMedia/dankgo v0.0.0-20260724133713-a4ef23371e05
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
@@ -79,22 +79,22 @@ require (
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-alpha.2
github.com/go-git/go-git/v6 v6.0.0-alpha.4
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0
github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-isatty v0.0.23
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0
gopkg.in/yaml.v3 v3.0.1
)
+56 -56
View File
@@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AvengeMedia/dankgo v0.0.0-20260724133713-a4ef23371e05 h1:Ij/yzOT8y2HL7V5Rkec1GxJV0rUvhKqfAzyGxVRTk1o=
github.com/AvengeMedia/dankgo v0.0.0-20260724133713-a4ef23371e05/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
@@ -11,8 +13,8 @@ github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukD
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -48,25 +50,23 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20=
github.com/dlclark/regexp2/v2 v2.5.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -85,14 +85,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 h1:wH21vHuv323v9x78JNFNJ6P7HEAsdwr9yq2k9/o4zEE=
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU=
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc=
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0/go.mod h1:1Lr7/vYEYyl6Ir9Ku0tKrCIRreM5zovv0Jdx2MPSM4s=
github.com/go-git/go-git/v6 v6.0.0-alpha.2 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI=
github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak=
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 h1:2WmHkJINIjgXXYDGik8d3oJvFA3DAwPy00csDJ3vo+o=
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ=
github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E=
github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1 h1:gmqi2jvsreu0s8JMLylYDFq4sbjHwwlhktMw0DUg3mA=
github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrOXjo/0EnvRZx6D++I86UYjPgAQ=
github.com/go-git/go-git/v6 v6.0.0-alpha.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ=
github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -117,8 +117,8 @@ github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5G
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -128,16 +128,16 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mdlayher/netlink v1.11.1 h1:T136gDS6Gkt+hLncaBwKdW5GpEC8Z0ykqimOebVoal0=
github.com/mdlayher/netlink v1.11.1/go.mod h1:ao4LjamyK4Uq9L8+fQzqFYpAncbeCdwbvd9Edv/pYnc=
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -180,8 +180,8 @@ 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/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@@ -193,36 +193,36 @@ github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR
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.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g=
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
@@ -235,5 +235,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
+1
View File
@@ -264,6 +264,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
{"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"input.kdl", NiriInputConfig},
{"outputs.kdl", ""},
{"cursor.kdl", ""},
{"windowrules.kdl", ""},
@@ -1,17 +0,0 @@
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
@@ -0,0 +1,15 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
input {
mouse {
accel-speed 0.0
}
touchpad {
tap
accel-speed 0.0
natural-scroll
}
}
+2 -25
View File
@@ -34,31 +34,6 @@ input {
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
@@ -77,6 +52,7 @@ input {
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
@@ -271,3 +247,4 @@ include optional=true "dms/alttab.kdl"
include optional=true "dms/binds.kdl"
include optional=true "dms/outputs.kdl"
include optional=true "dms/cursor.kdl"
include optional=true "dms/input.kdl"
+2 -2
View File
@@ -17,5 +17,5 @@ var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl
var NiriBindsConfig string
//go:embed embedded/niri-greeter.kdl
var NiriGreeterConfig string
//go:embed embedded/niri-input.kdl
var NiriInputConfig string
+15 -2
View File
@@ -520,7 +520,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
Progress: 0.90,
Step: "Cloning DankMaterialShell...",
IsComplete: false,
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
CommandInfo: "git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git",
}
configDir := filepath.Dir(dmsPath)
@@ -528,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return fmt.Errorf("failed to create quickshell config directory: %w", err)
}
cloneCmd := exec.CommandContext(ctx, "git", "clone",
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recurse-submodules",
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
@@ -553,6 +553,8 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
m.syncDMSSubmodules(ctx, dmsPath)
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
m.log("DankMaterialShell cloned successfully")
return nil
@@ -591,6 +593,8 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
m.syncDMSSubmodules(ctx, dmsPath)
m.log("DankMaterialShell updated successfully (git variant)")
return nil
}
@@ -609,10 +613,19 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
m.syncDMSSubmodules(ctx, dmsPath)
m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
return nil
}
func (m *ManualPackageInstaller) syncDMSSubmodules(ctx context.Context, dmsPath string) {
submoduleCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "submodule", "update", "--init", "--recursive")
if err := submoduleCmd.Run(); err != nil {
m.logError("Failed to update submodules", err)
}
}
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing xwayland-satellite from source...")
+13 -15
View File
@@ -1,9 +1,15 @@
package errdefs
type ErrorType int
import (
dankerrdefs "github.com/AvengeMedia/dankgo/errdefs"
)
type ErrorType = dankerrdefs.ErrorType
type CustomError = dankerrdefs.CustomError
const (
ErrTypeNotLinux ErrorType = iota
ErrTypeNotLinux ErrorType = dankerrdefs.AppErrorBase + iota
ErrTypeInvalidArchitecture
ErrTypeUnsupportedDistribution
ErrTypeUnsupportedVersion
@@ -22,20 +28,8 @@ const (
ErrTypeGeneric
)
type CustomError struct {
Type ErrorType
Message string
}
func (e *CustomError) Error() string {
return e.Message
}
func NewCustomError(errType ErrorType, message string) error {
return &CustomError{
Type: errType,
Message: message,
}
return dankerrdefs.NewCustomError(errType, message)
}
const (
@@ -47,6 +41,10 @@ const (
ErrWifiDisabled = "wifi-disabled"
ErrAlreadyConnected = "already-connected"
ErrConnectionFailed = "connection-failed"
ErrHotspotIPConfigFailed = "hotspot-ip-config-failed"
ErrHotspotSupplicantFailed = "hotspot-supplicant-failed"
ErrHotspotFailed = "hotspot-failed"
)
var (
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/dbusutil"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/godbus/dbus/v5"
)
@@ -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
-350
View File
@@ -1,350 +0,0 @@
package greeter
import (
"os"
"path/filepath"
"strings"
"testing"
)
func writeTestFile(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()
writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
writeTestFile(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)
}
})
}
}
func TestUpsertInitialSession(t *testing.T) {
t.Parallel()
baseConfig := `[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command niri"
`
t.Run("inserts initial session", func(t *testing.T) {
t.Parallel()
got := upsertInitialSession(baseConfig, "alice", "/var/cache/dms-greeter", true)
if !strings.Contains(got, "[initial_session]") {
t.Fatalf("expected [initial_session] section, got:\n%s", got)
}
if !strings.Contains(got, `user = "alice"`) {
t.Fatalf("expected alice user in initial session, got:\n%s", got)
}
if !strings.Contains(got, `dms greeter launch-session --from-memory --cache-dir`) {
t.Fatalf("expected stable launch-session command, got:\n%s", got)
}
if strings.Contains(got, `exec niri`) {
t.Fatalf("initial session must not bake the desktop Exec command, got:\n%s", got)
}
})
t.Run("updates existing initial session", func(t *testing.T) {
t.Parallel()
existing := baseConfig + `
[initial_session]
user = "bob"
command = "old-command"
`
got := upsertInitialSession(existing, "alice", "/var/cache/dms-greeter", true)
if strings.Contains(got, `user = "bob"`) {
t.Fatalf("expected bob to be replaced, got:\n%s", got)
}
if !strings.Contains(got, `dms greeter launch-session --from-memory`) {
t.Fatalf("expected launch-session command, got:\n%s", got)
}
})
t.Run("removes initial session when disabled", func(t *testing.T) {
t.Parallel()
existing := baseConfig + `
[initial_session]
user = "alice"
command = "niri"
`
got := upsertInitialSession(existing, "", "", false)
if strings.Contains(got, "[initial_session]") {
t.Fatalf("expected initial session removed, got:\n%s", got)
}
if !strings.Contains(got, "[default_session]") {
t.Fatalf("expected default session preserved, got:\n%s", got)
}
})
}
func TestStripDesktopExecCodes(t *testing.T) {
t.Parallel()
got := stripDesktopExecCodes("niri --session %f")
want := "niri --session"
if got != want {
t.Fatalf("stripDesktopExecCodes = %q, want %q", got, want)
}
}
func TestBuildGreetdCommand(t *testing.T) {
t.Parallel()
tests := []struct {
name string
wrapper string
compositor string
dmsPath string
useVoidLogind bool
want string
}{
{
name: "standard command",
wrapper: "/usr/bin/dms-greeter",
compositor: "Niri",
want: "/usr/bin/dms-greeter --command niri --cache-dir /var/cache/dms-greeter",
},
{
name: "void selects elogind and keeps custom DMS path",
wrapper: "/usr/bin/dms-greeter",
compositor: "Niri",
dmsPath: "/usr/share/quickshell/dms-greeter",
useVoidLogind: true,
want: "env LIBSEAT_BACKEND=logind DMS_VOID=1 /usr/bin/dms-greeter --command niri --cache-dir /var/cache/dms-greeter -p /usr/share/quickshell/dms-greeter",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := buildGreetdCommand(tt.wrapper, tt.compositor, tt.dmsPath, tt.useVoidLogind); got != tt.want {
t.Fatalf("buildGreetdCommand() = %q, want %q", got, tt.want)
}
})
}
}
func TestVoidLogindGreeterCommand(t *testing.T) {
t.Parallel()
const oldCommand = "/usr/bin/dms-greeter --command niri -C /etc/greetd/niri.kdl"
const want = "env LIBSEAT_BACKEND=logind DMS_VOID=1 " + oldCommand
if got := voidLogindGreeterCommand(oldCommand); got != want {
t.Fatalf("voidLogindGreeterCommand() = %q, want %q", got, want)
}
if got := voidLogindGreeterCommand(want); got != want {
t.Fatalf("voidLogindGreeterCommand() must be idempotent, got %q", got)
}
}
func TestResolveGreeterAutoLoginState(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": true,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"lastSuccessfulUser": "alice",
"lastSessionDesktopId": "niri.desktop"
}`)
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if !enabled || loginUser != "alice" || sessionID != "niri.desktop" {
t.Fatalf("got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
}
}
func TestResolveGreeterAutoLoginStateIgnoresStaleSessionExec(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": true,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"lastSuccessfulUser": "alice",
"lastSessionId": "/nix/store/old-session/share/wayland-sessions/example.desktop",
"lastSessionExec": "/nix/store/old-session/bin/start-example-session"
}`)
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if !enabled || loginUser != "alice" || sessionID != "example.desktop" {
t.Fatalf("got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
}
got := upsertInitialSession("", loginUser, cacheDir, true)
if strings.Contains(got, "/nix/store/old-session") {
t.Fatalf("initial session must not include stale store path, got:\n%s", got)
}
}
func TestResolveGreeterAutoLoginStateIgnoresMemoryFlag(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": false,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"autoLoginEnabled": true,
"lastSuccessfulUser": "alice",
"lastSessionExec": "niri"
}`)
enabled, loginUser, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if enabled || loginUser != "" || sessionID != "" {
t.Fatalf("expected disabled with empty user/session, got enabled=%v user=%q session=%q", enabled, loginUser, sessionID)
}
}
func TestResolveSessionExecInDirs(t *testing.T) {
t.Parallel()
oldDir := filepath.Join(t.TempDir(), "wayland-sessions")
newDir := filepath.Join(t.TempDir(), "wayland-sessions")
writeTestFile(t, filepath.Join(oldDir, "example.desktop"), `[Desktop Entry]
Name=Example Session
Exec=/nix/store/old-session/bin/start-example-session
`)
writeTestFile(t, filepath.Join(newDir, "example.desktop"), `[Desktop Entry]
Name=Example Session
Exec=/run/current-system/sw/bin/start-example-session
`)
got, err := resolveSessionExecInDirs("example.desktop", []string{newDir, oldDir})
if err != nil {
t.Fatalf("resolveSessionExecInDirs returned error: %v", err)
}
if got != "/run/current-system/sw/bin/start-example-session" {
t.Fatalf("resolveSessionExecInDirs = %q", got)
}
}
func TestClearGreeterAutoLoginMemory(t *testing.T) {
t.Parallel()
memoryPath := filepath.Join(t.TempDir(), "memory.json")
writeTestFile(t, memoryPath, `{
"autoLoginEnabled": true,
"lastSuccessfulUser": "alice"
}`)
if err := clearGreeterAutoLoginMemory(memoryPath, ""); err != nil {
t.Fatalf("clearGreeterAutoLoginMemory returned error: %v", err)
}
data, err := os.ReadFile(memoryPath)
if err != nil {
t.Fatalf("failed to read memory file: %v", err)
}
if strings.Contains(string(data), "autoLoginEnabled") {
t.Fatalf("expected autoLoginEnabled removed, got: %s", string(data))
}
if !strings.Contains(string(data), "lastSuccessfulUser") {
t.Fatalf("expected other memory fields preserved, got: %s", string(data))
}
}
-200
View File
@@ -1,200 +0,0 @@
package greeter
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
func sessionDesktopIDFromPath(path string) string {
id := strings.TrimSpace(path)
if id == "" {
return ""
}
if strings.ContainsAny(id, "/\\") {
id = filepath.Base(id)
}
if id == "" {
return ""
}
if !strings.HasSuffix(id, ".desktop") {
id += ".desktop"
}
return id
}
func sessionDesktopIDFromMemory(mem greeterAutoLoginMemory) string {
if id := sessionDesktopIDFromPath(mem.LastSessionDesktopID); id != "" {
return id
}
return sessionDesktopIDFromPath(mem.LastSessionID)
}
func sessionDesktopDirs() []string {
seen := make(map[string]bool)
dirs := make([]string, 0, 8)
addBase := func(base string) {
base = strings.TrimSpace(base)
if base == "" {
return
}
for _, sub := range []string{"wayland-sessions", "xsessions"} {
dir := filepath.Join(base, sub)
if seen[dir] {
continue
}
seen[dir] = true
dirs = append(dirs, dir)
}
}
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
addBase(dataHome)
} else if home, err := os.UserHomeDir(); err == nil && home != "" {
addBase(filepath.Join(home, ".local", "share"))
}
if dataDirs := os.Getenv("XDG_DATA_DIRS"); dataDirs != "" {
for _, dir := range strings.Split(dataDirs, ":") {
addBase(dir)
}
} else {
addBase("/usr/local/share")
addBase("/usr/share")
}
return dirs
}
func ResolveSessionExec(sessionID string) (string, error) {
return resolveSessionExecInDirs(sessionID, sessionDesktopDirs())
}
func resolveSessionExecInDirs(sessionID string, dirs []string) (string, error) {
id := sessionDesktopIDFromPath(sessionID)
if id == "" {
return "", fmt.Errorf("session id is empty")
}
for _, dir := range dirs {
path := filepath.Join(dir, id)
execLine, err := execFromDesktopFile(path)
if err == nil {
return execLine, nil
}
if !os.IsNotExist(err) {
return "", err
}
}
return "", fmt.Errorf("session desktop file %q was not found", id)
}
// parseExecString splits a Desktop Entry Exec= value into argv without
// involving a shell, mirroring quickshell's DesktopEntry::parseExecString
// (string quoting, value escapes, field code stripping).
func parseExecString(execLine string) []string {
var args []string
var cur strings.Builder
inString := false
escape := 0
percent := false
for _, c := range execLine {
switch {
case escape == 0 && c == '\\':
escape = 1
case inString:
switch {
case c == '\\':
escape++
if escape == 4 {
cur.WriteByte('\\')
escape = 0
}
case escape == 2:
cur.WriteRune(c)
escape = 0
case escape != 0:
switch c {
case 's':
cur.WriteByte(' ')
case 'n':
cur.WriteByte('\n')
case 't':
cur.WriteByte('\t')
case 'r':
cur.WriteByte('\r')
default:
cur.WriteRune(c)
}
escape = 0
case c == '"' || c == '\'':
inString = false
default:
cur.WriteRune(c)
}
case escape != 0:
cur.WriteRune(c)
escape = 0
case percent:
if c == '%' {
cur.WriteByte('%')
}
percent = false
case c == '%':
percent = true
case c == '"' || c == '\'':
inString = true
case c == ' ':
if cur.Len() > 0 {
args = append(args, cur.String())
cur.Reset()
}
default:
cur.WriteRune(c)
}
}
if cur.Len() > 0 {
args = append(args, cur.String())
}
return args
}
func LaunchSessionByID(sessionID string) error {
execLine, err := ResolveSessionExec(sessionID)
if err != nil {
return err
}
argv := parseExecString(strings.TrimSpace(execLine))
if len(argv) == 0 {
return fmt.Errorf("session %q has an empty Exec command", sessionID)
}
resolved, err := exec.LookPath(argv[0])
if err != nil {
return fmt.Errorf("session %q command %q not found: %w", sessionID, argv[0], err)
}
env := append(os.Environ(), "XDG_SESSION_TYPE=wayland")
return syscall.Exec(resolved, argv, env)
}
func LaunchSessionFromMemory(cacheDir, homeDir string) error {
enabled, _, sessionID, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
return err
}
if !enabled {
return fmt.Errorf("greeter auto-login is disabled")
}
if sessionID == "" {
return fmt.Errorf("greeter auto-login has no remembered session")
}
return LaunchSessionByID(sessionID)
}
@@ -1,57 +0,0 @@
package greeter
import (
"path/filepath"
"reflect"
"testing"
)
func TestParseExecString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
exec string
want []string
}{
{"plain", "niri --session", []string{"niri", "--session"}},
{"extra spaces", "niri --session", []string{"niri", "--session"}},
{"double quoted arg", `env "with space" run`, []string{"env", "with space", "run"}},
{"single quoted arg", `env 'with space' run`, []string{"env", "with space", "run"}},
{"escaped quote in quotes", `sh "say \\"hi\\""`, []string{"sh", `say "hi"`}},
{"field code dropped", "gnome-session %U", []string{"gnome-session"}},
{"field code mid-arg", "app --url=%u --run", []string{"app", "--url=", "--run"}},
{"literal percent", "app 100%% done", []string{"app", "100%", "done"}},
{"shell metachars stay literal", "sh -c $(reboot); echo", []string{"sh", "-c", "$(reboot);", "echo"}},
{"empty", "", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseExecString(tt.exec); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("parseExecString(%q) = %#v, want %#v", tt.exec, got, tt.want)
}
})
}
}
func TestExecFromDesktopFileOnlyReadsDesktopEntryGroup(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "example.desktop")
writeTestFile(t, path, `[Desktop Action other]
Exec=/wrong/binary
[Desktop Entry]
Name=Example
Exec = /right/binary --flag
`)
got, err := execFromDesktopFile(path)
if err != nil {
t.Fatalf("execFromDesktopFile returned error: %v", err)
}
if got != "/right/binary --flag" {
t.Fatalf("execFromDesktopFile = %q, want %q", got, "/right/binary --flag")
}
}
-548
View File
@@ -1,548 +0,0 @@
package greeter
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
var monitorWallpaperSanitizer = regexp.MustCompile(`[^a-zA-Z0-9]+`)
func userGreeterCacheDir(cacheDir, username string) string {
return filepath.Join(cacheDir, "users", username)
}
func isUserOwnedGreeterCacheSlot(path, username string) bool {
if strings.TrimSpace(username) == "" {
return false
}
userDir, err := filepath.Abs(userGreeterCacheDir(GreeterCacheDir, username))
if err != nil {
return false
}
abs, err := filepath.Abs(path)
if err != nil {
return false
}
return abs == userDir || strings.HasPrefix(abs, userDir+string(filepath.Separator))
}
func UserIsInGreeterGroup(username string) bool {
group := DetectGreeterGroup()
if !utils.HasGroup(group) {
return false
}
groupsCmd := exec.Command("groups", username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return false
}
return strings.Contains(string(groupsOutput), group)
}
func CanSyncOwnUserGreeterProfile(username string) bool {
currentUser, err := user.Current()
if err != nil || currentUser.Username != username {
return false
}
if !UserIsInGreeterGroup(username) {
return false
}
usersDir := filepath.Join(GreeterCacheDir, "users")
if st, err := os.Stat(usersDir); err != nil || !st.IsDir() {
return false
}
testFile := filepath.Join(usersDir, ".write-test-"+username)
file, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
if err != nil {
return false
}
_ = file.Close()
_ = os.Remove(testFile)
return true
}
func GreeterProfileSyncReady() bool {
if command := readGreeterSessionCommand(); command != "" && strings.Contains(command, "dms-greeter") {
return true
}
usersDir := filepath.Join(GreeterCacheDir, "users")
st, err := os.Stat(usersDir)
return err == nil && st.IsDir()
}
func readGreeterSessionCommand() string {
data, err := os.ReadFile("/etc/greetd/config.toml")
if err != nil {
return ""
}
inDefaultSession := false
for line := range strings.SplitSeq(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
inDefaultSession = strings.EqualFold(strings.Trim(trimmed, "[]"), "default_session")
continue
}
if !inDefaultSession {
continue
}
if idx := strings.Index(trimmed, "#"); idx >= 0 {
trimmed = strings.TrimSpace(trimmed[:idx])
}
if !strings.HasPrefix(trimmed, "command") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
if command != "" {
return command
}
}
return ""
}
// SyncUserProfileCache writes the current user's theme slot under users/<username>/
// without modifying greetd or other system configuration. Requires membership in the
// greeter group and a prior full greeter setup by an administrator.
func SyncUserProfileCache(logFunc func(string)) error {
if logFunc == nil {
logFunc = func(string) {}
}
if !GreeterProfileSyncReady() {
return fmt.Errorf("greeter is not set up on this system yet; an administrator must run 'dms greeter install' or 'dms greeter sync' once first")
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to resolve current user: %w", err)
}
if !CanSyncOwnUserGreeterProfile(currentUser.Username) {
group := DetectGreeterGroup()
return fmt.Errorf("cannot sync greeter profile: you must be in the %s group with write access to %s/users\nAsk an administrator to run:\n sudo usermod -aG %s %s\nThen log out and back in before running:\n dms greeter sync --profile",
group, GreeterCacheDir, group, currentUser.Username)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
return fmt.Errorf("failed to resolve greeter color source: %w", err)
}
if err := syncUserGreeterCacheSlot(homeDir, GreeterCacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{
profileOnly: true,
}); err != nil {
return err
}
logFunc(fmt.Sprintf(" → %s/users/%s/", GreeterCacheDir, currentUser.Username))
return nil
}
func canWriteUserGreeterCacheSlot(dest, username string) bool {
return isUserOwnedGreeterCacheSlot(dest, username) && CanSyncOwnUserGreeterProfile(username)
}
type userSlotSyncOpts struct {
sudoPassword string
profileOnly bool
username string
}
func (o userSlotSyncOpts) useDirectWrite(dest string) bool {
if !o.profileOnly {
return false
}
return canWriteUserGreeterCacheSlot(dest, o.username)
}
func isGreeterCachePath(path string) bool {
abs, err := filepath.Abs(path)
if err != nil {
return true
}
cacheAbs, err := filepath.Abs(GreeterCacheDir)
if err != nil {
return true
}
if abs == cacheAbs {
return true
}
return strings.HasPrefix(abs, cacheAbs+string(filepath.Separator))
}
func greeterCacheOwner() string {
greeterGroup := DetectGreeterGroup()
daemonUser := DetectGreeterUser()
return daemonUser + ":" + greeterGroup
}
func ensureGreeterCacheSubdir(dir string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dir) {
if err := os.MkdirAll(dir, 0o770); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
return nil
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "mkdir", "-p", dir); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), opts.sudoPassword, "chown", owner, dir); err != nil {
if fallbackErr := privesc.Run(context.Background(), opts.sudoPassword, "chown", "root:"+DetectGreeterGroup(), dir); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", dir, err)
}
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "chmod", "2770", dir); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", dir, err)
}
return nil
}
func setGreeterCacheFileOwnership(path, sudoPassword string) error {
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, path); err != nil {
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+DetectGreeterGroup(), path); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", path, err)
}
}
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", path); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", path, err)
}
return nil
}
func syncUserGreeterCacheSlot(homeDir, cacheDir, username string, state greeterThemeSyncState, logFunc func(string), opts userSlotSyncOpts) error {
if strings.TrimSpace(username) == "" {
return nil
}
opts.username = username
userDir := userGreeterCacheDir(cacheDir, username)
if err := ensureGreeterCacheSubdir(userDir, opts); err != nil {
return err
}
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
settingsBytes, err := os.ReadFile(settingsPath)
if err != nil {
return fmt.Errorf("failed to read settings for user cache slot: %w", err)
}
settingsMap := map[string]any{}
if strings.TrimSpace(string(settingsBytes)) != "" {
if err := json.Unmarshal(settingsBytes, &settingsMap); err != nil {
return fmt.Errorf("failed to parse settings for user cache slot: %w", err)
}
}
if customTheme, ok := settingsMap["customThemeFile"].(string); ok && strings.TrimSpace(customTheme) != "" {
resolvedTheme := customTheme
if !filepath.IsAbs(resolvedTheme) {
resolvedTheme = filepath.Join(homeDir, resolvedTheme)
}
if st, statErr := os.Stat(resolvedTheme); statErr == nil && !st.IsDir() {
destTheme := filepath.Join(userDir, "custom-theme.json")
if err := copyFileWithPrivesc(resolvedTheme, destTheme, opts); err != nil {
return err
}
settingsMap["customThemeFile"] = destTheme
}
}
settingsBytes, err = json.Marshal(settingsMap)
if err != nil {
return fmt.Errorf("failed to marshal settings for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "settings.json"), settingsBytes, opts); err != nil {
return err
}
sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json")
sessionBytes, err := os.ReadFile(sessionPath)
if err != nil {
return fmt.Errorf("failed to read session for user cache slot: %w", err)
}
sessionMap := map[string]any{}
if strings.TrimSpace(string(sessionBytes)) != "" {
if err := json.Unmarshal(sessionBytes, &sessionMap); err != nil {
return fmt.Errorf("failed to parse session for user cache slot: %w", err)
}
}
if err := localizeSessionWallpapers(sessionMap, userDir, opts); err != nil {
return err
}
sessionBytes, err = json.Marshal(sessionMap)
if err != nil {
return fmt.Errorf("failed to marshal session for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "session.json"), sessionBytes, opts); err != nil {
return err
}
colorsSource := state.effectiveColorsSource(homeDir)
if err := copyFileWithPrivesc(colorsSource, filepath.Join(userDir, "colors.json"), opts); err != nil {
return fmt.Errorf("failed to copy colors for user cache slot: %w", err)
}
if err := syncUserProfileImage(homeDir, userDir, opts); err != nil {
return err
}
rootOverride := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
userOverride := filepath.Join(userDir, "greeter_wallpaper_override.jpg")
if st, statErr := os.Stat(rootOverride); statErr == nil && !st.IsDir() {
if err := copyFileWithPrivesc(rootOverride, userOverride, opts); err != nil {
return fmt.Errorf("failed to copy greeter wallpaper override for user cache slot: %w", err)
}
} else if opts.useDirectWrite(userOverride) {
_ = os.Remove(userOverride)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", userOverride)
}
logFunc(fmt.Sprintf("✓ Synced per-user greeter cache for %s", username))
return nil
}
func localizeSessionWallpapers(session map[string]any, userDir string, opts userSlotSyncOpts) error {
stringKeys := []struct {
key string
prefix string
}{
{"wallpaperPath", "wallpaper"},
{"wallpaperPathLight", "wallpaper-light"},
{"wallpaperPathDark", "wallpaper-dark"},
}
for _, item := range stringKeys {
if err := localizeWallpaperStringField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
mapKeys := []struct {
key string
prefix string
}{
{"monitorWallpapers", "wallpaper-monitor"},
{"monitorWallpapersLight", "wallpaper-monitor-light"},
{"monitorWallpapersDark", "wallpaper-monitor-dark"},
}
for _, item := range mapKeys {
if err := localizeWallpaperMapField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
return nil
}
func localizeWallpaperStringField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok {
return nil
}
path, ok := raw.(string)
if !ok || strings.TrimSpace(path) == "" {
return nil
}
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix, opts)
if err != nil {
return err
}
if dest != "" {
session[key] = dest
}
return nil
}
func localizeWallpaperMapField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok || raw == nil {
return nil
}
values, ok := raw.(map[string]any)
if !ok {
return nil
}
for monitor, rawPath := range values {
path, ok := rawPath.(string)
if !ok || strings.TrimSpace(path) == "" {
continue
}
safeMonitor := monitorWallpaperSanitizer.ReplaceAllString(monitor, "-")
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix+"-"+safeMonitor, opts)
if err != nil {
return err
}
if dest != "" {
values[monitor] = dest
}
}
return nil
}
func copyWallpaperIntoUserCache(srcPath, userDir, prefix string, opts userSlotSyncOpts) (string, error) {
if strings.TrimSpace(srcPath) == "" {
return "", nil
}
st, err := os.Stat(srcPath)
if err != nil || st.IsDir() {
return "", nil
}
ext := filepath.Ext(srcPath)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, prefix+ext)
if err := copyFileWithPrivesc(srcPath, dest, opts); err != nil {
return "", err
}
return dest, nil
}
func copyFileWithPrivesc(src, dest string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
if !isGreeterCachePath(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", dest)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", src, dest); err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", src, dest, err)
}
return setGreeterCacheFileOwnership(dest, opts.sudoPassword)
}
func writeFileWithPrivesc(path string, data []byte, opts userSlotSyncOpts) error {
if opts.useDirectWrite(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
if !isGreeterCachePath(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
tmp, err := os.CreateTemp("", "dms-greeter-user-cache-*")
if err != nil {
return fmt.Errorf("failed to create temp file for %s: %w", path, err)
}
tmpPath := tmp.Name()
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to write temp file for %s: %w", path, err)
}
if err := tmp.Close(); err != nil {
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to close temp file for %s: %w", path, err)
}
defer os.Remove(tmpPath)
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", tmpPath, path); err != nil {
return fmt.Errorf("failed to install %s: %w", path, err)
}
return setGreeterCacheFileOwnership(path, opts.sudoPassword)
}
func resolveUserProfileImageSource(homeDir string) string {
candidates := []string{
filepath.Join(homeDir, ".face"),
filepath.Join(homeDir, ".face.icon"),
}
if homeDir != "" {
username := filepath.Base(homeDir)
if username != "" && username != "." && username != string(filepath.Separator) {
candidates = append([]string{filepath.Join("/var/lib/AccountsService/icons", username)}, candidates...)
}
}
for _, src := range candidates {
st, err := os.Stat(src)
if err == nil && !st.IsDir() && st.Size() > 0 {
return src
}
}
return ""
}
func syncUserProfileImage(homeDir, userDir string, opts userSlotSyncOpts) error {
for _, name := range []string{"profile.jpg", "profile.jpeg", "profile.png", "profile.webp"} {
path := filepath.Join(userDir, name)
if opts.useDirectWrite(path) {
_ = os.Remove(path)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
}
}
src := resolveUserProfileImageSource(homeDir)
if src == "" {
return nil
}
ext := filepath.Ext(src)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, "profile"+ext)
if err := copyFileWithPrivesc(src, dest, opts); err != nil {
return fmt.Errorf("failed to copy profile image for user cache slot: %w", err)
}
return nil
}
@@ -1,81 +0,0 @@
package greeter
import (
"path/filepath"
"testing"
)
func TestUserGreeterCacheDir(t *testing.T) {
t.Parallel()
got := userGreeterCacheDir("/var/cache/dms-greeter", "alice")
want := filepath.Join("/var/cache/dms-greeter", "users", "alice")
if got != want {
t.Fatalf("userGreeterCacheDir() = %q, want %q", got, want)
}
}
func TestResolveUserProfileImageSource(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
facePath := filepath.Join(homeDir, ".face")
writeTestFile(t, facePath, "face")
got := resolveUserProfileImageSource(homeDir)
if got != facePath {
t.Fatalf("resolveUserProfileImageSource() = %q, want %q", got, facePath)
}
}
func TestIsUserOwnedGreeterCacheSlot(t *testing.T) {
t.Parallel()
slot := filepath.Join(GreeterCacheDir, "users", "alice", "settings.json")
if !isUserOwnedGreeterCacheSlot(slot, "alice") {
t.Fatalf("expected alice to own %q", slot)
}
if isUserOwnedGreeterCacheSlot(slot, "bob") {
t.Fatalf("expected bob not to own alice slot")
}
if isUserOwnedGreeterCacheSlot(filepath.Join(GreeterCacheDir, "settings.json"), "alice") {
t.Fatalf("expected root cache file not to be a user slot")
}
}
func TestLocalizeSessionWallpapers(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
userDir := filepath.Join(homeDir, "users", "alice")
wallpaperPath := filepath.Join(homeDir, "wall.jpg")
writeTestFile(t, wallpaperPath, "wallpaper")
session := map[string]any{
"wallpaperPath": wallpaperPath,
"monitorWallpapers": map[string]any{
"DP-1": wallpaperPath,
},
}
if err := localizeSessionWallpapers(session, userDir, userSlotSyncOpts{}); err != nil {
t.Fatalf("localizeSessionWallpapers returned error: %v", err)
}
gotPath, ok := session["wallpaperPath"].(string)
if !ok || gotPath == "" {
t.Fatalf("expected localized wallpaperPath, got %#v", session["wallpaperPath"])
}
if gotPath == wallpaperPath {
t.Fatalf("expected copied wallpaper path, still points to source")
}
monitorMap, ok := session["monitorWallpapers"].(map[string]any)
if !ok {
t.Fatalf("expected monitorWallpapers map")
}
monitorPath, ok := monitorMap["DP-1"].(string)
if !ok || monitorPath == "" || monitorPath == wallpaperPath {
t.Fatalf("expected localized monitor wallpaper, got %#v", monitorMap["DP-1"])
}
}
+2 -6
View File
@@ -9,8 +9,8 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
// ErrConfirmationRequired is returned when --yes is not set and the user
@@ -223,16 +223,12 @@ func (r *Runner) Run() error {
// 9. Greeter setup (if dms-greeter was included)
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
compositorName := "niri"
if wm == deps.WindowManagerHyprland {
compositorName = "Hyprland"
}
fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
logFunc := func(line string) {
r.log(line)
fmt.Fprintf(os.Stdout, " greeter: %s\n", line)
}
if err := greeter.AutoSetupGreeter(compositorName, sudoPassword, logFunc); err != nil {
if err := utils.RunDmsGreeterInstall(sudoPassword, logFunc); err != nil {
// Non-fatal, matching TUI behavior
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
}
+19 -207
View File
@@ -1,220 +1,32 @@
package log
import (
"io"
"os"
"regexp"
"strings"
"sync"
"github.com/charmbracelet/lipgloss"
cblog "github.com/charmbracelet/log"
"github.com/mattn/go-isatty"
"github.com/muesli/termenv"
danklog "github.com/AvengeMedia/dankgo/log"
)
// Logger embeds the Charm Logger and adds Printf/Fatalf
type Logger struct{ *cblog.Logger }
type Logger = danklog.Logger
// Printf routes goose/info-style logs through Infof.
func (l *Logger) Printf(format string, v ...any) { l.Infof(format, v...) }
// Fatalf keeps gooses contract of exiting the program.
func (l *Logger) Fatalf(format string, v ...any) { l.Logger.Fatalf(format, v...) }
var (
logger *Logger
initLogger sync.Once
logMu sync.Mutex
logFile *os.File
logStderr io.Writer = os.Stderr
ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
)
// ansiStripWriter strips ANSI escape sequences before forwarding to w. Used
// for the file sink so colored stderr stays colored while the file stays plain.
type ansiStripWriter struct{ w io.Writer }
func (a *ansiStripWriter) Write(p []byte) (int, error) {
stripped := ansiRe.ReplaceAll(p, nil)
if _, err := a.w.Write(stripped); err != nil {
return 0, err
}
return len(p), nil
func init() {
danklog.SetEnvPrefix("DMS")
}
func parseLogLevel(level string) cblog.Level {
switch strings.ToLower(level) {
case "debug":
return cblog.DebugLevel
case "info":
return cblog.InfoLevel
case "warn", "warning":
return cblog.WarnLevel
case "error":
return cblog.ErrorLevel
case "fatal":
return cblog.FatalLevel
default:
return cblog.InfoLevel
}
}
func GetLogger() *Logger { return danklog.GetLogger() }
func GetQtLoggingRules() string {
level := os.Getenv("DMS_LOG_LEVEL")
if level == "" {
level = "info"
}
func GetQtLoggingRules() string { return danklog.GetQtLoggingRules() }
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
// probes); suppressed except at debug level
var rules []string
switch strings.ToLower(level) {
case "fatal":
rules = []string{"*.debug=false", "*.info=false", "*.warning=false", "*.critical=false"}
case "error":
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
case "warn", "warning":
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"}
case "info":
rules = []string{"*.debug=false", "scene.warning=false"}
case "debug":
return ""
default:
rules = []string{"*.debug=false", "scene.warning=false"}
}
func SetLevel(level string) { danklog.SetLevel(level) }
return strings.Join(rules, ";")
}
func SetLogFile(path string) error { return danklog.SetLogFile(path) }
// GetLogger returns a logger instance
func GetLogger() *Logger {
initLogger.Do(func() {
styles := cblog.DefaultStyles()
// Attempt to match the colors used by qml/quickshell logs
styles.Levels[cblog.FatalLevel] = lipgloss.NewStyle().
SetString(" FATAL").
Foreground(lipgloss.Color("1"))
styles.Levels[cblog.ErrorLevel] = lipgloss.NewStyle().
SetString(" ERROR").
Foreground(lipgloss.Color("9"))
styles.Levels[cblog.WarnLevel] = lipgloss.NewStyle().
SetString(" WARN").
Foreground(lipgloss.Color("3"))
styles.Levels[cblog.InfoLevel] = lipgloss.NewStyle().
SetString(" INFO").
Foreground(lipgloss.Color("2"))
styles.Levels[cblog.DebugLevel] = lipgloss.NewStyle().
SetString(" DEBUG").
Foreground(lipgloss.Color("4"))
func ApplyEnvOverrides() { danklog.ApplyEnvOverrides() }
base := cblog.New(logStderr)
base.SetStyles(styles)
base.SetReportTimestamp(false)
level := cblog.InfoLevel
if envLevel := os.Getenv("DMS_LOG_LEVEL"); envLevel != "" {
level = parseLogLevel(envLevel)
}
base.SetLevel(level)
base.SetPrefix(" go")
logger = &Logger{base}
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
logMu.Lock()
_ = setLogFile(logger, path)
logMu.Unlock()
}
})
return logger
}
// SetLevel updates the active log level. Accepts the same strings as
// DMS_LOG_LEVEL. Unknown values default to info.
func SetLevel(level string) {
GetLogger().SetLevel(parseLogLevel(level))
}
// SetLogFile makes the logger append to path in addition to stderr. Passing an
// empty string detaches the file sink. Atomic per-line writes (≤PIPE_BUF) on
// O_APPEND keep concurrent Go and QML writers from corrupting each other.
//
// Color handling: charmbracelet/log auto-detects color support from its
// io.Writer, and io.MultiWriter doesn't pass that through, so we force the ANSI
// profile when stderr is a TTY and route the file through ansiStripWriter so
// the file stays plain while stderr keeps its colors.
func SetLogFile(path string) error {
l := GetLogger()
logMu.Lock()
defer logMu.Unlock()
return setLogFile(l, path)
}
func setLogFile(l *Logger, path string) error {
if logFile != nil {
logFile.Close()
logFile = nil
}
if path == "" {
l.SetOutput(logStderr)
applyColorProfile(l, logStderr)
return nil
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
return err
}
logFile = f
out := io.MultiWriter(logStderr, &ansiStripWriter{w: f})
l.SetOutput(out)
applyColorProfile(l, logStderr)
return nil
}
// applyColorProfile forces the renderer's color profile to match what stderr
// would produce on its own, undoing the auto-downgrade triggered by wrapping
// stderr in a non-TTY writer (e.g. io.MultiWriter).
func applyColorProfile(l *Logger, stderr io.Writer) {
f, ok := stderr.(*os.File)
if !ok {
l.SetColorProfile(termenv.Ascii)
return
}
if isatty.IsTerminal(f.Fd()) {
l.SetColorProfile(termenv.ANSI)
return
}
l.SetColorProfile(termenv.Ascii)
}
// ApplyEnvOverrides re-reads DMS_LOG_LEVEL and DMS_LOG_FILE and reconfigures
// the singleton. Safe to call after CLI flags have rewritten the environment.
func ApplyEnvOverrides() {
GetLogger()
if level := os.Getenv("DMS_LOG_LEVEL"); level != "" {
SetLevel(level)
}
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
if err := SetLogFile(path); err != nil {
Warnf("Failed to open log file %q: %v", path, err)
}
}
}
// * Convenience wrappers
func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) }
func Debugf(format string, v ...any) { GetLogger().Debugf(format, v...) }
func Info(msg any, keyvals ...any) { GetLogger().Info(msg, keyvals...) }
func Infof(format string, v ...any) { GetLogger().Infof(format, v...) }
func Warn(msg any, keyvals ...any) { GetLogger().Warn(msg, keyvals...) }
func Warnf(format string, v ...any) { GetLogger().Warnf(format, v...) }
func Error(msg any, keyvals ...any) { GetLogger().Error(msg, keyvals...) }
func Errorf(format string, v ...any) { GetLogger().Errorf(format, v...) }
func Fatal(msg any, keyvals ...any) { GetLogger().Fatal(msg, keyvals...) }
func Fatalf(format string, v ...any) { GetLogger().Fatalf(format, v...) }
func Debug(msg any, keyvals ...any) { danklog.Debug(msg, keyvals...) }
func Debugf(format string, v ...any) { danklog.Debugf(format, v...) }
func Info(msg any, keyvals ...any) { danklog.Info(msg, keyvals...) }
func Infof(format string, v ...any) { danklog.Infof(format, v...) }
func Warn(msg any, keyvals ...any) { danklog.Warn(msg, keyvals...) }
func Warnf(format string, v ...any) { danklog.Warnf(format, v...) }
func Error(msg any, keyvals ...any) { danklog.Error(msg, keyvals...) }
func Errorf(format string, v ...any) { danklog.Errorf(format, v...) }
func Fatal(msg any, keyvals ...any) { danklog.Fatal(msg, keyvals...) }
func Fatalf(format string, v ...any) { danklog.Fatalf(format, v...) }
+79 -38
View File
@@ -44,6 +44,7 @@ type TemplateDef struct {
ID string
Commands []string
Flatpaks []string
ConfigDirs []string
ConfigFile string
Kind TemplateKind
RunUnconditionally bool
@@ -60,9 +61,9 @@ var templateRegistry = []TemplateDef{
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigFile: "vencord.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigDirs: []string{"vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigDirs: []string{"Vencord"}, ConfigFile: "vencord.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigDirs: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},
{ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal},
@@ -385,8 +386,12 @@ func buildOnce(opts *Options) (bool, error) {
default:
syncAccentColor(primaryDark)
}
refreshGTK(opts.Mode)
refreshGTK4()
refreshGTKTheme(opts.Mode)
refreshGTKColorScheme()
}
if isDMSKDEColorSchemeActive(opts.ConfigDir) {
applyKDEColorScheme(opts.Mode)
}
if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) {
@@ -459,9 +464,9 @@ output_path = '%s'
case TemplateKindGTK:
switch opts.Mode {
case ColorModeLight:
appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml")
appendConfig(opts, cfgFile, nil, nil, nil, "gtk3-light.toml")
default:
appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml")
appendConfig(opts, cfgFile, nil, nil, nil, "gtk3-dark.toml")
}
case TemplateKindTerminal:
appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
@@ -474,10 +479,10 @@ output_path = '%s'
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
}
default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
}
}
@@ -512,13 +517,14 @@ func appendConfig(
cfgFile *os.File,
checkCmd []string,
checkFlatpaks []string,
checkConfigDirs []string,
fileName string,
) {
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
if _, err := os.Stat(configPath); err != nil {
return
}
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) && !configDirExists(checkConfigDirs) {
return
}
data, err := os.ReadFile(configPath)
@@ -599,6 +605,20 @@ func templateSessionActive(tmpl TemplateDef) bool {
return err == nil
}
func configDirExists(names []string) bool {
configHome := utils.XDGConfigHome()
if configHome == "" {
return false
}
for _, name := range names {
info, err := os.Stat(filepath.Join(configHome, name))
if err == nil && info.IsDir() {
return true
}
}
return false
}
func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool {
// Both nil is treated as "skip check" / unconditionally run
if checkCmd == nil && checkFlatpaks == nil {
@@ -891,7 +911,7 @@ func generateDank16Variants(primaryDark, primaryLight, surface string, mode Colo
}
func isDMSGTKActive(configDir string) bool {
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
gtkCSS := filepath.Join(configDir, "gtk-4.0", "gtk.css")
info, err := os.Lstat(gtkCSS)
if err != nil {
@@ -907,7 +927,48 @@ func isDMSGTKActive(configDir string) bool {
return err == nil && strings.Contains(string(data), "dank-colors.css")
}
func refreshGTK(mode ColorMode) {
// isDMSKDEColorSchemeActive only flips the scheme when the user is already on a
// DankMatugen one, leaving Breeze (or anything else) untouched.
func isDMSKDEColorSchemeActive(configDir string) bool {
data, err := os.ReadFile(filepath.Join(configDir, "kdeglobals"))
if err != nil {
return false
}
inGeneral := false
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "[") {
inGeneral = line == "[General]"
continue
}
if !inGeneral {
continue
}
if name, ok := strings.CutPrefix(line, "ColorScheme="); ok {
return strings.HasPrefix(strings.TrimSpace(name), "DankMatugen")
}
}
return false
}
func applyKDEColorScheme(mode ColorMode) {
if !utils.CommandExists("plasma-apply-colorscheme") {
return
}
scheme := "DankMatugenDark"
if mode == ColorModeLight {
scheme = "DankMatugenLight"
}
log.Infof("Applying KDE color scheme: %s", scheme)
if err := exec.Command("plasma-apply-colorscheme", scheme).Run(); err != nil {
log.Warnf("Failed to apply KDE color scheme: %v", err)
}
}
func refreshGTKTheme(mode ColorMode) {
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
log.Warnf("Failed to reset gtk-theme: %v", err)
}
@@ -932,8 +993,8 @@ func expectColorSchemeEcho(scheme string) {
// apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps
// following the portal color-scheme (Chromium) can drop the restore signal
// mid-repaint and latch the wrong mode, so this is opt-in.
func refreshGTK4() {
if os.Getenv("DMS_ENABLE_GTK4_REFRESH") != "1" {
func refreshGTKColorScheme() {
if os.Getenv("DMS_ENABLE_GTK_REFRESH") != "1" {
return
}
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
@@ -951,13 +1012,13 @@ func refreshGTK4() {
expectColorSchemeEcho(toggle)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
log.Warnf("Failed to toggle color-scheme for GTK refresh: %v", err)
return
}
time.Sleep(400 * time.Millisecond)
time.Sleep(50 * time.Millisecond)
expectColorSchemeEcho(current)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
log.Warnf("Failed to restore color-scheme for GTK refresh: %v", err)
}
}
@@ -980,26 +1041,6 @@ func signalTerminals(opts *Options) {
}
}
func signalByName(name string, sig syscall.Signal) {
entries, err := os.ReadDir("/proc")
if err != nil {
return
}
for _, entry := range entries {
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
if err != nil {
continue
}
if strings.TrimSpace(string(comm)) == name {
syscall.Kill(pid, sig)
}
}
}
func syncColorScheme(mode ColorMode) {
scheme := "prefer-dark"
if mode == ColorModeLight {
@@ -1096,7 +1137,7 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
case tmpl.Kind == TemplateKindEmacs:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
default:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && templateSessionActive(tmpl)
detected = (appExists(checker, tmpl.Commands, tmpl.Flatpaks) || configDirExists(tmpl.ConfigDirs)) && templateSessionActive(tmpl)
}
checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected})
+95 -8
View File
@@ -38,7 +38,7 @@ func TestAppendConfigBinaryExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"sh"}, nil, "test.toml")
appendConfig(opts, cfgFile, []string{"sh"}, nil, nil, "test.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
@@ -82,7 +82,7 @@ func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, "test.toml")
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, nil, "test.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
@@ -122,7 +122,7 @@ func TestAppendConfigFlatpakExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, "test.toml")
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, nil, "test.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
@@ -163,7 +163,7 @@ func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, "test.toml")
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, nil, "test.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
@@ -203,7 +203,7 @@ func TestAppendConfigBothExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, "test.toml")
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, nil, "test.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
@@ -244,7 +244,7 @@ func TestAppendConfigNeitherExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, "test.toml")
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, nil, "test.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
@@ -281,7 +281,7 @@ func TestAppendConfigNoChecks(t *testing.T) {
opts := &Options{ShellDir: shellDir}
appendConfig(opts, cfgFile, nil, nil, "test.toml")
appendConfig(opts, cfgFile, nil, nil, nil, "test.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
@@ -312,7 +312,7 @@ func TestAppendConfigFileDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir}
appendConfig(opts, cfgFile, nil, nil, "nonexistent.toml")
appendConfig(opts, cfgFile, nil, nil, nil, "nonexistent.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
@@ -487,3 +487,90 @@ func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) {
}
assert.NotContains(t, string(output), "[templates.dmsmango]")
}
func TestAppendConfigConfigDirExists(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)
}
testConfig := "vencord config content"
if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte(testConfig), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
configHome := filepath.Join(tempDir, "config")
if err := os.MkdirAll(filepath.Join(configHome, "Vencord"), 0o755); err != nil {
t.Fatalf("failed to create Vencord config dir: %v", err)
}
t.Setenv("XDG_CONFIG_HOME", configHome)
outFile := filepath.Join(tempDir, "output.toml")
cfgFile, err := os.Create(outFile)
if err != nil {
t.Fatalf("failed to create output file: %v", err)
}
defer cfgFile.Close()
mockChecker := mocks_utils.NewMockAppChecker(t)
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
assert.Equal(t, testConfig+"\n", string(output))
}
func TestAppendConfigConfigDirDoesNotExist(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)
}
if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte("vencord config content"), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
configHome := filepath.Join(tempDir, "config")
if err := os.MkdirAll(configHome, 0o755); err != nil {
t.Fatalf("failed to create config home: %v", err)
}
t.Setenv("XDG_CONFIG_HOME", configHome)
outFile := filepath.Join(tempDir, "output.toml")
cfgFile, err := os.Create(outFile)
if err != nil {
t.Fatalf("failed to create output file: %v", err)
}
defer cfgFile.Close()
mockChecker := mocks_utils.NewMockAppChecker(t)
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
assert.Empty(t, string(output))
}
+16
View File
@@ -0,0 +1,16 @@
package matugen
import (
"os/exec"
"strings"
"syscall"
"golang.org/x/sys/unix"
)
// procfs(5) is optional on FreeBSD; pkill(1) from base queries the kernel
// directly.
func signalByName(name string, sig syscall.Signal) {
signame := strings.TrimPrefix(unix.SignalName(sig), "SIG")
exec.Command("pkill", "-"+signame, "-x", name).Run()
}
+29
View File
@@ -0,0 +1,29 @@
package matugen
import (
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
)
func signalByName(name string, sig syscall.Signal) {
entries, err := os.ReadDir("/proc")
if err != nil {
return
}
for _, entry := range entries {
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
if err != nil {
continue
}
if strings.TrimSpace(string(comm)) == name {
syscall.Kill(pid, sig)
}
}
}
+21 -393
View File
@@ -5,30 +5,20 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
const (
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
legacyGreeterPamU2FComment = "# DMS greeter U2F"
GreetdPamPath = "/etc/pam.d/greetd"
DankshellPamPath = "/etc/pam.d/dankshell"
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
)
@@ -38,7 +28,8 @@ const (
// rest cover distros (or minimal installs) with no /etc/pam.d/login.
// lockscreenPamBaseDirs mirrors libpam's search order: /etc overrides, then the
// vendor dir (/usr/lib) and the stateless-distro default (/usr/share).
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d"}
// /usr/local/etc/pam.d is OpenPAM's ports dir on FreeBSD (openpam_configure).
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d", "/usr/local/etc/pam.d"}
// Standalone auth+account services, most universal first. login exists almost
// everywhere (util-linux); system-* cover Fedora/Arch/Gentoo/SUSE-Leap.
@@ -50,13 +41,15 @@ var lockscreenPamEntryCandidates = []string{
}
// Fallback for distros with no standalone login service, only shared building
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*).
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*), FreeBSD
// (system holds both stanzas, included by login).
var lockscreenPamSharedIncludePairs = []struct {
auth string
account string
}{
{auth: "common-auth", account: "common-account"},
{auth: "base-auth", account: "base-account"},
{auth: "system", account: "system"},
}
var includedPamAuthFiles = []string{
@@ -67,34 +60,28 @@ var includedPamAuthFiles = []string{
"system-local-login",
"common-auth-pc",
"login",
"system",
}
type AuthSettings struct {
EnableFprint bool `json:"enableFprint"`
EnableU2f bool `json:"enableU2f"`
GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"`
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
EnableFprint bool `json:"enableFprint"`
EnableU2f bool `json:"enableU2f"`
}
type SyncAuthOptions struct {
HomeDir string
ForceGreeterAuth bool
HomeDir string
}
type syncDeps struct {
pamDir string
greetdPath string
dankshellPath string
dankshellU2fPath string
isNixOS func() bool
readFile func(string) ([]byte, error)
stat func(string) (os.FileInfo, error)
createTemp func(string, string) (*os.File, error)
removeFile func(string) error
runSudoCmd func(string, string, ...string) error
pamModuleExists func(string) bool
fingerprintAvailableForCurrentUser func() bool
pamDir string
dankshellPath string
dankshellU2fPath string
isNixOS func() bool
readFile func(string) ([]byte, error)
stat func(string) (os.FileInfo, error)
createTemp func(string, string) (*os.File, error)
removeFile func(string) error
runSudoCmd func(string, string, ...string) error
}
type lockscreenPamIncludeDirective struct {
@@ -150,7 +137,6 @@ func (r lockscreenPamResolver) locate(target string) (string, error) {
func defaultSyncDeps() syncDeps {
return syncDeps{
pamDir: "/etc/pam.d",
greetdPath: GreetdPamPath,
dankshellPath: DankshellPamPath,
dankshellU2fPath: DankshellU2FPamPath,
isNixOS: IsNixOS,
@@ -161,8 +147,6 @@ func defaultSyncDeps() syncDeps {
runSudoCmd: func(password, command string, args ...string) error {
return privesc.Run(context.Background(), password, append([]string{command}, args...)...)
},
pamModuleExists: pamModuleExists,
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
}
}
@@ -191,22 +175,10 @@ func ReadAuthSettings(homeDir string) (AuthSettings, error) {
return settings, nil
}
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
settings, err := ReadAuthSettings(homeDir)
if err != nil {
return false, false, err
}
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
}
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
}
func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
}
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
homeDir := strings.TrimSpace(options.HomeDir)
if homeDir == "" {
@@ -229,99 +201,9 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S
return err
}
if _, err := deps.stat(deps.greetdPath); err != nil {
if os.IsNotExist(err) {
logFunc(" /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
return nil
}
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
}
if settings.GreeterPamExternallyManaged {
if err := removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, deps); err != nil {
return err
}
logFunc(" /etc/pam.d/greetd is externally managed. Skipping DMS greeter PAM sync.")
return nil
}
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
return err
}
return nil
}
func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
if deps.isNixOS() {
return nil
}
data, err := deps.readFile(deps.greetdPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
}
originalContent := string(data)
stripped, removed := stripManagedGreeterPamBlock(originalContent)
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
if !removed && !removedLegacy {
return nil
}
if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
}
logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
return nil
}
func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
if pamText == "" {
return false, false, false, false
}
lines := strings.Split(pamText, "\n")
inManaged := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
switch trimmed {
case GreeterPamManagedBlockStart:
managed = true
inManaged = true
continue
case GreeterPamManagedBlockEnd:
inManaged = false
continue
}
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
legacy = true
}
if !inManaged {
continue
}
if strings.Contains(trimmed, "pam_fprintd") {
fingerprint = true
}
if strings.Contains(trimmed, "pam_u2f") {
u2f = true
}
}
return managed, fingerprint, u2f, legacy
}
func StripManagedGreeterPamContent(pamText string) (string, bool) {
stripped, removed := stripManagedGreeterPamBlock(pamText)
stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
return stripped, removed || removedLegacy
}
func PamTextIncludesFile(pamText, filename string) bool {
lines := strings.Split(pamText, "\n")
for _, line := range lines {
@@ -1030,186 +912,6 @@ func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword strin
return nil
}
func stripManagedGreeterPamBlock(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
inManagedBlock := false
removed := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == GreeterPamManagedBlockStart {
inManagedBlock = true
removed = true
continue
}
if trimmed == GreeterPamManagedBlockEnd {
inManagedBlock = false
removed = true
continue
}
if inManagedBlock {
removed = true
continue
}
filtered = append(filtered, line)
}
return strings.Join(filtered, "\n"), removed
}
func stripLegacyGreeterPamLines(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
removed := false
for i := 0; i < len(lines); i++ {
trimmed := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
removed = true
if i+1 < len(lines) {
nextLine := strings.TrimSpace(lines[i+1])
if strings.HasPrefix(nextLine, "auth") &&
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
i++
}
}
continue
}
filtered = append(filtered, lines[i])
}
return strings.Join(filtered, "\n"), removed
}
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
lines := strings.Split(content, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
block := strings.Join(blockLines, "\n")
prefix := strings.Join(lines[:i], "\n")
suffix := strings.Join(lines[i:], "\n")
switch {
case prefix == "":
return block + "\n" + suffix, nil
case suffix == "":
return prefix + "\n" + block, nil
default:
return prefix + "\n" + block + "\n" + suffix, nil
}
}
}
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
}
func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
var wantFprint, wantU2f bool
fprintToggleEnabled := forceAuth
u2fToggleEnabled := forceAuth
if forceAuth {
wantFprint = deps.pamModuleExists("pam_fprintd.so")
wantU2f = deps.pamModuleExists("pam_u2f.so")
} else {
fprintToggleEnabled = settings.GreeterEnableFprint
u2fToggleEnabled = settings.GreeterEnableU2f
fprintModule := deps.pamModuleExists("pam_fprintd.so")
u2fModule := deps.pamModuleExists("pam_u2f.so")
wantFprint = settings.GreeterEnableFprint && fprintModule
wantU2f = settings.GreeterEnableU2f && u2fModule
if settings.GreeterEnableFprint && !fprintModule {
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
}
if settings.GreeterEnableU2f && !u2fModule {
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
}
}
if deps.isNixOS() {
logFunc(" NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
return nil
}
pamData, err := deps.readFile(deps.greetdPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
}
originalContent := string(pamData)
content, _ := stripManagedGreeterPamBlock(originalContent)
content, _ = stripLegacyGreeterPamLines(content)
includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
if wantFprint && includedFprintFile != "" {
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
wantFprint = false
}
if wantU2f && includedU2fFile != "" {
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
wantU2f = false
}
if !wantFprint && includedFprintFile != "" {
if fprintToggleEnabled {
logFunc(" Fingerprint auth is still enabled via included " + includedFprintFile + ".")
if fprintAvailableForCurrentUser {
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
} else {
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
}
} else {
if fprintAvailableForCurrentUser {
logFunc(" Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
} else {
logFunc(" pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
logFunc(" Password auth remains the effective login path.")
}
}
}
if !wantU2f && includedU2fFile != "" {
if u2fToggleEnabled {
logFunc(" Security-key auth is still enabled via included " + includedU2fFile + ".")
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
} else {
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
logFunc(" " + pamManagerHintForCurrentDistro())
}
}
if wantFprint || wantU2f {
blockLines := []string{GreeterPamManagedBlockStart}
if wantFprint {
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=2 timeout=10")
}
if wantU2f {
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
}
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
if err != nil {
return err
}
}
if content == originalContent {
return nil
}
if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
}
if wantFprint || wantU2f {
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
} else {
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
}
return nil
}
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
if err != nil {
@@ -1236,26 +938,6 @@ func writeManagedPamFile(content string, destPath string, sudoPassword string, d
return nil
}
func pamManagerHintForCurrentDistro() string {
osInfo, err := distros.GetOSInfo()
if err != nil {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
switch config.Family {
case distros.FamilyFedora:
return "Disable it in authselect to force password-only greeter login."
case distros.FamilyDebian, distros.FamilyUbuntu:
return "Disable it in pam-auth-update to force password-only greeter login."
default:
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
}
func pamModuleExists(module string) bool {
for _, libDir := range []string{
"/usr/lib64/security",
@@ -1268,6 +950,9 @@ func pamModuleExists(module string) bool {
"/usr/lib/aarch64-linux-gnu/security",
"/run/current-system/sw/lib64/security",
"/run/current-system/sw/lib/security",
"/usr/local/lib/security",
"/usr/local/lib",
"/usr/lib",
} {
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
return true
@@ -1275,60 +960,3 @@ func pamModuleExists(module string) bool {
}
return false
}
func hasEnrolledFingerprintOutput(output string) bool {
lower := strings.ToLower(output)
if strings.Contains(lower, "no fingers enrolled") ||
strings.Contains(lower, "no fingerprints enrolled") ||
strings.Contains(lower, "no prints enrolled") {
return false
}
if strings.Contains(lower, "has fingers enrolled") ||
strings.Contains(lower, "has fingerprints enrolled") {
return true
}
for _, line := range strings.Split(lower, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "finger:") {
return true
}
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
return true
}
}
return false
}
func FingerprintAuthAvailableForCurrentUser() bool {
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
if username == "" {
username = strings.TrimSpace(os.Getenv("USER"))
}
if username == "" {
out, err := exec.Command("id", "-un").Output()
if err == nil {
username = strings.TrimSpace(string(out))
}
}
return fingerprintAuthAvailableForUser(username)
}
func fingerprintAuthAvailableForUser(username string) bool {
username = strings.TrimSpace(username)
if username == "" {
return false
}
if !pamModuleExists("pam_fprintd.so") {
return false
}
if _, err := exec.LookPath("fprintd-list"); err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
if err != nil {
return false
}
return hasEnrolledFingerprintOutput(string(out))
}
+94 -271
View File
@@ -18,42 +18,6 @@ func writeTestFile(t *testing.T, path string, content string) {
}
}
type pamTestEnv struct {
pamDir string
greetdPath string
dankshellPath string
dankshellU2fPath string
tmpDir string
homeDir string
availableModules map[string]bool
fingerprintAvailable bool
}
func newPamTestEnv(t *testing.T) *pamTestEnv {
t.Helper()
root := t.TempDir()
pamDir := filepath.Join(root, "pam.d")
tmpDir := filepath.Join(root, "tmp")
homeDir := filepath.Join(root, "home")
for _, dir := range []string{pamDir, tmpDir, homeDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("failed to create %s: %v", dir, err)
}
}
return &pamTestEnv{
pamDir: pamDir,
greetdPath: filepath.Join(pamDir, "greetd"),
dankshellPath: filepath.Join(pamDir, "dankshell"),
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
tmpDir: tmpDir,
homeDir: homeDir,
availableModules: map[string]bool{},
}
}
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
t.Helper()
writeTestFile(t, filepath.Join(e.pamDir, name), content)
@@ -64,59 +28,6 @@ func (e *pamTestEnv) writeSettings(t *testing.T, content string) {
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
}
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
return syncDeps{
pamDir: e.pamDir,
greetdPath: e.greetdPath,
dankshellPath: e.dankshellPath,
dankshellU2fPath: e.dankshellU2fPath,
isNixOS: func() bool { return isNixOS },
readFile: os.ReadFile,
stat: os.Stat,
createTemp: func(_ string, pattern string) (*os.File, error) {
return os.CreateTemp(e.tmpDir, pattern)
},
removeFile: os.Remove,
runSudoCmd: func(_ string, command string, args ...string) error {
switch command {
case "cp":
if len(args) != 2 {
return fmt.Errorf("unexpected cp args: %v", args)
}
data, err := os.ReadFile(args[0])
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
return err
}
return os.WriteFile(args[1], data, 0o644)
case "chmod":
if len(args) != 2 {
return fmt.Errorf("unexpected chmod args: %v", args)
}
return nil
case "rm":
if len(args) != 2 || args[0] != "-f" {
return fmt.Errorf("unexpected rm args: %v", args)
}
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
return err
}
return nil
default:
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
}
},
pamModuleExists: func(module string) bool {
return e.availableModules[module]
},
fingerprintAvailableForCurrentUser: func() bool {
return e.fingerprintAvailable
},
}
}
func readFileString(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
@@ -703,98 +614,6 @@ func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) {
})
}
func TestSyncGreeterPamConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("adds managed block for enabled auth modules", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true}
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
for _, want := range []string{
GreeterPamManagedBlockStart,
"auth sufficient pam_fprintd.so max-tries=2 timeout=10",
"auth sufficient pam_u2f.so cue nouserok timeout=10",
GreeterPamManagedBlockEnd,
} {
if !strings.Contains(got, want) {
t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got)
}
}
if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") {
t.Fatalf("managed block was not inserted before first auth line:\n%s", got)
}
})
t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.fingerprintAvailable = true
original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
env.writePamFile(t, "greetd", original)
env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n")
settings := AuthSettings{GreeterEnableFprint: true}
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
if got != original {
t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original)
}
if strings.Contains(got, GreeterPamManagedBlockStart) {
t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got)
}
})
}
func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "greetd", "#%PAM-1.0\n"+
legacyGreeterPamFprintComment+"\n"+
"auth sufficient pam_fprintd.so max-tries=1\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_u2f.so cue nouserok timeout=10\n"+
GreeterPamManagedBlockEnd+"\n"+
"auth include system-auth\n")
if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil {
t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) {
t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got)
}
if !strings.Contains(got, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got)
}
}
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: []string{e.pamDir},
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: func(module string) bool { return e.availableModules[module] },
}
}
func TestListLockscreenPamServices(t *testing.T) {
t.Parallel()
@@ -1081,10 +900,98 @@ func containsSubstr(items []string, substr string) bool {
return false
}
type pamTestEnv struct {
pamDir string
dankshellPath string
dankshellU2fPath string
tmpDir string
homeDir string
availableModules map[string]bool
}
func newPamTestEnv(t *testing.T) *pamTestEnv {
t.Helper()
root := t.TempDir()
pamDir := filepath.Join(root, "pam.d")
tmpDir := filepath.Join(root, "tmp")
homeDir := filepath.Join(root, "home")
for _, dir := range []string{pamDir, tmpDir, homeDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("failed to create %s: %v", dir, err)
}
}
return &pamTestEnv{
pamDir: pamDir,
dankshellPath: filepath.Join(pamDir, "dankshell"),
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
tmpDir: tmpDir,
homeDir: homeDir,
availableModules: map[string]bool{},
}
}
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
return syncDeps{
pamDir: e.pamDir,
dankshellPath: e.dankshellPath,
dankshellU2fPath: e.dankshellU2fPath,
isNixOS: func() bool { return isNixOS },
readFile: os.ReadFile,
stat: os.Stat,
createTemp: func(_ string, pattern string) (*os.File, error) {
return os.CreateTemp(e.tmpDir, pattern)
},
removeFile: os.Remove,
runSudoCmd: func(_ string, command string, args ...string) error {
switch command {
case "cp":
if len(args) != 2 {
return fmt.Errorf("unexpected cp args: %v", args)
}
data, err := os.ReadFile(args[0])
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
return err
}
return os.WriteFile(args[1], data, 0o644)
case "chmod":
if len(args) != 2 {
return fmt.Errorf("unexpected chmod args: %v", args)
}
return nil
case "rm":
if len(args) != 2 || args[0] != "-f" {
return fmt.Errorf("unexpected rm args: %v", args)
}
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
return err
}
return nil
default:
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
}
},
}
}
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: []string{e.pamDir},
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: func(module string) bool { return e.availableModules[module] },
}
}
func TestSyncAuthConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) {
t.Run("creates lockscreen targets", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
@@ -1092,10 +999,7 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
@@ -1106,105 +1010,24 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
}
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") {
t.Fatalf("expected greetd skip log, got %v", logs)
}
})
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) {
t.Run("removes dankshell-u2f when disabled", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`)
env.writeSettings(t, `{"enableU2f":false}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
dankshell := readFileString(t, env.dankshellPath)
if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") {
t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err)
}
greetd := readFileString(t, env.greetdPath)
if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=2 timeout=10") {
t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd)
}
if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") {
t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd)
}
})
t.Run("externally managed greetd is stripped and greeter sync skipped", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.writeSettings(t, `{"greeterPamExternallyManaged":true,"greeterEnableFprint":true}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_fprintd.so max-tries=2 timeout=10\n"+
GreeterPamManagedBlockEnd+"\n")
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
greetd := readFileString(t, env.greetdPath)
if strings.Contains(greetd, GreeterPamManagedBlockStart) || strings.Contains(greetd, "pam_fprintd") {
t.Fatalf("expected DMS-managed block stripped from externally managed greetd:\n%s", greetd)
}
if !strings.Contains(greetd, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd lines to remain:\n%s", greetd)
}
if !containsSubstr(logs, "externally managed") {
t.Fatalf("expected externally-managed skip log, got %v", logs)
}
})
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`)
originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
env.writePamFile(t, "greetd", originalGreetd)
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err)
}
if got := readFileString(t, env.greetdPath); got != originalGreetd {
t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd)
}
if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") {
t.Fatalf("expected informational NixOS logs, got %v", logs)
t.Fatalf("expected dankshell-u2f to be removed, stat err = %v", err)
}
})
}
@@ -34,7 +34,7 @@ import (
"unsafe"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
)
func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) {
@@ -1,25 +0,0 @@
package qmlchecks
import (
"os"
"strings"
"testing"
)
func TestGreeterExternalAuthStatusUsesEffectiveFingerprintAvailability(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Greetd/GreeterContent.qml")
if err != nil {
t.Fatalf("read greeter QML: %v", err)
}
content := string(data)
for _, required := range []string{
"readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f",
"if (greeterPamHasFprint && greeterPamHasU2f)",
"if (greeterPamHasFprint)",
} {
if !strings.Contains(content, required) {
t.Fatalf("greeter external-auth status must contain %q", required)
}
}
}
+2 -4
View File
@@ -1,14 +1,12 @@
package apppicker
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/desktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "apppicker.open", "browser.open":
handleOpen(conn, req, manager)
@@ -17,7 +15,7 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleOpen(conn net.Conn, req models.Request, manager *Manager) {
func handleOpen(conn *models.Conn, req models.Request, manager *Manager) {
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
target, ok := models.Get[string](req, "target")
+1 -1
View File
@@ -3,7 +3,7 @@ package apppicker
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
)
type Manager struct {
+17 -19
View File
@@ -1,12 +1,10 @@
package bluez
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
"github.com/AvengeMedia/dankgo/ipc/params"
)
type BluetoothEvent struct {
@@ -14,7 +12,7 @@ type BluetoothEvent struct {
Data BluetoothState `json:"data"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "bluetooth.getState":
handleGetState(conn, req, manager)
@@ -47,11 +45,11 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleStartDiscovery(conn net.Conn, req models.Request, manager *Manager) {
func handleStartDiscovery(conn *models.Conn, req models.Request, manager *Manager) {
if err := manager.StartDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -59,7 +57,7 @@ func handleStartDiscovery(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery started"})
}
func handleStopDiscovery(conn net.Conn, req models.Request, manager *Manager) {
func handleStopDiscovery(conn *models.Conn, req models.Request, manager *Manager) {
if err := manager.StopDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -67,7 +65,7 @@ func handleStopDiscovery(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery stopped"})
}
func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) {
func handleSetPowered(conn *models.Conn, req models.Request, manager *Manager) {
powered, err := params.Bool(req.Params, "powered")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -82,7 +80,7 @@ func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "powered state updated"})
}
func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) {
func handlePairDevice(conn *models.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -97,7 +95,7 @@ func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing initiated"})
}
func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) {
func handleConnectDevice(conn *models.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -112,7 +110,7 @@ func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
}
func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager) {
func handleDisconnectDevice(conn *models.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -127,7 +125,7 @@ func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
}
func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) {
func handleRemoveDevice(conn *models.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -142,7 +140,7 @@ func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device removed"})
}
func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) {
func handleTrustDevice(conn *models.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -157,7 +155,7 @@ func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device trusted"})
}
func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) {
func handleUntrustDevice(conn *models.Conn, req models.Request, manager *Manager) {
devicePath, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -172,7 +170,7 @@ func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device untrusted"})
}
func handlePairingSubmit(conn net.Conn, req models.Request, manager *Manager) {
func handlePairingSubmit(conn *models.Conn, req models.Request, manager *Manager) {
token, err := params.String(req.Params, "token")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -190,7 +188,7 @@ func handlePairingSubmit(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing response submitted"})
}
func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) {
func handlePairingCancel(conn *models.Conn, req models.Request, manager *Manager) {
token, err := params.String(req.Params, "token")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -205,7 +203,7 @@ func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing cancelled"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
@@ -216,7 +214,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
Data: initialState,
}
if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
if err := conn.WriteResponse(models.Response[BluetoothEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
@@ -228,7 +226,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
Type: "state_changed",
Data: state,
}
if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
if err := conn.WriteResponse(models.Response[BluetoothEvent]{
Result: &event,
}); err != nil {
return
+12 -7
View File
@@ -7,7 +7,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/dankgo/dbusutil"
"github.com/godbus/dbus/v5"
)
@@ -43,10 +43,10 @@ func NewManager() (*Manager, error) {
broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
m.promptBroker = broker
adapter, err := m.findAdapter()
adapter, err := findAdapter(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("no bluetooth adapter found: %w", err)
return nil, err
}
m.adapterPath = adapter
@@ -74,12 +74,12 @@ func NewManager() (*Manager, error) {
return m, nil
}
func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/"))
func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) {
obj := conn.Object(bluezService, dbus.ObjectPath("/"))
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
return "", err
return "", fmt.Errorf("%w: %v", ErrNoAdapter, err)
}
for path, interfaces := range objects {
@@ -89,7 +89,7 @@ func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
}
}
return "", fmt.Errorf("no adapter found")
return "", ErrNoAdapter
}
func (m *Manager) initialize() error {
@@ -487,6 +487,11 @@ func (m *Manager) StopDiscovery() error {
}
func (m *Manager) SetPowered(powered bool) error {
if powered {
if err := rfkillUnblockBluetooth(); err != nil {
log.Debugf("[BluezManager] rfkill unblock failed: %v", err)
}
}
obj := m.dbusConn.Object(bluezService, m.adapterPath)
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
}
@@ -0,0 +1,5 @@
package bluez
func rfkillUnblockBluetooth() error {
return nil
}
@@ -0,0 +1,19 @@
package bluez
import "os"
// linux/rfkill.h: struct rfkill_event { __u32 idx; __u8 type; __u8 op; __u8 soft; __u8 hard; },
// RFKILL_TYPE_BLUETOOTH=2, RFKILL_OP_CHANGE_ALL=3
func rfkillUnblockBluetooth() error {
f, err := os.OpenFile("/dev/rfkill", os.O_WRONLY, 0)
if err != nil {
return err
}
defer f.Close()
var event [8]byte
event[4] = 2
event[5] = 3
_, err = f.Write(event[:])
return err
}
@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
)
type SubscriptionBroker struct {
+1 -1
View File
@@ -3,7 +3,7 @@ package bluez
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/godbus/dbus/v5"
)
+46
View File
@@ -0,0 +1,46 @@
package bluez
import (
"errors"
"fmt"
"github.com/godbus/dbus/v5"
)
var ErrNoAdapter = errors.New("no bluetooth adapter found")
func WaitForAdapter() error {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return err
}
defer conn.Close()
if err := conn.AddMatchSignal(
dbus.WithMatchInterface(objectMgrIface),
dbus.WithMatchMember("InterfacesAdded"),
); err != nil {
return err
}
signals := make(chan *dbus.Signal, 64)
conn.Signal(signals)
if _, err := findAdapter(conn); err == nil {
return nil
}
for sig := range signals {
if sig == nil || sig.Name != objectMgrIface+".InterfacesAdded" || len(sig.Body) < 2 {
continue
}
ifaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant)
if !ok {
continue
}
if _, ok := ifaces[adapter1Iface]; ok {
return nil
}
}
return fmt.Errorf("dbus signal stream closed")
}
@@ -0,0 +1,167 @@
package brightness
import (
"fmt"
"math"
"os"
"path/filepath"
"strings"
"unsafe"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/syncmap"
"golang.org/x/sys/unix"
)
// sys/sys/backlight.h: brightness is a 0-100 percent;
// BACKLIGHTGETSTATUS/BACKLIGHTUPDATESTATUS = _IOWR('G', 0/1, struct
// backlight_props{uint32 brightness; uint32 nlevels; uint32 levels[100]}).
const (
backlightDevDir = "/dev/backlight"
backlightGetStatus = 0xc1984700
backlightUpdateStatus = 0xc1984701
)
type backlightProps struct {
brightness uint32
nlevels uint32
levels [100]uint32
}
type BacklightBackend struct {
devices syncmap.Map[string, string]
}
func NewBacklightBackend() (*BacklightBackend, error) {
b := &BacklightBackend{}
if err := b.scanDevices(); err != nil {
return nil, err
}
return b, nil
}
func isGenericBacklightName(name string) bool {
rest, ok := strings.CutPrefix(name, "backlight")
if !ok || rest == "" {
return false
}
for _, r := range rest {
if r < '0' || r > '9' {
return false
}
}
return true
}
func (b *BacklightBackend) scanDevices() error {
entries, err := os.ReadDir(backlightDevDir)
if err != nil {
return fmt.Errorf("read %s: %w", backlightDevDir, err)
}
// backlight_register (sys/dev/backlight/backlight.c) publishes each unit
// as backlight/backlightN plus a driver-named alias for the same cdev;
// dedupe on the device number and keep the descriptive alias.
names := make(map[uint64]string)
for _, entry := range entries {
var st unix.Stat_t
if err := unix.Stat(filepath.Join(backlightDevDir, entry.Name()), &st); err != nil {
continue
}
rdev := uint64(st.Rdev)
current, exists := names[rdev]
if exists && !isGenericBacklightName(current) {
continue
}
names[rdev] = entry.Name()
}
for _, name := range names {
b.devices.Store("backlight:"+name, filepath.Join(backlightDevDir, name))
}
return nil
}
func backlightIoctl(fd int, req uint, props *backlightProps) error {
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(unsafe.Pointer(props)))
if errno != 0 {
return errno
}
return nil
}
func readBrightness(path string) (int, error) {
fd, err := unix.Open(path, unix.O_RDWR, 0)
if err != nil {
return 0, err
}
defer unix.Close(fd)
var props backlightProps
if err := backlightIoctl(fd, backlightGetStatus, &props); err != nil {
return 0, err
}
return int(props.brightness), nil
}
func (b *BacklightBackend) Rescan() error {
return b.scanDevices()
}
func (b *BacklightBackend) GetDevices() ([]Device, error) {
devices := make([]Device, 0)
b.devices.Range(func(id, path string) bool {
brightness, err := readBrightness(path)
if err != nil {
log.Debugf("failed to read brightness for %s: %v", id, err)
return true
}
devices = append(devices, Device{
Class: ClassBacklight,
ID: id,
Name: strings.TrimPrefix(id, "backlight:"),
Current: brightness,
Max: 100,
CurrentPercent: brightness,
Backend: "backlight",
})
return true
})
return devices, nil
}
func (b *BacklightBackend) SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error {
if percent < 0 || percent > 100 {
return fmt.Errorf("percent out of range: %d", percent)
}
path, ok := b.devices.Load(id)
if !ok {
return fmt.Errorf("device not found: %s", id)
}
value := percent
switch {
case percent == 0:
value = 1
case exponential:
value = 1 + int(math.Round(math.Pow(float64(percent-1)/99.0, exponent)*99.0))
}
fd, err := unix.Open(path, unix.O_RDWR, 0)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer unix.Close(fd)
props := backlightProps{brightness: uint32(value)}
if err := backlightIoctl(fd, backlightUpdateStatus, &props); err != nil {
return fmt.Errorf("set brightness: %w", err)
}
log.Debugf("set %s to %d%% (hw %d) via backlight(4)", id, percent, value)
return nil
}
@@ -0,0 +1,162 @@
package brightness
import (
"net"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
// devd(8) publishes device events on this SOCK_SEQPACKET socket, one
// "!system=... subsystem=... type=..." record per packet.
const devdSocketPath = "/var/run/devd.seqpacket.pipe"
const (
devdMaxRetries = 5
devdBaseDelay = 2 * time.Second
devdMaxDelay = 60 * time.Second
)
type DevdMonitor struct {
stop chan struct{}
rescanMutex sync.Mutex
rescanTimer *time.Timer
rescanPending bool
}
func newDevdMonitor(manager *Manager) *DevdMonitor {
m := &DevdMonitor{
stop: make(chan struct{}),
}
go m.run(manager)
return m
}
func (m *DevdMonitor) run(manager *Manager) {
failures := 0
for {
if err := m.monitorLoop(manager); err != nil {
log.Errorf("Devd monitor error: %v", err)
}
select {
case <-m.stop:
return
default:
}
failures++
if failures > devdMaxRetries {
log.Errorf("Devd monitor exceeded %d retries, giving up", devdMaxRetries)
return
}
delay := min(devdBaseDelay*time.Duration(1<<(failures-1)), devdMaxDelay)
log.Infof("Devd monitor reconnecting in %v (attempt %d/%d)", delay, failures, devdMaxRetries)
select {
case <-m.stop:
return
case <-time.After(delay):
}
}
}
func (m *DevdMonitor) monitorLoop(manager *Manager) error {
conn, err := net.Dial("unixpacket", devdSocketPath)
if err != nil {
return err
}
defer conn.Close()
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-m.stop:
conn.Close()
case <-done:
}
}()
log.Info("Devd monitor started for backlight/drm events")
buf := make([]byte, 8192)
for {
n, err := conn.Read(buf)
if err != nil {
select {
case <-m.stop:
return nil
default:
return err
}
}
m.handleEvent(manager, string(buf[:n]))
}
}
func (m *DevdMonitor) handleEvent(manager *Manager, event string) {
notification, ok := strings.CutPrefix(event, "!")
if !ok {
return
}
fields := parseDevdEvent(notification)
switch fields["system"] {
case "DRM":
m.debouncedRescan(manager)
case "DEVFS":
if fields["subsystem"] != "CDEV" {
return
}
if !strings.HasPrefix(fields["cdev"], "backlight/") {
return
}
m.debouncedRescan(manager)
}
}
func parseDevdEvent(s string) map[string]string {
fields := make(map[string]string)
for _, part := range strings.Fields(s) {
k, v, ok := strings.Cut(part, "=")
if !ok {
continue
}
fields[k] = v
}
return fields
}
func (m *DevdMonitor) debouncedRescan(manager *Manager) {
m.rescanMutex.Lock()
defer m.rescanMutex.Unlock()
m.rescanPending = true
if m.rescanTimer != nil {
m.rescanTimer.Reset(2 * time.Second)
return
}
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
m.rescanMutex.Lock()
pending := m.rescanPending
m.rescanPending = false
m.rescanMutex.Unlock()
if !pending {
return
}
manager.Rescan()
})
}
func (m *DevdMonitor) Close() {
close(m.stop)
}
+10 -12
View File
@@ -1,15 +1,13 @@
package brightness
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
"github.com/AvengeMedia/dankgo/ipc/params"
)
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
switch req.Method {
case "brightness.getState":
handleGetState(conn, req, m)
@@ -28,11 +26,11 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) {
func handleSetBrightness(conn *models.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -56,7 +54,7 @@ func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleIncrement(conn net.Conn, req models.Request, m *Manager) {
func handleIncrement(conn *models.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -75,7 +73,7 @@ func handleIncrement(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleDecrement(conn net.Conn, req models.Request, m *Manager) {
func handleDecrement(conn *models.Conn, req models.Request, m *Manager) {
device, err := params.String(req.Params, "device")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -94,19 +92,19 @@ func handleDecrement(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleRescan(conn net.Conn, req models.Request, m *Manager) {
func handleRescan(conn *models.Conn, req models.Request, m *Manager) {
m.Rescan()
models.Respond(conn, req.ID, m.GetState())
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("brightness-%d", req.ID)
ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID)
initialState := m.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
if err := conn.WriteResponse(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
@@ -114,7 +112,7 @@ func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
}
for state := range ch {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
if err := conn.WriteResponse(models.Response[State]{
ID: req.ID,
Result: &state,
}); err != nil {
+15 -45
View File
@@ -20,7 +20,7 @@ func NewManagerWithOptions(exponential bool) (*Manager, error) {
}
go m.initLogind()
go m.initSysfs()
go m.initNative()
go m.initDDC()
return m, nil
@@ -40,39 +40,6 @@ func (m *Manager) initLogind() {
log.Info("Logind backend initialized - will use for brightness control")
}
func (m *Manager) initSysfs() {
log.Debug("Initializing sysfs backend...")
sysfs, err := NewSysfsBackend()
if err != nil {
log.Warnf("Failed to initialize sysfs backend: %v", err)
return
}
devices, err := sysfs.GetDevices()
if err != nil {
log.Warnf("Failed to get initial sysfs devices: %v", err)
m.sysfsBackend = sysfs
m.sysfsReady = true
m.updateState()
m.initUdev()
return
}
log.Infof("Sysfs backend initialized with %d devices", len(devices))
for _, d := range devices {
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
}
m.sysfsBackend = sysfs
m.sysfsReady = true
m.updateState()
m.initUdev()
}
func (m *Manager) initUdev() {
m.udevMonitor = NewUdevMonitor(m)
}
func (m *Manager) initDDC() {
ddc, err := NewDDCBackend()
if err != nil {
@@ -96,9 +63,9 @@ func (m *Manager) Rescan() {
}
}
if m.sysfsReady && m.sysfsBackend != nil {
if err := m.sysfsBackend.Rescan(); err != nil {
log.Debugf("Sysfs rescan failed: %v", err)
if m.nativeReady && m.nativeBackend != nil {
if err := m.nativeBackend.Rescan(); err != nil {
log.Debugf("Native backend rescan failed: %v", err)
}
}
@@ -150,10 +117,10 @@ func stateChanged(old, new State) bool {
func (m *Manager) updateState() {
allDevices := make([]Device, 0)
if m.sysfsReady && m.sysfsBackend != nil {
devices, err := m.sysfsBackend.GetDevices()
if m.nativeReady && m.nativeBackend != nil {
devices, err := m.nativeBackend.GetDevices()
if err != nil {
log.Debugf("Failed to get sysfs devices: %v", err)
log.Debugf("Failed to get native backend devices: %v", err)
}
if err == nil {
allDevices = append(allDevices, devices...)
@@ -232,18 +199,21 @@ func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, expone
m.stateMutex.Unlock()
var err error
if deviceClass == ClassDDC {
switch {
case deviceClass == ClassDDC:
log.Debugf("Calling DDC backend for %s", deviceID)
err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() {
m.updateState()
m.debouncedBroadcast(deviceID)
})
} else if m.logindReady && m.logindBackend != nil {
case m.logindReady && m.logindBackend != nil:
log.Debugf("Calling logind backend for %s", deviceID)
err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent)
} else {
log.Debugf("Calling sysfs backend for %s", deviceID)
err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
case m.nativeBackend != nil:
log.Debugf("Calling native backend for %s", deviceID)
err = m.nativeBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
default:
err = fmt.Errorf("no brightness backend for %s", deviceID)
}
if err != nil {
@@ -0,0 +1,24 @@
package brightness
import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
func (m *Manager) initNative() {
log.Debug("Initializing backlight backend...")
backend, err := NewBacklightBackend()
if err != nil {
log.Warnf("Failed to initialize backlight backend: %v", err)
return
}
devices, err := backend.GetDevices()
if err == nil {
log.Infof("Backlight backend initialized with %d devices", len(devices))
}
m.nativeBackend = backend
m.nativeReady = true
m.updateState()
m.monitor = newDevdMonitor(m)
}
@@ -0,0 +1,36 @@
package brightness
import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
func (m *Manager) initNative() {
log.Debug("Initializing sysfs backend...")
sysfs, err := NewSysfsBackend()
if err != nil {
log.Warnf("Failed to initialize sysfs backend: %v", err)
return
}
devices, err := sysfs.GetDevices()
if err != nil {
log.Warnf("Failed to get initial sysfs devices: %v", err)
m.sysfsBackend = sysfs
m.nativeBackend = sysfs
m.nativeReady = true
m.updateState()
m.monitor = NewUdevMonitor(m)
return
}
log.Infof("Sysfs backend initialized with %d devices", len(devices))
for _, d := range devices {
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
}
m.sysfsBackend = sysfs
m.nativeBackend = sysfs
m.nativeReady = true
m.updateState()
m.monitor = NewUdevMonitor(m)
}
@@ -43,7 +43,7 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
nativeReady: true,
stopChan: make(chan struct{}),
}
@@ -114,7 +114,7 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
nativeReady: true,
stopChan: make(chan struct{}),
}
@@ -180,8 +180,9 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
m := &Manager{
logindBackend: nil,
sysfsBackend: sysfs,
nativeBackend: sysfs,
logindReady: false,
sysfsReady: true,
nativeReady: true,
stopChan: make(chan struct{}),
}
@@ -243,7 +244,7 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
logindBackend: mockLogind,
sysfsBackend: sysfs,
logindReady: true,
sysfsReady: true,
nativeReady: true,
stopChan: make(chan struct{}),
}
+16 -5
View File
@@ -4,7 +4,7 @@ import (
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
)
type DeviceClass string
@@ -33,14 +33,25 @@ type DeviceUpdate struct {
Device Device `json:"device"`
}
type Backend interface {
Rescan() error
GetDevices() ([]Device, error)
SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error
}
type deviceMonitor interface {
Close()
}
type Manager struct {
logindBackend *LogindBackend
sysfsBackend *SysfsBackend
nativeBackend Backend
ddcBackend *DDCBackend
udevMonitor *UdevMonitor
monitor deviceMonitor
logindReady bool
sysfsReady bool
nativeReady bool
ddcReady bool
exponential bool
@@ -170,8 +181,8 @@ func (m *Manager) Close() {
return true
})
if m.udevMonitor != nil {
m.udevMonitor.Close()
if m.monitor != nil {
m.monitor.Close()
}
if m.logindBackend != nil {
@@ -33,7 +33,7 @@ func setupTestManager(t *testing.T) (*Manager, string) {
m := &Manager{
sysfsBackend: sysfs,
sysfsReady: true,
nativeReady: true,
stopChan: make(chan struct{}),
}
+1 -3
View File
@@ -1,12 +1,10 @@
package browser
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "browser.open":
url, ok := models.Get[string](req, "url")
+1 -1
View File
@@ -3,7 +3,7 @@ package browser
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
)
type Manager struct {
+23 -25
View File
@@ -1,17 +1,15 @@
package clipboard
import (
"encoding/json"
"errors"
"fmt"
"net"
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
"github.com/AvengeMedia/dankgo/ipc/params"
)
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
switch req.Method {
case "clipboard.getState":
handleGetState(conn, req, m)
@@ -58,11 +56,11 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
func handleGetHistory(conn net.Conn, req models.Request, m *Manager) {
func handleGetHistory(conn *models.Conn, req models.Request, m *Manager) {
history := m.GetHistory()
for i := range history {
history[i].Data = nil
@@ -70,7 +68,7 @@ func handleGetHistory(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, history)
}
func handleGetEntry(conn net.Conn, req models.Request, m *Manager) {
func handleGetEntry(conn *models.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -90,7 +88,7 @@ func handleGetEntry(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, entry)
}
func handleDeleteEntry(conn net.Conn, req models.Request, m *Manager) {
func handleDeleteEntry(conn *models.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -105,12 +103,12 @@ func handleDeleteEntry(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry deleted"})
}
func handleClearHistory(conn net.Conn, req models.Request, m *Manager) {
func handleClearHistory(conn *models.Conn, req models.Request, m *Manager) {
m.ClearHistory()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "history cleared"})
}
func handleCopy(conn net.Conn, req models.Request, m *Manager) {
func handleCopy(conn *models.Conn, req models.Request, m *Manager) {
text, err := params.String(req.Params, "text")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -125,7 +123,7 @@ func handleCopy(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
}
func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
func handleCopyEntry(conn *models.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -173,7 +171,7 @@ func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
}
func handlePaste(conn net.Conn, req models.Request, m *Manager) {
func handlePaste(conn *models.Conn, req models.Request, m *Manager) {
text, err := m.PasteText()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -183,7 +181,7 @@ func handlePaste(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, map[string]string{"text": text})
}
func handleSendPaste(conn net.Conn, req models.Request) {
func handleSendPaste(conn *models.Conn, req models.Request) {
shift, _ := models.Get[bool](req, "shift")
if err := clipboardstore.SendPasteKeystroke(shift); err != nil {
@@ -194,14 +192,14 @@ func handleSendPaste(conn net.Conn, req models.Request) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paste sent"})
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("clipboard-%d", req.ID)
ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID)
initialState := m.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
if err := conn.WriteResponse(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
@@ -209,7 +207,7 @@ func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
}
for state := range ch {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
if err := conn.WriteResponse(models.Response[State]{
ID: req.ID,
Result: &state,
}); err != nil {
@@ -218,7 +216,7 @@ func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
}
}
func handleSearch(conn net.Conn, req models.Request, m *Manager) {
func handleSearch(conn *models.Conn, req models.Request, m *Manager) {
p := SearchParams{
Query: params.StringOpt(req.Params, "query", ""),
MimeType: params.StringOpt(req.Params, "mimeType", ""),
@@ -241,11 +239,11 @@ func handleSearch(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.Search(p))
}
func handleGetConfig(conn net.Conn, req models.Request, m *Manager) {
func handleGetConfig(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetConfig())
}
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
func handleSetConfig(conn *models.Conn, req models.Request, m *Manager) {
cfg := m.GetConfig()
if v, ok := models.Get[float64](req, "maxHistory"); ok {
@@ -275,7 +273,7 @@ func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
}
func handleStore(conn net.Conn, req models.Request, m *Manager) {
func handleStore(conn *models.Conn, req models.Request, m *Manager) {
data, err := params.String(req.Params, "data")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -292,7 +290,7 @@ func handleStore(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
}
func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
func handlePinEntry(conn *models.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -307,7 +305,7 @@ func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
}
func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
func handleUnpinEntry(conn *models.Conn, req models.Request, m *Manager) {
id, err := params.Int(req.Params, "id")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -322,17 +320,17 @@ func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
}
func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
func handleGetPinnedEntries(conn *models.Conn, req models.Request, m *Manager) {
pinned := m.GetPinnedEntries()
models.Respond(conn, req.ID, pinned)
}
func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
func handleGetPinnedCount(conn *models.Conn, req models.Request, m *Manager) {
count := m.GetPinnedCount()
models.Respond(conn, req.ID, map[string]int{"count": count})
}
func handleCopyFile(conn net.Conn, req models.Request, m *Manager) {
func handleCopyFile(conn *models.Conn, req models.Request, m *Manager) {
filePath, err := params.String(req.Params, "filePath")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -311,14 +311,15 @@ func TestHandleGetEntry_ReturnsExistingEntry(t *testing.T) {
history := m.GetHistory()
require.Len(t, history, 1)
conn := newClipboardTestConn()
mc := newClipboardTestConn()
conn := models.NewConn(mc)
handleGetEntry(conn, models.Request{
ID: 1,
Params: map[string]any{"id": float64(history[0].ID)},
}, m)
var resp models.Response[Entry]
require.NoError(t, json.NewDecoder(conn.writeBuf).Decode(&resp))
require.NoError(t, json.NewDecoder(mc.writeBuf).Decode(&resp))
assert.Empty(t, resp.Error)
require.NotNil(t, resp.Result)
assert.Equal(t, history[0].ID, resp.Result.ID)
@@ -327,7 +328,8 @@ func TestHandleGetEntry_ReturnsExistingEntry(t *testing.T) {
func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
m := newTestManagerWithDB(t)
conn := newClipboardTestConn()
mc := newClipboardTestConn()
conn := models.NewConn(mc)
handleGetEntry(conn, models.Request{
ID: 1,
@@ -335,7 +337,7 @@ func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
}, m)
var resp models.Response[any]
require.NoError(t, json.NewDecoder(conn.writeBuf).Decode(&resp))
require.NoError(t, json.NewDecoder(mc.writeBuf).Decode(&resp))
assert.Empty(t, resp.Error)
assert.Nil(t, resp.Result)
}
+29 -31
View File
@@ -1,12 +1,10 @@
package cups
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
"github.com/AvengeMedia/dankgo/ipc/params"
)
type CUPSEvent struct {
@@ -20,7 +18,7 @@ type TestPageResult struct {
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "cups.subscribe":
handleSubscribe(conn, req, manager)
@@ -77,7 +75,7 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) {
func handleGetPrinters(conn *models.Conn, req models.Request, manager *Manager) {
printers, err := manager.GetPrinters()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -86,7 +84,7 @@ func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, printers)
}
func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
func handleGetJobs(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -101,7 +99,7 @@ func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, jobs)
}
func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
func handlePausePrinter(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -115,7 +113,7 @@ func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paused"})
}
func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
func handleResumePrinter(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -129,7 +127,7 @@ func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "resumed"})
}
func handleCancelJob(conn net.Conn, req models.Request, manager *Manager) {
func handleCancelJob(conn *models.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -143,7 +141,7 @@ func handleCancelJob(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job canceled"})
}
func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
func handlePurgeJobs(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.String(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -157,7 +155,7 @@ func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "jobs canceled"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
@@ -168,7 +166,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
Data: initialState,
}
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
if err := conn.WriteResponse(models.Response[CUPSEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
@@ -180,7 +178,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
Type: "state_changed",
Data: state,
}
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
if err := conn.WriteResponse(models.Response[CUPSEvent]{
Result: &event,
}); err != nil {
return
@@ -188,7 +186,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
func handleGetDevices(conn *models.Conn, req models.Request, manager *Manager) {
devices, err := manager.GetDevices()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -197,7 +195,7 @@ func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, devices)
}
func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
func handleGetPPDs(conn *models.Conn, req models.Request, manager *Manager) {
ppds, err := manager.GetPPDs()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -206,7 +204,7 @@ func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, ppds)
}
func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
func handleGetClasses(conn *models.Conn, req models.Request, manager *Manager) {
classes, err := manager.GetClasses()
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -215,7 +213,7 @@ func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, classes)
}
func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) {
func handleCreatePrinter(conn *models.Conn, req models.Request, manager *Manager) {
name, err := params.StringNonEmpty(req.Params, "name")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -246,7 +244,7 @@ func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer created"})
}
func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
func handleDeletePrinter(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -260,7 +258,7 @@ func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer deleted"})
}
func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
func handleAcceptJobs(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -274,7 +272,7 @@ func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "accepting jobs"})
}
func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
func handleRejectJobs(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -288,7 +286,7 @@ func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "rejecting jobs"})
}
func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager) {
func handleSetPrinterShared(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -308,7 +306,7 @@ func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sharing updated"})
}
func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manager) {
func handleSetPrinterLocation(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -328,7 +326,7 @@ func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manage
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location updated"})
}
func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
func handleSetPrinterInfo(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -348,7 +346,7 @@ func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "info updated"})
}
func handleMoveJob(conn net.Conn, req models.Request, manager *Manager) {
func handleMoveJob(conn *models.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -368,7 +366,7 @@ func handleMoveJob(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job moved"})
}
func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
func handlePrintTestPage(conn *models.Conn, req models.Request, manager *Manager) {
printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -383,7 +381,7 @@ func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"})
}
func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager) {
func handleAddPrinterToClass(conn *models.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -403,7 +401,7 @@ func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer added to class"})
}
func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Manager) {
func handleRemovePrinterFromClass(conn *models.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -423,7 +421,7 @@ func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Ma
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer removed from class"})
}
func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
func handleDeleteClass(conn *models.Conn, req models.Request, manager *Manager) {
className, err := params.StringNonEmpty(req.Params, "className")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -437,7 +435,7 @@ func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "class deleted"})
}
func handleRestartJob(conn net.Conn, req models.Request, manager *Manager) {
func handleRestartJob(conn *models.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -451,7 +449,7 @@ func handleRestartJob(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job restarted"})
}
func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
func handleHoldJob(conn *models.Conn, req models.Request, manager *Manager) {
jobID, err := params.Int(req.Params, "jobID")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -467,7 +465,7 @@ func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
}
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
func handleTestConnection(conn *models.Conn, req models.Request, manager *Manager) {
host, err := params.StringNonEmpty(req.Params, "host")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
+28 -28
View File
@@ -41,7 +41,7 @@ func TestHandleGetPrinters(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -66,7 +66,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -98,7 +98,7 @@ func TestHandleGetJobs(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -125,7 +125,7 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -150,7 +150,7 @@ func TestHandlePausePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -177,7 +177,7 @@ func TestHandleResumePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -204,7 +204,7 @@ func TestHandleCancelJob(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -231,7 +231,7 @@ func TestHandlePurgeJobs(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -258,7 +258,7 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -285,7 +285,7 @@ func TestHandleGetDevices(t *testing.T) {
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{ID: 1, Method: "cups.getDevices"}
handleGetDevices(conn, req, m)
@@ -307,7 +307,7 @@ func TestHandleGetPPDs(t *testing.T) {
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{ID: 1, Method: "cups.getPPDs"}
handleGetPPDs(conn, req, m)
@@ -330,7 +330,7 @@ func TestHandleGetClasses(t *testing.T) {
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{ID: 1, Method: "cups.getClasses"}
handleGetClasses(conn, req, m)
@@ -351,7 +351,7 @@ func TestHandleCreatePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -375,7 +375,7 @@ func TestHandleCreatePrinter_MissingParams(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{client: mockClient}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
handleCreatePrinter(conn, req, m)
@@ -394,7 +394,7 @@ func TestHandleDeletePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -417,7 +417,7 @@ func TestHandleAcceptJobs(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -440,7 +440,7 @@ func TestHandleRejectJobs(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -463,7 +463,7 @@ func TestHandleSetPrinterShared(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -486,7 +486,7 @@ func TestHandleSetPrinterLocation(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -509,7 +509,7 @@ func TestHandleSetPrinterInfo(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -532,7 +532,7 @@ func TestHandleMoveJob(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -555,7 +555,7 @@ func TestHandlePrintTestPage(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -579,7 +579,7 @@ func TestHandleAddPrinterToClass(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -602,7 +602,7 @@ func TestHandleRemovePrinterFromClass(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -625,7 +625,7 @@ func TestHandleDeleteClass(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -648,7 +648,7 @@ func TestHandleRestartJob(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -671,7 +671,7 @@ func TestHandleHoldJob(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -694,7 +694,7 @@ func TestHandleHoldJob_WithHoldUntil(t *testing.T) {
m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -302,7 +302,7 @@ func TestHandleTestConnection_Success(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -325,7 +325,7 @@ func TestHandleTestConnection_Success(t *testing.T) {
func TestHandleTestConnection_MissingHost(t *testing.T) {
m := NewTestManager(nil, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -351,7 +351,7 @@ func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
@@ -379,7 +379,7 @@ func TestHandleRequest_TestConnection(t *testing.T) {
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
conn := models.NewConn(&mockConn{Buffer: buf})
req := models.Request{
ID: 1,
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
)
type CUPSState struct {
+10 -11
View File
@@ -2,10 +2,9 @@ package dbus
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
"github.com/AvengeMedia/dankgo/ipc/params"
)
type objectParams struct {
@@ -43,7 +42,7 @@ func extractObjectParams(p map[string]any, requirePath bool) (objectParams, erro
return objectParams{bus: bus, dest: dest, path: path, iface: iface}, nil
}
func HandleRequest(conn net.Conn, req models.Request, m *Manager, clientID string) {
func HandleRequest(conn *models.Conn, req models.Request, m *Manager, clientID string) {
switch req.Method {
case "dbus.call":
handleCall(conn, req, m)
@@ -66,7 +65,7 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager, clientID strin
}
}
func handleCall(conn net.Conn, req models.Request, m *Manager) {
func handleCall(conn *models.Conn, req models.Request, m *Manager) {
op, err := extractObjectParams(req.Params, true)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -95,7 +94,7 @@ func handleCall(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleGetProperty(conn net.Conn, req models.Request, m *Manager) {
func handleGetProperty(conn *models.Conn, req models.Request, m *Manager) {
op, err := extractObjectParams(req.Params, true)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -117,7 +116,7 @@ func handleGetProperty(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleSetProperty(conn net.Conn, req models.Request, m *Manager) {
func handleSetProperty(conn *models.Conn, req models.Request, m *Manager) {
op, err := extractObjectParams(req.Params, true)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -144,7 +143,7 @@ func handleSetProperty(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
}
func handleGetAllProperties(conn net.Conn, req models.Request, m *Manager) {
func handleGetAllProperties(conn *models.Conn, req models.Request, m *Manager) {
op, err := extractObjectParams(req.Params, true)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -160,7 +159,7 @@ func handleGetAllProperties(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleIntrospect(conn net.Conn, req models.Request, m *Manager) {
func handleIntrospect(conn *models.Conn, req models.Request, m *Manager) {
bus, err := params.String(req.Params, "bus")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -184,7 +183,7 @@ func handleIntrospect(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleListNames(conn net.Conn, req models.Request, m *Manager) {
func handleListNames(conn *models.Conn, req models.Request, m *Manager) {
bus, err := params.String(req.Params, "bus")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -200,7 +199,7 @@ func handleListNames(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, result)
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) {
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager, clientID string) {
if id := params.StringOpt(req.Params, "clientId", ""); id != "" {
clientID = id
}
@@ -225,7 +224,7 @@ func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID str
models.Respond(conn, req.ID, result)
}
func handleUnsubscribe(conn net.Conn, req models.Request, m *Manager) {
func handleUnsubscribe(conn *models.Conn, req models.Request, m *Manager) {
subID, err := params.String(req.Params, "subscriptionId")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/dankgo/dbusutil"
"github.com/godbus/dbus/v5"
)
+1 -1
View File
@@ -1,7 +1,7 @@
package dbus
import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/godbus/dbus/v5"
)
+2 -4
View File
@@ -1,12 +1,10 @@
package evdev
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
switch req.Method {
case "evdev.getState":
handleGetState(conn, req, m)
@@ -15,6 +13,6 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, m.GetState())
}
+9 -6
View File
@@ -52,7 +52,8 @@ func TestHandleRequest(t *testing.T) {
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "evdev.getState",
@@ -62,7 +63,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, m)
var resp models.Response[State]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -81,7 +82,8 @@ func TestHandleRequest(t *testing.T) {
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 456,
Method: "evdev.unknownMethod",
@@ -91,7 +93,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, m)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 456, resp.ID)
@@ -110,7 +112,8 @@ func TestHandleGetState(t *testing.T) {
closeChan: make(chan struct{}),
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 789,
Method: "evdev.getState",
@@ -120,7 +123,7 @@ func TestHandleGetState(t *testing.T) {
handleGetState(conn, req, m)
var resp models.Response[State]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 789, resp.ID)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/fsnotify/fsnotify"
evdev "github.com/holoplot/go-evdev"
)
+11 -12
View File
@@ -2,13 +2,12 @@ package freedesktop
import (
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
"github.com/AvengeMedia/dankgo/ipc/params"
)
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "freedesktop.getState":
handleGetState(conn, req, manager)
@@ -33,11 +32,11 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
func handleSetIconFile(conn *models.Conn, req models.Request, manager *Manager) {
iconPath, err := params.String(req.Params, "path")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -52,7 +51,7 @@ func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon file set"})
}
func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
func handleSetRealName(conn *models.Conn, req models.Request, manager *Manager) {
name, err := params.String(req.Params, "name")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -67,7 +66,7 @@ func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "real name set"})
}
func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
func handleSetEmail(conn *models.Conn, req models.Request, manager *Manager) {
email, err := params.String(req.Params, "email")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -82,7 +81,7 @@ func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "email set"})
}
func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
func handleSetLanguage(conn *models.Conn, req models.Request, manager *Manager) {
language, err := params.String(req.Params, "language")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -97,7 +96,7 @@ func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "language set"})
}
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
func handleSetLocation(conn *models.Conn, req models.Request, manager *Manager) {
location, err := params.String(req.Params, "location")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -112,7 +111,7 @@ func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
}
func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager) {
func handleGetUserIconFile(conn *models.Conn, req models.Request, manager *Manager) {
username, err := params.String(req.Params, "username")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -128,7 +127,7 @@ func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager)
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Value: iconFile})
}
func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
func handleGetColorScheme(conn *models.Conn, req models.Request, manager *Manager) {
if err := manager.updateSettingsState(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
@@ -138,7 +137,7 @@ func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme})
}
func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) {
func handleSetIconTheme(conn *models.Conn, req models.Request, manager *Manager) {
iconTheme, err := params.String(req.Params, "iconTheme")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
@@ -60,11 +60,12 @@ func mockGetAllAccountsProperties() *dbus.Call {
}
func TestRespondError_Freedesktop(t *testing.T) {
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
models.RespondError(conn, 123, "test error")
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -73,12 +74,13 @@ func TestRespondError_Freedesktop(t *testing.T) {
}
func TestRespond_Freedesktop(t *testing.T) {
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -105,13 +107,14 @@ func TestHandleGetState(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ID: 123, Method: "freedesktop.getState"}
handleGetState(conn, req, manager)
var resp models.Response[FreedeskState]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -130,7 +133,8 @@ func TestHandleSetIconFile(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
@@ -140,7 +144,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -163,7 +167,8 @@ func TestHandleSetIconFile(t *testing.T) {
accountsObj: mockAccountsObj,
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
@@ -175,7 +180,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -195,7 +200,8 @@ func TestHandleSetIconFile(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setIconFile",
@@ -207,7 +213,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -222,7 +228,8 @@ func TestHandleSetRealName(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setRealName",
@@ -232,7 +239,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -255,7 +262,8 @@ func TestHandleSetRealName(t *testing.T) {
accountsObj: mockAccountsObj,
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setRealName",
@@ -267,7 +275,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -285,7 +293,8 @@ func TestHandleSetEmail(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setEmail",
@@ -295,7 +304,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -318,7 +327,8 @@ func TestHandleSetEmail(t *testing.T) {
accountsObj: mockAccountsObj,
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setEmail",
@@ -330,7 +340,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -348,7 +358,8 @@ func TestHandleSetLanguage(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setLanguage",
@@ -358,7 +369,7 @@ func TestHandleSetLanguage(t *testing.T) {
handleSetLanguage(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -373,7 +384,8 @@ func TestHandleSetLocation(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.setLocation",
@@ -383,7 +395,7 @@ func TestHandleSetLocation(t *testing.T) {
handleSetLocation(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -398,7 +410,8 @@ func TestHandleGetUserIconFile(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.getUserIconFile",
@@ -408,7 +421,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -425,7 +438,8 @@ func TestHandleGetUserIconFile(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.accounts.getUserIconFile",
@@ -437,7 +451,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -456,13 +470,14 @@ func TestHandleGetColorScheme(t *testing.T) {
stateMutex: sync.RWMutex{},
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager)
var resp models.Response[map[string]uint32]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -487,13 +502,14 @@ func TestHandleGetColorScheme(t *testing.T) {
settingsObj: mockSettingsObj,
}
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager)
var resp models.Response[map[string]uint32]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -515,7 +531,8 @@ func TestHandleRequest(t *testing.T) {
}
t.Run("unknown method", func(t *testing.T) {
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.unknown",
@@ -524,7 +541,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -532,7 +549,8 @@ func TestHandleRequest(t *testing.T) {
})
t.Run("valid method - getState", func(t *testing.T) {
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: "freedesktop.getState",
@@ -541,7 +559,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager)
var resp models.Response[FreedeskState]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
@@ -560,7 +578,8 @@ func TestHandleRequest(t *testing.T) {
}
for _, method := range tests {
conn := newMockNetConn()
mc := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{
ID: 123,
Method: method,
@@ -570,7 +589,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager)
var resp models.Response[any]
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
require.NoError(t, err)
assert.Equal(t, 123, resp.ID)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/dankgo/dbusutil"
"github.com/godbus/dbus/v5"
)
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
"github.com/godbus/dbus/v5"
)
+5 -7
View File
@@ -1,9 +1,7 @@
package location
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
@@ -13,7 +11,7 @@ type LocationEvent struct {
Data State `json:"data"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "location.getState":
handleGetState(conn, req, manager)
@@ -25,11 +23,11 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
@@ -40,7 +38,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
Data: initialState,
}
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
if err := conn.WriteResponse(models.Response[LocationEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
@@ -52,7 +50,7 @@ func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
Type: "state_changed",
Data: state,
}
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
if err := conn.WriteResponse(models.Response[LocationEvent]{
Result: &event,
}); err != nil {
return
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/AvengeMedia/dankgo/syncmap"
)
type State struct {

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