mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 069ddab041 | |||
| 45cf6ecefd | |||
| 7058b00091 | |||
| 6ba4b79039 | |||
| b8e2ce1da8 | |||
| c58d55db7d | |||
| e15db00714 | |||
| 906eba5c17 | |||
| d2b61b91ef | |||
| 6c27b92445 | |||
| a64bd80cc9 | |||
| 661dc27d26 | |||
| 3d5a6fb9d7 | |||
| c2a5397d8a | |||
| 9b5538323d | |||
| 8321d28d18 | |||
| 39278860c8 | |||
| f455456ff1 | |||
| 5a80a32951 | |||
| 3cedd56f23 | |||
| dc1ad5b329 | |||
| 25c12a14a9 | |||
| b72b276fb4 | |||
| f89fb67316 | |||
| f457e04cb1 | |||
| 23daca0b57 | |||
| accdd0972c | |||
| 00c28f69fd | |||
| 2e0337b68c | |||
| 4975960ea4 | |||
| 0381a112b3 | |||
| ed5e2a22c0 | |||
| 847718459c | |||
| 95b120b475 | |||
| 4dd2f1d5be | |||
| bf5faf725b | |||
| e5eff919b8 | |||
| 606115748a | |||
| ad936bfdc9 | |||
| 110396cb2a | |||
| 7b08e61863 | |||
| 876ead665b | |||
| 74896fb87c | |||
| f1f528b2cb | |||
| 9853e1431b | |||
| e7b8196a4a | |||
| 85ebb402cb | |||
| f37f4a1f35 | |||
| fc11dfbc57 | |||
| 7806cf4a58 | |||
| 89895da4fa | |||
| 4ff93bf46e | |||
| 63ffd7caec | |||
| dac08b34c6 | |||
| 840a448b4a | |||
| 26d429fa31 | |||
| fd20cce9b7 | |||
| ad5a96d95b | |||
| 8ba5e1a082 | |||
| 7d6a7bae06 | |||
| 986a6e0230 | |||
| 64ba1fbb1b | |||
| ab9df791b3 | |||
| 65f4a9a044 | |||
| 59164cbd30 | |||
| 7ab1f6a956 | |||
| 841167ac44 | |||
| c2a6a7aebd | |||
| b17ef80c91 | |||
| a55177f1d2 | |||
| 3d0ce17a8c | |||
| 6306bc7aa2 | |||
| eca92898f9 | |||
| db9fb0bd90 | |||
| 815820aa27 | |||
| 01c0ed2778 | |||
| 9967db0c5a | |||
| 883a787db7 | |||
| 3b1a3c1c22 | |||
| 970262f276 | |||
| b708e97ec5 | |||
| e94fc7fe99 | |||
| c4bca6f6cd | |||
| 9e7fc833e9 | |||
| 9e2d493749 | |||
| 93120fd73b | |||
| 536ebf7b0a | |||
| 1105047a80 | |||
| 8b46397200 | |||
| 6b70491ba9 | |||
| deb0138198 | |||
| 2e0587ca16 | |||
| 45cafc6bdb | |||
| 82d5081b7d | |||
| b5b083305f | |||
| 039d14c790 | |||
| ee7ac9d2bd | |||
| bd159be414 | |||
| 4dda0e3a5e | |||
| 724ee9422d | |||
| c91cb5d89a | |||
| 660953ebc5 | |||
| bd70a6286e | |||
| d87e2fcb01 | |||
| 928b272628 | |||
| 781ed16538 | |||
| 417da7408d | |||
| 0d550662de | |||
| 35e35a16fa | |||
| d96559f7df | |||
| 7f37799e53 | |||
| fcda17f517 | |||
| 4b05e7a0fa | |||
| 7c6ad65b5a | |||
| acda199e28 | |||
| b3a129af2f | |||
| e9d958b32b | |||
| dd9edd8a00 | |||
| 28c25d46d5 | |||
| 58530ae56c | |||
| c0134c249a | |||
| f7157f0e92 | |||
| e29b9749df | |||
| 6791c20f40 | |||
| 6c2572fbfe | |||
| 3ee07f7f20 | |||
| ef244cb645 | |||
| 860c347362 | |||
| c7b58e52d7 | |||
| e0fb817a75 | |||
| b5908f4d1b | |||
| ee6c91e094 | |||
| 59d367f4a4 | |||
| ee84489b95 | |||
| b1beecb083 | |||
| ffd3840058 | |||
| 447b4d89b6 | |||
| 36772735f3 | |||
| 375afaaf3a | |||
| fe0214d522 | |||
| fce1a09790 | |||
| 554e55c63a | |||
| f7d8e2b56c | |||
| 1d4572ac1c | |||
| c02a24fecf | |||
| bf644516f8 | |||
| 61f9ea4954 | |||
| 5a169bea72 | |||
| 3d60f35683 | |||
| b95f4cdbf3 | |||
| b834e82668 | |||
| 302cfda2f4 | |||
| c86676df9b | |||
| fa26dbb544 | |||
| 067d01f678 | |||
| 95c8cc80f6 | |||
| b8f216dc0f | |||
| e84e63fa3a | |||
| 6d08b91004 |
@@ -1,56 +0,0 @@
|
||||
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 }}
|
||||
@@ -27,8 +27,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
@@ -50,39 +48,8 @@ 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 withshell' ./cmd/dms
|
||||
run: go build -v -tags distro_binary ./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
|
||||
|
||||
@@ -33,6 +33,8 @@ 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
|
||||
|
||||
@@ -10,8 +10,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
|
||||
@@ -27,11 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { goos: linux, arch: amd64 }
|
||||
- { goos: linux, arch: arm64 }
|
||||
- { goos: freebsd, arch: amd64 }
|
||||
- { goos: freebsd, arch: arm64 }
|
||||
arch: [amd64, arm64]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -39,8 +35,6 @@ 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
|
||||
@@ -48,7 +42,6 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -56,7 +49,6 @@ 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:"
|
||||
@@ -65,43 +57,38 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.goos == 'linux'
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build dankinstall (${{ env.ASSET }})
|
||||
if: matrix.goos == 'linux'
|
||||
- name: Build dankinstall (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dankinstall
|
||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dankinstall-${{ env.ASSET }}
|
||||
-o ../../dankinstall-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dankinstall-${{ env.ASSET }}
|
||||
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
|
||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
||||
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Sync embedded shell
|
||||
run: make sync-shell
|
||||
|
||||
- name: Build dms (${{ env.ASSET }})
|
||||
- name: Build dms (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dms
|
||||
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dms-${{ env.ASSET }}
|
||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dms-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dms-${{ env.ASSET }}
|
||||
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
|
||||
gzip -9 -k dms-${{ matrix.arch }}
|
||||
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Generate shell completions
|
||||
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||
if: matrix.arch == 'amd64'
|
||||
run: |
|
||||
set -eux
|
||||
chmod +x dms-amd64
|
||||
@@ -109,58 +96,46 @@ jobs:
|
||||
./dms-amd64 completion fish > completion.fish
|
||||
./dms-amd64 completion zsh > completion.zsh
|
||||
|
||||
- name: Build dms-distropkg (${{ env.ASSET }})
|
||||
- name: Build dms-distropkg (${{ matrix.arch }})
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dms
|
||||
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dms-distropkg-${{ env.ASSET }}
|
||||
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
||||
cd ../..
|
||||
gzip -9 -k dms-distropkg-${{ env.ASSET }}
|
||||
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
||||
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
|
||||
- name: Upload artifacts (${{ env.ASSET }})
|
||||
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
|
||||
- name: Upload artifacts (${{ matrix.arch }})
|
||||
if: matrix.arch == 'arm64'
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ env.ASSET }}
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
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
|
||||
core/dankinstall-${{ matrix.arch }}.gz
|
||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-${{ matrix.arch }}.gz
|
||||
core/dms-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload artifacts with completions
|
||||
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||
if: matrix.arch == 'amd64'
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ env.ASSET }}
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
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/dankinstall-${{ matrix.arch }}.gz
|
||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-${{ matrix.arch }}.gz
|
||||
core/dms-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
core/completion.bash
|
||||
core/completion.fish
|
||||
core/completion.zsh
|
||||
@@ -216,13 +191,11 @@ 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
|
||||
@@ -269,23 +242,16 @@ 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
|
||||
@@ -333,22 +299,6 @@ 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/
|
||||
@@ -365,8 +315,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 os/architecture
|
||||
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
|
||||
# Create full packages for each architecture
|
||||
for arch in amd64 arm64; do
|
||||
mkdir -p _temp_full/dms
|
||||
mkdir -p _temp_full/bin
|
||||
mkdir -p _temp_full/completions
|
||||
@@ -409,16 +359,18 @@ jobs:
|
||||
|
||||
## Installation Steps
|
||||
|
||||
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.
|
||||
1. **Install quickshell assets:**
|
||||
```bash
|
||||
mkdir -p ~/.config/quickshell
|
||||
cp -r dms ~/.config/quickshell/
|
||||
```
|
||||
|
||||
1. **Install the DMS CLI binaries:**
|
||||
2. **Install the DMS CLI binaries:**
|
||||
```bash
|
||||
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||
```
|
||||
|
||||
2. **Install shell completions (optional):**
|
||||
3. **Install shell completions (optional):**
|
||||
```bash
|
||||
# Bash
|
||||
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
||||
@@ -430,7 +382,7 @@ jobs:
|
||||
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
||||
```
|
||||
|
||||
3. **Start the shell:**
|
||||
4. **Start the shell:**
|
||||
```bash
|
||||
dms run
|
||||
```
|
||||
|
||||
@@ -4,12 +4,14 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package to build'
|
||||
description: 'Package to build (dms, dms-greeter, or both)'
|
||||
required: false
|
||||
default: 'dms'
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- both
|
||||
version:
|
||||
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
||||
required: false
|
||||
@@ -29,7 +31,11 @@ jobs:
|
||||
id: set-packages
|
||||
run: |
|
||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||
if [ "$PACKAGE_INPUT" = "both" ]; then
|
||||
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-and-upload:
|
||||
needs: determine-packages
|
||||
@@ -157,6 +163,8 @@ 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
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
@@ -35,7 +36,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
@@ -74,12 +74,27 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check dms-greeter stable tag
|
||||
check_dms_greeter_stable() {
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
||||
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||
# Run from tag with no package specified - update stable package
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
# Run from tag with no package specified - update both stable packages
|
||||
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
@@ -105,7 +120,7 @@ jobs:
|
||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check the stable package and build list of those needing updates
|
||||
# Check each stable package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
@@ -113,6 +128,10 @@ jobs:
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
if check_dms_greeter_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
@@ -121,7 +140,7 @@ jobs:
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
echo "✓ Both packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
@@ -145,6 +164,18 @@ jobs:
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||
if check_dms_greeter_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
@@ -168,7 +199,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Wait before OBS upload
|
||||
run: sleep 3
|
||||
@@ -189,9 +219,9 @@ jobs:
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow dispatch
|
||||
|
||||
# Determine version for dms stable using the API
|
||||
# Determine version for dms stable and dms-greeter using the API
|
||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
@@ -257,7 +287,7 @@ jobs:
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms-git/debian/changelog"
|
||||
|
||||
- name: Update stable version (dms)
|
||||
- name: Update stable version (dms + dms-greeter)
|
||||
if: steps.packages.outputs.version != ''
|
||||
run: |
|
||||
VERSION="${{ steps.packages.outputs.version }}"
|
||||
@@ -293,12 +323,25 @@ 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 stable)
|
||||
# Update download_url paths (for dms, dms-greeter stable)
|
||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||
fi
|
||||
@@ -359,7 +402,7 @@ jobs:
|
||||
UPLOADED_PACKAGES=()
|
||||
SKIPPED_PACKAGES=()
|
||||
|
||||
# PACKAGES can be a space-separated list (from the "all" check)
|
||||
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
echo ""
|
||||
@@ -462,6 +505,9 @@ 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
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
@@ -35,7 +36,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -94,7 +94,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -134,6 +133,7 @@ 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,3 +148,4 @@ 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"
|
||||
|
||||
@@ -27,6 +27,11 @@ on:
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
build_greeter:
|
||||
description: 'Build stable dms-greeter package'
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -78,7 +83,6 @@ 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"
|
||||
@@ -101,6 +105,7 @@ 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)
|
||||
@@ -174,16 +179,19 @@ 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
|
||||
|
||||
@@ -208,20 +216,23 @@ 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" ]; then
|
||||
echo "🔧 Updating stable template for $ARCHIVE_TAG"
|
||||
if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then
|
||||
echo "🔧 Updating stable templates for $ARCHIVE_TAG"
|
||||
TARBALL="$(mktemp)"
|
||||
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
|
||||
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/archive/refs/tags/${ARCHIVE_TAG}.tar.gz"
|
||||
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
|
||||
rm -f "$TARBALL"
|
||||
|
||||
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
|
||||
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
|
||||
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
|
||||
fi
|
||||
|
||||
# 1. Build dms-git (development package)
|
||||
@@ -235,7 +246,7 @@ jobs:
|
||||
# into $wrksrc (create_wrksrc=yes handles the rest).
|
||||
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
|
||||
mkdir -p "$SRC_CACHE"
|
||||
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||
--exclude=void-packages \
|
||||
--exclude=r2-repo \
|
||||
--exclude=.git \
|
||||
@@ -277,6 +288,22 @@ 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
|
||||
|
||||
@@ -129,5 +129,3 @@ distro/void/masterdir*/
|
||||
|
||||
# Often gets built
|
||||
core/dms
|
||||
|
||||
core/internal/shellembed/dist
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[submodule "dank-qml-common"]
|
||||
path = dank-qml-common
|
||||
url = https://github.com/AvengeMedia/dank-qml-common.git
|
||||
branch = master
|
||||
@@ -36,14 +36,6 @@ repos:
|
||||
language: system
|
||||
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: i18n-term-variants
|
||||
name: i18n term variants (no case/punctuation duplicates)
|
||||
entry: python3 quickshell/translations/check_term_variants.py
|
||||
language: system
|
||||
files: ^quickshell/(.*\.qml|translations/(check_term_variants\.py|extract_translations\.py))$
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-console-in-qml
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
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
|
||||
|
||||
@@ -6,14 +6,6 @@ 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
|
||||
```
|
||||
|
||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
||||
|
||||
```bash
|
||||
@@ -36,47 +28,6 @@ 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.
|
||||
@@ -153,8 +104,6 @@ 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)
|
||||
|
||||
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||
ASSETS_DIR=assets
|
||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||
|
||||
.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
|
||||
.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
|
||||
|
||||
all: build
|
||||
|
||||
@@ -27,12 +27,6 @@ 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
|
||||
@@ -41,12 +35,6 @@ 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)..."
|
||||
@@ -55,9 +43,8 @@ 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 -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||
@echo "Shell files installed"
|
||||
|
||||
@@ -72,9 +59,6 @@ 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
|
||||
@@ -82,7 +66,6 @@ else
|
||||
@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..."
|
||||
|
||||
@@ -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 a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
|
||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
||||
|
||||
**Plugin System**
|
||||
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
||||
@@ -132,17 +132,6 @@ 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:
|
||||
|
||||
+26
-50
@@ -3,8 +3,6 @@ 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
|
||||
|
||||
@@ -23,46 +21,17 @@ 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
|
||||
|
||||
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
|
||||
.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
|
||||
|
||||
# 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: sync-shell
|
||||
build:
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
CGO_ENABLED=0 $(GO) build -tags withshell $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||
|
||||
dankinstall:
|
||||
@@ -71,18 +40,26 @@ 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 (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))..."
|
||||
# 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)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@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"
|
||||
@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
|
||||
|
||||
build-all: build dankinstall
|
||||
|
||||
@@ -122,7 +99,7 @@ uninstall-dankinstall:
|
||||
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@rm -rf $(BUILD_DIR) $(EMBED_DIR)
|
||||
@rm -rf $(BUILD_DIR)
|
||||
@echo "Clean complete"
|
||||
|
||||
test:
|
||||
@@ -164,11 +141,10 @@ print-version:
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " all - Build the main binary (dms) (default)"
|
||||
@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 " build - Build the main binary (dms)"
|
||||
@echo " dankinstall - Build dankinstall binary"
|
||||
@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 " dist - Build dms for linux amd64/arm64 (no update/greeter)"
|
||||
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
|
||||
@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
@@ -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` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
|
||||
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
||||
|
||||
### Color Picker
|
||||
|
||||
@@ -109,7 +109,7 @@ make test # Run tests
|
||||
**Distribution build:**
|
||||
|
||||
```bash
|
||||
make dist # Build without update features
|
||||
make dist # Build without update/greeter 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.
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -16,7 +15,7 @@ import (
|
||||
var authCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Manage DMS authentication sync",
|
||||
Long: "Manage PAM/authentication setup for the DMS lock screen",
|
||||
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
||||
}
|
||||
|
||||
var authSyncCmd = &cobra.Command{
|
||||
@@ -212,31 +211,3 @@ 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)")
|
||||
}
|
||||
|
||||
@@ -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,6 +19,63 @@ 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",
|
||||
@@ -207,41 +264,29 @@ 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 {
|
||||
if ver := parseBaseVersion(shellembed.Version()); ver != "" {
|
||||
return ver
|
||||
paths := []string{
|
||||
"/usr/share/quickshell/dms/VERSION",
|
||||
"/usr/local/share/quickshell/dms/VERSION",
|
||||
"/etc/xdg/quickshell/dms/VERSION",
|
||||
}
|
||||
|
||||
for _, path := range shellVersionPaths {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if ver := parseBaseVersion(string(content)); 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -698,9 +743,12 @@ func checkAllPluginsCLI() error {
|
||||
}
|
||||
|
||||
func getCommonCommands() []*cobra.Command {
|
||||
commands := shellApp.Commands()
|
||||
return append(commands, []*cobra.Command{
|
||||
return []*cobra.Command{
|
||||
versionCmd,
|
||||
runCmd,
|
||||
restartCmd,
|
||||
restartDetachedCmd,
|
||||
killCmd,
|
||||
ipcCmd,
|
||||
debugSrvCmd,
|
||||
pluginsCmd,
|
||||
@@ -727,5 +775,5 @@ func getCommonCommands() []*cobra.Command {
|
||||
trashCmd,
|
||||
systemCmd,
|
||||
switchUserCmd,
|
||||
}...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,12 +401,12 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
|
||||
}
|
||||
|
||||
func getDMSShellVersion() (version, path string) {
|
||||
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
|
||||
versionFile := filepath.Join(shellApp.ConfigPath(), "VERSION")
|
||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
||||
versionFile := filepath.Join(configPath, "VERSION")
|
||||
if data, err := os.ReadFile(versionFile); err == nil {
|
||||
return strings.TrimSpace(string(data)), shellApp.ConfigPath()
|
||||
return strings.TrimSpace(string(data)), configPath
|
||||
}
|
||||
return "installed", shellApp.ConfigPath()
|
||||
return "installed", configPath
|
||||
}
|
||||
|
||||
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
||||
@@ -450,8 +450,8 @@ func checkDMSInstallation() []checkResult {
|
||||
var results []checkResult
|
||||
|
||||
dmsPath := ""
|
||||
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
|
||||
dmsPath = shellApp.ConfigPath()
|
||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
||||
dmsPath = configPath
|
||||
} else if path, err := config.LocateDMSConfig(); err == nil {
|
||||
dmsPath = path
|
||||
}
|
||||
@@ -833,8 +833,6 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||
return "systemd-networkd"
|
||||
case network.BackendConnMan:
|
||||
return "ConnMan"
|
||||
case network.BackendWpaSupplicant:
|
||||
return "wpa_supplicant"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -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: shellApp.ResolveConfig,
|
||||
PreRunE: findConfig,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runUpdate()
|
||||
},
|
||||
@@ -98,7 +98,7 @@ func runUpdate() {
|
||||
}
|
||||
|
||||
log.Info("Update complete! Restarting DMS...")
|
||||
shellApp.Restart()
|
||||
restartShell()
|
||||
}
|
||||
|
||||
func updateArchLinux() error {
|
||||
@@ -250,10 +250,6 @@ 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()
|
||||
@@ -295,10 +291,6 @@ 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
|
||||
}
|
||||
@@ -328,21 +320,10 @@ 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)
|
||||
@@ -361,7 +342,7 @@ func offerReclone(dmsPath string) bool {
|
||||
}
|
||||
|
||||
fmt.Println("Cloning fresh copy...")
|
||||
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
|
||||
+2099
-27
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
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,9 +1,19 @@
|
||||
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",
|
||||
@@ -11,5 +21,54 @@ var rootCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
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)")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -231,16 +231,16 @@ func setPopoutScreenshotMode(begin bool) {
|
||||
fn = "begin"
|
||||
}
|
||||
cmdArgs := []string{"ipc"}
|
||||
if pid, ok := shellApp.SessionPID(); ok {
|
||||
if pid, ok := getFirstDMSPID(); ok {
|
||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||
} else {
|
||||
if err := shellApp.ResolveConfig(nil, nil); err != nil {
|
||||
if err := findConfig(nil, nil); err != nil {
|
||||
return
|
||||
}
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
|
||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
|
||||
_ = exec.Command("qs", cmdArgs...).Run()
|
||||
|
||||
@@ -11,6 +11,7 @@ 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"
|
||||
@@ -204,12 +205,7 @@ func detectTerminal() (string, error) {
|
||||
}
|
||||
|
||||
func detectCompositorForSetup() (string, error) {
|
||||
var compositors []string
|
||||
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
|
||||
if utils.CommandExists(candidate) {
|
||||
compositors = append(compositors, candidate)
|
||||
}
|
||||
}
|
||||
compositors := greeter.DetectCompositors()
|
||||
|
||||
switch len(compositors) {
|
||||
case 0:
|
||||
@@ -218,20 +214,11 @@ func detectCompositorForSetup() (string, error) {
|
||||
return strings.ToLower(compositors[0]), nil
|
||||
}
|
||||
|
||||
fmt.Println("Multiple compositors detected:")
|
||||
for i, compositor := range compositors {
|
||||
fmt.Printf("%d) %s\n", i+1, compositor)
|
||||
selected, err := greeter.PromptCompositorChoice(compositors)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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
|
||||
return strings.ToLower(selected), nil
|
||||
}
|
||||
|
||||
func runSetupDmsConfig(name string) error {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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"
|
||||
}
|
||||
@@ -12,6 +12,14 @@ 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)
|
||||
|
||||
@@ -12,6 +12,14 @@ 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)
|
||||
|
||||
@@ -101,8 +101,11 @@ func getServerSocketPath() string {
|
||||
runtimeDir = os.TempDir()
|
||||
}
|
||||
|
||||
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
|
||||
return sessionSock
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(runtimeDir)
|
||||
|
||||
+696
-6
@@ -4,20 +4,591 @@ 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()
|
||||
@@ -61,17 +632,17 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||
|
||||
func buildQsIPCBaseArgs() ([]string, error) {
|
||||
cmdArgs := []string{"ipc"}
|
||||
switch pid, ok := shellApp.SessionPID(); {
|
||||
switch pid, ok := getSessionDMSPID(); {
|
||||
case ok:
|
||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||
default:
|
||||
if err := shellApp.ResolveConfig(nil, nil); err != nil {
|
||||
if err := findConfig(nil, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
|
||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||
}
|
||||
return cmdArgs, nil
|
||||
}
|
||||
@@ -128,6 +699,101 @@ 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()
|
||||
@@ -280,12 +946,36 @@ 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(exitCode int, uptime time.Duration, stderrTail string) {
|
||||
if uptime >= 5*time.Second || exitCode == 0 || exitCode > 128 {
|
||||
func logStartupFailure(startTime time.Time, exitCode int, tracker *stderrTracker) {
|
||||
if time.Since(startTime) >= 5*time.Second || exitCode == 0 || exitCode > 128 {
|
||||
return
|
||||
}
|
||||
if containsFontCrashSignature(stderrTail) {
|
||||
if containsFontCrashSignature(tracker.String()) {
|
||||
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)
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -26,3 +28,62 @@ 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
@@ -1,13 +1,14 @@
|
||||
module github.com/AvengeMedia/DankMaterialShell/core
|
||||
|
||||
go 1.26.4
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
github.com/alecthomas/chroma/v2 v2.24.1
|
||||
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
|
||||
@@ -17,12 +18,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.4
|
||||
github.com/yuin/goldmark v1.8.2
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.etcd.io/bbolt v1.5.0
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
||||
golang.org/x/image v0.44.0
|
||||
tailscale.com v1.100.0
|
||||
golang.org/x/image v0.39.0
|
||||
tailscale.com v1.96.5
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -30,28 +31,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.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/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/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-alpha.1 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // 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-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.4.0 // indirect
|
||||
github.com/mdlayher/netlink v1.11.2 // indirect
|
||||
github.com/mdlayher/socket v0.6.1 // 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/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
@@ -61,15 +62,14 @@ 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.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.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.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AvengeMedia/dankgo v0.0.0-20260718184203-ed59ffc6e599
|
||||
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.4
|
||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0
|
||||
github.com/mattn/go-isatty v0.0.23
|
||||
github.com/mattn/go-isatty v0.0.22
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // 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 // indirect
|
||||
github.com/muesli/termenv v0.16.0
|
||||
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.47.0
|
||||
golang.org/x/text v0.40.0
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/text v0.36.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
||||
+56
-56
@@ -1,7 +1,5 @@
|
||||
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-20260718184203-ed59ffc6e599 h1:hxy5SRGREsQ6v4xUOxWnWVPWD6ZU+H0jA+LHcTwXbCg=
|
||||
github.com/AvengeMedia/dankgo v0.0.0-20260718184203-ed59ffc6e599/go.mod h1:7p7cfydr4WM1G6eOPFlANXF3IV5du3FoA4CbDPprHAo=
|
||||
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=
|
||||
@@ -13,8 +11,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.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
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/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=
|
||||
@@ -50,23 +48,25 @@ 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.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/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/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-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
|
||||
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
|
||||
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/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/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20=
|
||||
github.com/dlclark/regexp2/v2 v2.5.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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-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-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-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.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
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/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.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
|
||||
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
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-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.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/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/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-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
|
||||
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/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.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
|
||||
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
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-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.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
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.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/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/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.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.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.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
|
||||
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
|
||||
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
|
||||
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
hotkey-overlay {
|
||||
skip-at-startup
|
||||
}
|
||||
|
||||
environment {
|
||||
DMS_RUN_GREETER "1"
|
||||
}
|
||||
|
||||
gestures {
|
||||
hot-corners {
|
||||
off
|
||||
}
|
||||
}
|
||||
|
||||
layout {
|
||||
background-color "#000000"
|
||||
}
|
||||
@@ -16,3 +16,6 @@ var NiriAlttabConfig string
|
||||
|
||||
//go:embed embedded/niri-binds.kdl
|
||||
var NiriBindsConfig string
|
||||
|
||||
//go:embed embedded/niri-greeter.kdl
|
||||
var NiriGreeterConfig string
|
||||
|
||||
@@ -520,7 +520,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
Progress: 0.90,
|
||||
Step: "Cloning DankMaterialShell...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git",
|
||||
CommandInfo: "git clone 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", "--recurse-submodules",
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
||||
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
||||
@@ -553,8 +553,6 @@ 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
|
||||
@@ -593,8 +591,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return nil
|
||||
}
|
||||
|
||||
m.syncDMSSubmodules(ctx, dmsPath)
|
||||
|
||||
m.log("DankMaterialShell updated successfully (git variant)")
|
||||
return nil
|
||||
}
|
||||
@@ -613,19 +609,10 @@ 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...")
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
package errdefs
|
||||
|
||||
import (
|
||||
dankerrdefs "github.com/AvengeMedia/dankgo/errdefs"
|
||||
)
|
||||
|
||||
type ErrorType = dankerrdefs.ErrorType
|
||||
|
||||
type CustomError = dankerrdefs.CustomError
|
||||
type ErrorType int
|
||||
|
||||
const (
|
||||
ErrTypeNotLinux ErrorType = dankerrdefs.AppErrorBase + iota
|
||||
ErrTypeNotLinux ErrorType = iota
|
||||
ErrTypeInvalidArchitecture
|
||||
ErrTypeUnsupportedDistribution
|
||||
ErrTypeUnsupportedVersion
|
||||
@@ -28,8 +22,20 @@ 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 dankerrdefs.NewCustomError(errType, message)
|
||||
return &CustomError{
|
||||
Type: errType,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -41,10 +47,6 @@ const (
|
||||
ErrWifiDisabled = "wifi-disabled"
|
||||
ErrAlreadyConnected = "already-connected"
|
||||
ErrConnectionFailed = "connection-failed"
|
||||
|
||||
ErrHotspotIPConfigFailed = "hotspot-ip-config-failed"
|
||||
ErrHotspotSupplicantFailed = "hotspot-supplicant-failed"
|
||||
ErrHotspotFailed = "hotspot-failed"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/dankgo/dbusutil"
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# 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
@@ -0,0 +1,350 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
@@ -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,12 +223,16 @@ 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 := utils.RunDmsGreeterInstall(sudoPassword, logFunc); err != nil {
|
||||
if err := greeter.AutoSetupGreeter(compositorName, sudoPassword, logFunc); err != nil {
|
||||
// Non-fatal, matching TUI behavior
|
||||
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
|
||||
}
|
||||
|
||||
+207
-19
@@ -1,32 +1,220 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
danklog "github.com/AvengeMedia/dankgo/log"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
cblog "github.com/charmbracelet/log"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
type Logger = danklog.Logger
|
||||
// Logger embeds the Charm Logger and adds Printf/Fatalf
|
||||
type Logger struct{ *cblog.Logger }
|
||||
|
||||
func init() {
|
||||
danklog.SetEnvPrefix("DMS")
|
||||
// Printf routes goose/info-style logs through Infof.
|
||||
func (l *Logger) Printf(format string, v ...any) { l.Infof(format, v...) }
|
||||
|
||||
// Fatalf keeps goose’s 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 GetLogger() *Logger { return danklog.GetLogger() }
|
||||
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 GetQtLoggingRules() string { return danklog.GetQtLoggingRules() }
|
||||
func GetQtLoggingRules() string {
|
||||
level := os.Getenv("DMS_LOG_LEVEL")
|
||||
if level == "" {
|
||||
level = "info"
|
||||
}
|
||||
|
||||
func SetLevel(level string) { danklog.SetLevel(level) }
|
||||
// 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 SetLogFile(path string) error { return danklog.SetLogFile(path) }
|
||||
return strings.Join(rules, ";")
|
||||
}
|
||||
|
||||
func ApplyEnvOverrides() { danklog.ApplyEnvOverrides() }
|
||||
// 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 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...) }
|
||||
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...) }
|
||||
|
||||
@@ -390,10 +390,6 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
refreshGTK4()
|
||||
}
|
||||
|
||||
if isDMSKDEColorSchemeActive(opts.ConfigDir) {
|
||||
applyKDEColorScheme(opts.Mode)
|
||||
}
|
||||
|
||||
if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) {
|
||||
refreshQt6ct()
|
||||
}
|
||||
@@ -927,47 +923,6 @@ func isDMSGTKActive(configDir string) bool {
|
||||
return err == nil && strings.Contains(string(data), "dank-colors.css")
|
||||
}
|
||||
|
||||
// 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 refreshGTK(mode ColorMode) {
|
||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
|
||||
log.Warnf("Failed to reset gtk-theme: %v", err)
|
||||
@@ -1041,6 +996,26 @@ 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 {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+393
-21
@@ -5,20 +5,30 @@ 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"
|
||||
)
|
||||
@@ -28,8 +38,7 @@ 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).
|
||||
// /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"}
|
||||
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d"}
|
||||
|
||||
// Standalone auth+account services, most universal first. login exists almost
|
||||
// everywhere (util-linux); system-* cover Fedora/Arch/Gentoo/SUSE-Leap.
|
||||
@@ -41,15 +50,13 @@ var lockscreenPamEntryCandidates = []string{
|
||||
}
|
||||
|
||||
// Fallback for distros with no standalone login service, only shared building
|
||||
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*), FreeBSD
|
||||
// (system holds both stanzas, included by login).
|
||||
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*).
|
||||
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{
|
||||
@@ -60,28 +67,34 @@ var includedPamAuthFiles = []string{
|
||||
"system-local-login",
|
||||
"common-auth-pc",
|
||||
"login",
|
||||
"system",
|
||||
}
|
||||
|
||||
type AuthSettings struct {
|
||||
EnableFprint bool `json:"enableFprint"`
|
||||
EnableU2f bool `json:"enableU2f"`
|
||||
EnableFprint bool `json:"enableFprint"`
|
||||
EnableU2f bool `json:"enableU2f"`
|
||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
|
||||
}
|
||||
|
||||
type SyncAuthOptions struct {
|
||||
HomeDir string
|
||||
HomeDir string
|
||||
ForceGreeterAuth bool
|
||||
}
|
||||
|
||||
type syncDeps struct {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
type lockscreenPamIncludeDirective struct {
|
||||
@@ -137,6 +150,7 @@ 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,
|
||||
@@ -147,6 +161,8 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,10 +191,22 @@ 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 == "" {
|
||||
@@ -201,9 +229,99 @@ 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 {
|
||||
@@ -912,6 +1030,186 @@ 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 {
|
||||
@@ -938,6 +1236,26 @@ 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",
|
||||
@@ -950,9 +1268,6 @@ 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
|
||||
@@ -960,3 +1275,60 @@ 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))
|
||||
}
|
||||
|
||||
+271
-94
@@ -18,6 +18,42 @@ 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)
|
||||
@@ -28,6 +64,59 @@ 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)
|
||||
@@ -614,6 +703,98 @@ 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()
|
||||
|
||||
@@ -900,98 +1081,10 @@ 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", func(t *testing.T) {
|
||||
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
@@ -999,7 +1092,10 @@ 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")
|
||||
|
||||
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
||||
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)
|
||||
}
|
||||
@@ -1010,24 +1106,105 @@ 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("removes dankshell-u2f when disabled", func(t *testing.T) {
|
||||
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.writeSettings(t, `{"enableU2f":false}`)
|
||||
env.availableModules["pam_fprintd.so"] = true
|
||||
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":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, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
|
||||
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||
|
||||
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 be removed, stat err = %v", 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
func registerServerProxy(ctx *client.Context, proxy client.Proxy, serverID uint32) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package qmlchecks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGreeterRememberLastSessionFallsBackToDesktopID(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)
|
||||
if !strings.Contains(content, "GreetdMemory.lastSessionDesktopId || desktopIdFromPath(GreetdMemory.lastSessionId)") {
|
||||
t.Fatalf("remembered greeter sessions should derive a desktop id from legacy absolute session paths")
|
||||
}
|
||||
if !strings.Contains(content, "GreeterState.sessionDesktopIds[i] === savedDesktopId") {
|
||||
t.Fatalf("remembered greeter sessions should match current sessions by desktop id")
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,32 @@ func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockScreenAuthenticationCardOwnsFactorControls(t *testing.T) {
|
||||
data, err := os.ReadFile("../../../quickshell/Modules/Settings/LockScreenTab.qml")
|
||||
if err != nil {
|
||||
t.Fatalf("read lock screen settings QML: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
authCard := strings.Index(content, `title: I18n.tr("Authentication")`)
|
||||
behaviorCard := strings.Index(content, `title: I18n.tr("Behavior")`)
|
||||
fingerprintToggle := strings.Index(content, `settingKey: "enableFprint"`)
|
||||
u2fToggle := strings.Index(content, `settingKey: "enableU2f"`)
|
||||
u2fSource := strings.Index(content, `settingKey: "lockU2fPamPath"`)
|
||||
if authCard < 0 || behaviorCard < 0 || fingerprintToggle < 0 || u2fToggle < 0 || u2fSource < 0 {
|
||||
t.Fatalf("expected authentication card, factor toggles, and U2F source setting")
|
||||
}
|
||||
for name, position := range map[string]int{
|
||||
"fingerprint toggle": fingerprintToggle,
|
||||
"U2F toggle": u2fToggle,
|
||||
"U2F source": u2fSource,
|
||||
} {
|
||||
if position < authCard || position > behaviorCard {
|
||||
t.Fatalf("%s must remain in the authentication card", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) {
|
||||
data, err := os.ReadFile("../../../quickshell/Modules/Lock/Pam.qml")
|
||||
if err != nil {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
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 *models.Conn, req models.Request, manager *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "apppicker.open", "browser.open":
|
||||
handleOpen(conn, req, manager)
|
||||
@@ -15,7 +17,7 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleOpen(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleOpen(conn net.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")
|
||||
|
||||
@@ -3,7 +3,7 @@ package apppicker
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/dankgo/ipc/params"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
type BluetoothEvent struct {
|
||||
@@ -12,7 +14,7 @@ type BluetoothEvent struct {
|
||||
Data BluetoothState `json:"data"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "bluetooth.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
@@ -45,11 +47,11 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, manager.GetState())
|
||||
}
|
||||
|
||||
func handleStartDiscovery(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleStartDiscovery(conn net.Conn, req models.Request, manager *Manager) {
|
||||
if err := manager.StartDiscovery(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
@@ -57,7 +59,7 @@ func handleStartDiscovery(conn *models.Conn, req models.Request, manager *Manage
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery started"})
|
||||
}
|
||||
|
||||
func handleStopDiscovery(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleStopDiscovery(conn net.Conn, req models.Request, manager *Manager) {
|
||||
if err := manager.StopDiscovery(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
@@ -65,7 +67,7 @@ func handleStopDiscovery(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "discovery stopped"})
|
||||
}
|
||||
|
||||
func handleSetPowered(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetPowered(conn net.Conn, req models.Request, manager *Manager) {
|
||||
powered, err := params.Bool(req.Params, "powered")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -80,7 +82,7 @@ func handleSetPowered(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "powered state updated"})
|
||||
}
|
||||
|
||||
func handlePairDevice(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handlePairDevice(conn net.Conn, req models.Request, manager *Manager) {
|
||||
devicePath, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -95,7 +97,7 @@ func handlePairDevice(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing initiated"})
|
||||
}
|
||||
|
||||
func handleConnectDevice(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleConnectDevice(conn net.Conn, req models.Request, manager *Manager) {
|
||||
devicePath, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -110,7 +112,7 @@ func handleConnectDevice(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connecting"})
|
||||
}
|
||||
|
||||
func handleDisconnectDevice(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleDisconnectDevice(conn net.Conn, req models.Request, manager *Manager) {
|
||||
devicePath, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -125,7 +127,7 @@ func handleDisconnectDevice(conn *models.Conn, req models.Request, manager *Mana
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
|
||||
}
|
||||
|
||||
func handleRemoveDevice(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleRemoveDevice(conn net.Conn, req models.Request, manager *Manager) {
|
||||
devicePath, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -140,7 +142,7 @@ func handleRemoveDevice(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device removed"})
|
||||
}
|
||||
|
||||
func handleTrustDevice(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleTrustDevice(conn net.Conn, req models.Request, manager *Manager) {
|
||||
devicePath, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -155,7 +157,7 @@ func handleTrustDevice(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device trusted"})
|
||||
}
|
||||
|
||||
func handleUntrustDevice(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleUntrustDevice(conn net.Conn, req models.Request, manager *Manager) {
|
||||
devicePath, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -170,7 +172,7 @@ func handleUntrustDevice(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "device untrusted"})
|
||||
}
|
||||
|
||||
func handlePairingSubmit(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handlePairingSubmit(conn net.Conn, req models.Request, manager *Manager) {
|
||||
token, err := params.String(req.Params, "token")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -188,7 +190,7 @@ func handlePairingSubmit(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing response submitted"})
|
||||
}
|
||||
|
||||
func handlePairingCancel(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handlePairingCancel(conn net.Conn, req models.Request, manager *Manager) {
|
||||
token, err := params.String(req.Params, "token")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -203,7 +205,7 @@ func handlePairingCancel(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "pairing cancelled"})
|
||||
}
|
||||
|
||||
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
@@ -214,7 +216,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
Data: initialState,
|
||||
}
|
||||
|
||||
if err := conn.WriteResponse(models.Response[BluetoothEvent]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
|
||||
ID: req.ID,
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
@@ -226,7 +228,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
Type: "state_changed",
|
||||
Data: state,
|
||||
}
|
||||
if err := conn.WriteResponse(models.Response[BluetoothEvent]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/dankgo/dbusutil"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/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/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type SubscriptionBroker struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package bluez
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/dankgo/ipc/params"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
switch req.Method {
|
||||
case "brightness.getState":
|
||||
handleGetState(conn, req, m)
|
||||
@@ -26,11 +28,11 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func handleSetBrightness(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) {
|
||||
device, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -54,7 +56,7 @@ func handleSetBrightness(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func handleIncrement(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleIncrement(conn net.Conn, req models.Request, m *Manager) {
|
||||
device, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -73,7 +75,7 @@ func handleIncrement(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func handleDecrement(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleDecrement(conn net.Conn, req models.Request, m *Manager) {
|
||||
device, err := params.String(req.Params, "device")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -92,19 +94,19 @@ func handleDecrement(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func handleRescan(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleRescan(conn net.Conn, req models.Request, m *Manager) {
|
||||
m.Rescan()
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleSubscribe(conn net.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 := conn.WriteResponse(models.Response[State]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
ID: req.ID,
|
||||
Result: &initialState,
|
||||
}); err != nil {
|
||||
@@ -112,7 +114,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
|
||||
}
|
||||
|
||||
for state := range ch {
|
||||
if err := conn.WriteResponse(models.Response[State]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
ID: req.ID,
|
||||
Result: &state,
|
||||
}); err != nil {
|
||||
|
||||
@@ -20,7 +20,7 @@ func NewManagerWithOptions(exponential bool) (*Manager, error) {
|
||||
}
|
||||
|
||||
go m.initLogind()
|
||||
go m.initNative()
|
||||
go m.initSysfs()
|
||||
go m.initDDC()
|
||||
|
||||
return m, nil
|
||||
@@ -40,6 +40,39 @@ 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 {
|
||||
@@ -63,9 +96,9 @@ func (m *Manager) Rescan() {
|
||||
}
|
||||
}
|
||||
|
||||
if m.nativeReady && m.nativeBackend != nil {
|
||||
if err := m.nativeBackend.Rescan(); err != nil {
|
||||
log.Debugf("Native backend rescan failed: %v", err)
|
||||
if m.sysfsReady && m.sysfsBackend != nil {
|
||||
if err := m.sysfsBackend.Rescan(); err != nil {
|
||||
log.Debugf("Sysfs rescan failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,10 +150,10 @@ func stateChanged(old, new State) bool {
|
||||
func (m *Manager) updateState() {
|
||||
allDevices := make([]Device, 0)
|
||||
|
||||
if m.nativeReady && m.nativeBackend != nil {
|
||||
devices, err := m.nativeBackend.GetDevices()
|
||||
if m.sysfsReady && m.sysfsBackend != nil {
|
||||
devices, err := m.sysfsBackend.GetDevices()
|
||||
if err != nil {
|
||||
log.Debugf("Failed to get native backend devices: %v", err)
|
||||
log.Debugf("Failed to get sysfs devices: %v", err)
|
||||
}
|
||||
if err == nil {
|
||||
allDevices = append(allDevices, devices...)
|
||||
@@ -199,21 +232,18 @@ func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, expone
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
var err error
|
||||
switch {
|
||||
case deviceClass == ClassDDC:
|
||||
if deviceClass == ClassDDC {
|
||||
log.Debugf("Calling DDC backend for %s", deviceID)
|
||||
err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() {
|
||||
m.updateState()
|
||||
m.debouncedBroadcast(deviceID)
|
||||
})
|
||||
case m.logindReady && m.logindBackend != nil:
|
||||
} else if m.logindReady && m.logindBackend != nil {
|
||||
log.Debugf("Calling logind backend for %s", deviceID)
|
||||
err = m.setViaSysfsWithLogindWithExponent(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)
|
||||
} else {
|
||||
log.Debugf("Calling sysfs backend for %s", deviceID)
|
||||
err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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)
|
||||
}
|
||||
+4
-5
@@ -43,7 +43,7 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
nativeReady: true,
|
||||
sysfsReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
nativeReady: true,
|
||||
sysfsReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -180,9 +180,8 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
|
||||
m := &Manager{
|
||||
logindBackend: nil,
|
||||
sysfsBackend: sysfs,
|
||||
nativeBackend: sysfs,
|
||||
logindReady: false,
|
||||
nativeReady: true,
|
||||
sysfsReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -244,7 +243,7 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
nativeReady: true,
|
||||
sysfsReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type DeviceClass string
|
||||
@@ -33,25 +33,14 @@ 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
|
||||
monitor deviceMonitor
|
||||
udevMonitor *UdevMonitor
|
||||
|
||||
logindReady bool
|
||||
nativeReady bool
|
||||
sysfsReady bool
|
||||
ddcReady bool
|
||||
|
||||
exponential bool
|
||||
@@ -181,8 +170,8 @@ func (m *Manager) Close() {
|
||||
return true
|
||||
})
|
||||
|
||||
if m.monitor != nil {
|
||||
m.monitor.Close()
|
||||
if m.udevMonitor != nil {
|
||||
m.udevMonitor.Close()
|
||||
}
|
||||
|
||||
if m.logindBackend != nil {
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ func setupTestManager(t *testing.T) (*Manager, string) {
|
||||
|
||||
m := &Manager{
|
||||
sysfsBackend: sysfs,
|
||||
nativeReady: true,
|
||||
sysfsReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
|
||||
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "browser.open":
|
||||
url, ok := models.Get[string](req, "url")
|
||||
|
||||
@@ -3,7 +3,7 @@ package browser
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
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/dankgo/ipc/params"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
switch req.Method {
|
||||
case "clipboard.getState":
|
||||
handleGetState(conn, req, m)
|
||||
@@ -56,11 +58,11 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func handleGetHistory(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetHistory(conn net.Conn, req models.Request, m *Manager) {
|
||||
history := m.GetHistory()
|
||||
for i := range history {
|
||||
history[i].Data = nil
|
||||
@@ -68,7 +70,7 @@ func handleGetHistory(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, history)
|
||||
}
|
||||
|
||||
func handleGetEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -88,7 +90,7 @@ func handleGetEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, entry)
|
||||
}
|
||||
|
||||
func handleDeleteEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleDeleteEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -103,12 +105,12 @@ func handleDeleteEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry deleted"})
|
||||
}
|
||||
|
||||
func handleClearHistory(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleClearHistory(conn net.Conn, req models.Request, m *Manager) {
|
||||
m.ClearHistory()
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "history cleared"})
|
||||
}
|
||||
|
||||
func handleCopy(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleCopy(conn net.Conn, req models.Request, m *Manager) {
|
||||
text, err := params.String(req.Params, "text")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -123,7 +125,7 @@ func handleCopy(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
|
||||
}
|
||||
|
||||
func handleCopyEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -171,7 +173,7 @@ func handleCopyEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
|
||||
}
|
||||
|
||||
func handlePaste(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handlePaste(conn net.Conn, req models.Request, m *Manager) {
|
||||
text, err := m.PasteText()
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -181,7 +183,7 @@ func handlePaste(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, map[string]string{"text": text})
|
||||
}
|
||||
|
||||
func handleSendPaste(conn *models.Conn, req models.Request) {
|
||||
func handleSendPaste(conn net.Conn, req models.Request) {
|
||||
shift, _ := models.Get[bool](req, "shift")
|
||||
|
||||
if err := clipboardstore.SendPasteKeystroke(shift); err != nil {
|
||||
@@ -192,14 +194,14 @@ func handleSendPaste(conn *models.Conn, req models.Request) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paste sent"})
|
||||
}
|
||||
|
||||
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleSubscribe(conn net.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 := conn.WriteResponse(models.Response[State]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
ID: req.ID,
|
||||
Result: &initialState,
|
||||
}); err != nil {
|
||||
@@ -207,7 +209,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
|
||||
}
|
||||
|
||||
for state := range ch {
|
||||
if err := conn.WriteResponse(models.Response[State]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
ID: req.ID,
|
||||
Result: &state,
|
||||
}); err != nil {
|
||||
@@ -216,7 +218,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleSearch(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleSearch(conn net.Conn, req models.Request, m *Manager) {
|
||||
p := SearchParams{
|
||||
Query: params.StringOpt(req.Params, "query", ""),
|
||||
MimeType: params.StringOpt(req.Params, "mimeType", ""),
|
||||
@@ -239,11 +241,11 @@ func handleSearch(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.Search(p))
|
||||
}
|
||||
|
||||
func handleGetConfig(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetConfig(conn net.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetConfig())
|
||||
}
|
||||
|
||||
func handleSetConfig(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleSetConfig(conn net.Conn, req models.Request, m *Manager) {
|
||||
cfg := m.GetConfig()
|
||||
|
||||
if v, ok := models.Get[float64](req, "maxHistory"); ok {
|
||||
@@ -273,7 +275,7 @@ func handleSetConfig(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
|
||||
}
|
||||
|
||||
func handleStore(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleStore(conn net.Conn, req models.Request, m *Manager) {
|
||||
data, err := params.String(req.Params, "data")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -290,7 +292,7 @@ func handleStore(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "stored"})
|
||||
}
|
||||
|
||||
func handlePinEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handlePinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -305,7 +307,7 @@ func handlePinEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry pinned"})
|
||||
}
|
||||
|
||||
func handleUnpinEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleUnpinEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
id, err := params.Int(req.Params, "id")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -320,17 +322,17 @@ func handleUnpinEntry(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "entry unpinned"})
|
||||
}
|
||||
|
||||
func handleGetPinnedEntries(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetPinnedEntries(conn net.Conn, req models.Request, m *Manager) {
|
||||
pinned := m.GetPinnedEntries()
|
||||
models.Respond(conn, req.ID, pinned)
|
||||
}
|
||||
|
||||
func handleGetPinnedCount(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
|
||||
count := m.GetPinnedCount()
|
||||
models.Respond(conn, req.ID, map[string]int{"count": count})
|
||||
}
|
||||
|
||||
func handleCopyFile(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleCopyFile(conn net.Conn, req models.Request, m *Manager) {
|
||||
filePath, err := params.String(req.Params, "filePath")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
|
||||
@@ -311,15 +311,14 @@ func TestHandleGetEntry_ReturnsExistingEntry(t *testing.T) {
|
||||
history := m.GetHistory()
|
||||
require.Len(t, history, 1)
|
||||
|
||||
mc := newClipboardTestConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newClipboardTestConn()
|
||||
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(mc.writeBuf).Decode(&resp))
|
||||
require.NoError(t, json.NewDecoder(conn.writeBuf).Decode(&resp))
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.Equal(t, history[0].ID, resp.Result.ID)
|
||||
@@ -328,8 +327,7 @@ func TestHandleGetEntry_ReturnsExistingEntry(t *testing.T) {
|
||||
|
||||
func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
|
||||
m := newTestManagerWithDB(t)
|
||||
mc := newClipboardTestConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newClipboardTestConn()
|
||||
|
||||
handleGetEntry(conn, models.Request{
|
||||
ID: 1,
|
||||
@@ -337,7 +335,7 @@ func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
|
||||
}, m)
|
||||
|
||||
var resp models.Response[any]
|
||||
require.NoError(t, json.NewDecoder(mc.writeBuf).Decode(&resp))
|
||||
require.NoError(t, json.NewDecoder(conn.writeBuf).Decode(&resp))
|
||||
assert.Empty(t, resp.Error)
|
||||
assert.Nil(t, resp.Result)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/dankgo/ipc/params"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
type CUPSEvent struct {
|
||||
@@ -18,7 +20,7 @@ type TestPageResult struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "cups.subscribe":
|
||||
handleSubscribe(conn, req, manager)
|
||||
@@ -75,7 +77,7 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetPrinters(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetPrinters(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printers, err := manager.GetPrinters()
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -84,7 +86,7 @@ func handleGetPrinters(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, printers)
|
||||
}
|
||||
|
||||
func handleGetJobs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetJobs(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.String(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -99,7 +101,7 @@ func handleGetJobs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, jobs)
|
||||
}
|
||||
|
||||
func handlePausePrinter(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handlePausePrinter(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.String(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -113,7 +115,7 @@ func handlePausePrinter(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paused"})
|
||||
}
|
||||
|
||||
func handleResumePrinter(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleResumePrinter(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.String(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -127,7 +129,7 @@ func handleResumePrinter(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "resumed"})
|
||||
}
|
||||
|
||||
func handleCancelJob(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleCancelJob(conn net.Conn, req models.Request, manager *Manager) {
|
||||
jobID, err := params.Int(req.Params, "jobID")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -141,7 +143,7 @@ func handleCancelJob(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job canceled"})
|
||||
}
|
||||
|
||||
func handlePurgeJobs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handlePurgeJobs(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.String(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -155,7 +157,7 @@ func handlePurgeJobs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "jobs canceled"})
|
||||
}
|
||||
|
||||
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
@@ -166,7 +168,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
Data: initialState,
|
||||
}
|
||||
|
||||
if err := conn.WriteResponse(models.Response[CUPSEvent]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
|
||||
ID: req.ID,
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
@@ -178,7 +180,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
Type: "state_changed",
|
||||
Data: state,
|
||||
}
|
||||
if err := conn.WriteResponse(models.Response[CUPSEvent]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
@@ -186,7 +188,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetDevices(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetDevices(conn net.Conn, req models.Request, manager *Manager) {
|
||||
devices, err := manager.GetDevices()
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -195,7 +197,7 @@ func handleGetDevices(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, devices)
|
||||
}
|
||||
|
||||
func handleGetPPDs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetPPDs(conn net.Conn, req models.Request, manager *Manager) {
|
||||
ppds, err := manager.GetPPDs()
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -204,7 +206,7 @@ func handleGetPPDs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, ppds)
|
||||
}
|
||||
|
||||
func handleGetClasses(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetClasses(conn net.Conn, req models.Request, manager *Manager) {
|
||||
classes, err := manager.GetClasses()
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -213,7 +215,7 @@ func handleGetClasses(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, classes)
|
||||
}
|
||||
|
||||
func handleCreatePrinter(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleCreatePrinter(conn net.Conn, req models.Request, manager *Manager) {
|
||||
name, err := params.StringNonEmpty(req.Params, "name")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -244,7 +246,7 @@ func handleCreatePrinter(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer created"})
|
||||
}
|
||||
|
||||
func handleDeletePrinter(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleDeletePrinter(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -258,7 +260,7 @@ func handleDeletePrinter(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer deleted"})
|
||||
}
|
||||
|
||||
func handleAcceptJobs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleAcceptJobs(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -272,7 +274,7 @@ func handleAcceptJobs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "accepting jobs"})
|
||||
}
|
||||
|
||||
func handleRejectJobs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleRejectJobs(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -286,7 +288,7 @@ func handleRejectJobs(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "rejecting jobs"})
|
||||
}
|
||||
|
||||
func handleSetPrinterShared(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetPrinterShared(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -306,7 +308,7 @@ func handleSetPrinterShared(conn *models.Conn, req models.Request, manager *Mana
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sharing updated"})
|
||||
}
|
||||
|
||||
func handleSetPrinterLocation(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetPrinterLocation(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -326,7 +328,7 @@ func handleSetPrinterLocation(conn *models.Conn, req models.Request, manager *Ma
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location updated"})
|
||||
}
|
||||
|
||||
func handleSetPrinterInfo(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetPrinterInfo(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -346,7 +348,7 @@ func handleSetPrinterInfo(conn *models.Conn, req models.Request, manager *Manage
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "info updated"})
|
||||
}
|
||||
|
||||
func handleMoveJob(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleMoveJob(conn net.Conn, req models.Request, manager *Manager) {
|
||||
jobID, err := params.Int(req.Params, "jobID")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -366,7 +368,7 @@ func handleMoveJob(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job moved"})
|
||||
}
|
||||
|
||||
func handlePrintTestPage(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handlePrintTestPage(conn net.Conn, req models.Request, manager *Manager) {
|
||||
printerName, err := params.StringNonEmpty(req.Params, "printerName")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -381,7 +383,7 @@ func handlePrintTestPage(conn *models.Conn, req models.Request, manager *Manager
|
||||
models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"})
|
||||
}
|
||||
|
||||
func handleAddPrinterToClass(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleAddPrinterToClass(conn net.Conn, req models.Request, manager *Manager) {
|
||||
className, err := params.StringNonEmpty(req.Params, "className")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -401,7 +403,7 @@ func handleAddPrinterToClass(conn *models.Conn, req models.Request, manager *Man
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer added to class"})
|
||||
}
|
||||
|
||||
func handleRemovePrinterFromClass(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleRemovePrinterFromClass(conn net.Conn, req models.Request, manager *Manager) {
|
||||
className, err := params.StringNonEmpty(req.Params, "className")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -421,7 +423,7 @@ func handleRemovePrinterFromClass(conn *models.Conn, req models.Request, manager
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "printer removed from class"})
|
||||
}
|
||||
|
||||
func handleDeleteClass(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleDeleteClass(conn net.Conn, req models.Request, manager *Manager) {
|
||||
className, err := params.StringNonEmpty(req.Params, "className")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -435,7 +437,7 @@ func handleDeleteClass(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "class deleted"})
|
||||
}
|
||||
|
||||
func handleRestartJob(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleRestartJob(conn net.Conn, req models.Request, manager *Manager) {
|
||||
jobID, err := params.Int(req.Params, "jobID")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -449,7 +451,7 @@ func handleRestartJob(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job restarted"})
|
||||
}
|
||||
|
||||
func handleHoldJob(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
|
||||
jobID, err := params.Int(req.Params, "jobID")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -465,7 +467,7 @@ func handleHoldJob(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
|
||||
}
|
||||
|
||||
func handleTestConnection(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
|
||||
host, err := params.StringNonEmpty(req.Params, "host")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestHandleGetPrinters(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
@@ -66,7 +66,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
@@ -98,7 +98,7 @@ func TestHandleGetJobs(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
@@ -125,7 +125,7 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
@@ -258,7 +258,7 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
|
||||
@@ -229,6 +229,9 @@ func (m *Manager) snapshotState() CUPSState {
|
||||
func (m *Manager) Subscribe(id string) chan CUPSState {
|
||||
ch := make(chan CUPSState, 64)
|
||||
|
||||
m.subLifecycleMu.Lock()
|
||||
defer m.subLifecycleMu.Unlock()
|
||||
|
||||
wasEmpty := true
|
||||
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
||||
wasEmpty = false
|
||||
@@ -237,19 +240,25 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
|
||||
|
||||
m.subscribers.Store(id, ch)
|
||||
|
||||
if wasEmpty && m.subscription != nil {
|
||||
if err := m.subscription.Start(); err != nil {
|
||||
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
||||
} else {
|
||||
m.eventWG.Add(1)
|
||||
go m.eventHandler()
|
||||
}
|
||||
if !wasEmpty || m.subscription == nil {
|
||||
return ch
|
||||
}
|
||||
|
||||
if err := m.subscription.Start(); err != nil {
|
||||
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
||||
return ch
|
||||
}
|
||||
|
||||
m.eventWG.Add(1)
|
||||
go m.eventHandler()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
m.subLifecycleMu.Lock()
|
||||
defer m.subLifecycleMu.Unlock()
|
||||
|
||||
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||
close(val)
|
||||
}
|
||||
@@ -260,18 +269,22 @@ func (m *Manager) Unsubscribe(id string) {
|
||||
return false
|
||||
})
|
||||
|
||||
if isEmpty && m.subscription != nil {
|
||||
m.subscription.Stop()
|
||||
m.eventWG.Wait()
|
||||
if !isEmpty || m.subscription == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.subscription.Stop()
|
||||
m.eventWG.Wait()
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
close(m.stopChan)
|
||||
|
||||
m.subLifecycleMu.Lock()
|
||||
if m.subscription != nil {
|
||||
m.subscription.Stop()
|
||||
}
|
||||
m.subLifecycleMu.Unlock()
|
||||
|
||||
m.eventWG.Wait()
|
||||
m.notifierWg.Wait()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
|
||||
@@ -75,6 +78,67 @@ func TestManager_Subscribe(t *testing.T) {
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
// mirrors the real managers: eventChan guarded by mu, conn/running deliberately
|
||||
// unsynchronized so overlapping Start/Stop trips the race detector
|
||||
type stubSubscription struct {
|
||||
mu sync.Mutex
|
||||
events chan SubscriptionEvent
|
||||
conn *int
|
||||
running bool
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Start() error {
|
||||
if s.running {
|
||||
return errors.New("already running")
|
||||
}
|
||||
s.running = true
|
||||
|
||||
s.mu.Lock()
|
||||
s.events = make(chan SubscriptionEvent)
|
||||
s.mu.Unlock()
|
||||
|
||||
v := 0
|
||||
s.conn = &v
|
||||
*s.conn++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Stop() {
|
||||
if !s.running {
|
||||
return
|
||||
}
|
||||
s.running = false
|
||||
s.conn = nil
|
||||
|
||||
s.mu.Lock()
|
||||
close(s.events)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Events() <-chan SubscriptionEvent {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.events
|
||||
}
|
||||
|
||||
func TestManager_SubscribeUnsubscribeRace(t *testing.T) {
|
||||
m := NewTestManager(mocks_cups.NewMockCUPSClientInterface(t), nil)
|
||||
m.subscription = &stubSubscription{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range 8 {
|
||||
wg.Go(func() {
|
||||
id := fmt.Sprintf("client-%d", i)
|
||||
for range 50 {
|
||||
m.Subscribe(id)
|
||||
m.Unsubscribe(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestManager_Close(t *testing.T) {
|
||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ func TestHandleTestConnection_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &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 := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
@@ -351,7 +351,7 @@ func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
@@ -379,7 +379,7 @@ func TestHandleRequest_TestConnection(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := models.NewConn(&mockConn{Buffer: buf})
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type CUPSState struct {
|
||||
@@ -79,6 +79,7 @@ type Manager struct {
|
||||
client CUPSClientInterface
|
||||
pkHelper PkHelper
|
||||
subscription SubscriptionManagerInterface
|
||||
subLifecycleMu sync.Mutex
|
||||
stateMutex sync.RWMutex
|
||||
subscribers syncmap.Map[string, chan CUPSState]
|
||||
stopChan chan struct{}
|
||||
|
||||
@@ -2,9 +2,10 @@ package dbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/dankgo/ipc/params"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
type objectParams struct {
|
||||
@@ -42,7 +43,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 *models.Conn, req models.Request, m *Manager, clientID string) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, m *Manager, clientID string) {
|
||||
switch req.Method {
|
||||
case "dbus.call":
|
||||
handleCall(conn, req, m)
|
||||
@@ -65,7 +66,7 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager, clientID s
|
||||
}
|
||||
}
|
||||
|
||||
func handleCall(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleCall(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -94,7 +95,7 @@ func handleCall(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleGetProperty(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetProperty(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -116,7 +117,7 @@ func handleGetProperty(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleSetProperty(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleSetProperty(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -143,7 +144,7 @@ func handleSetProperty(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
|
||||
}
|
||||
|
||||
func handleGetAllProperties(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetAllProperties(conn net.Conn, req models.Request, m *Manager) {
|
||||
op, err := extractObjectParams(req.Params, true)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -159,7 +160,7 @@ func handleGetAllProperties(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleIntrospect(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleIntrospect(conn net.Conn, req models.Request, m *Manager) {
|
||||
bus, err := params.String(req.Params, "bus")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -183,7 +184,7 @@ func handleIntrospect(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleListNames(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleListNames(conn net.Conn, req models.Request, m *Manager) {
|
||||
bus, err := params.String(req.Params, "bus")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -199,7 +200,7 @@ func handleListNames(conn *models.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleSubscribe(conn *models.Conn, req models.Request, m *Manager, clientID string) {
|
||||
func handleSubscribe(conn net.Conn, req models.Request, m *Manager, clientID string) {
|
||||
if id := params.StringOpt(req.Params, "clientId", ""); id != "" {
|
||||
clientID = id
|
||||
}
|
||||
@@ -224,7 +225,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager, clientID
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
func handleUnsubscribe(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleUnsubscribe(conn net.Conn, req models.Request, m *Manager) {
|
||||
subID, err := params.String(req.Params, "subscriptionId")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/dankgo/dbusutil"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dbus
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package evdev
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
|
||||
func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
switch req.Method {
|
||||
case "evdev.getState":
|
||||
handleGetState(conn, req, m)
|
||||
@@ -13,6 +15,6 @@ func HandleRequest(conn *models.Conn, req models.Request, m *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn *models.Conn, req models.Request, m *Manager) {
|
||||
func handleGetState(conn net.Conn, req models.Request, m *Manager) {
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
@@ -52,8 +52,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
closeChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "evdev.getState",
|
||||
@@ -63,7 +62,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
HandleRequest(conn, req, m)
|
||||
|
||||
var resp models.Response[State]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -82,8 +81,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
closeChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 456,
|
||||
Method: "evdev.unknownMethod",
|
||||
@@ -93,7 +91,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
HandleRequest(conn, req, m)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 456, resp.ID)
|
||||
@@ -112,8 +110,7 @@ func TestHandleGetState(t *testing.T) {
|
||||
closeChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 789,
|
||||
Method: "evdev.getState",
|
||||
@@ -123,7 +120,7 @@ func TestHandleGetState(t *testing.T) {
|
||||
handleGetState(conn, req, m)
|
||||
|
||||
var resp models.Response[State]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 789, resp.ID)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
evdev "github.com/holoplot/go-evdev"
|
||||
)
|
||||
|
||||
@@ -2,12 +2,13 @@ package freedesktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/dankgo/ipc/params"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||
)
|
||||
|
||||
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "freedesktop.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
@@ -32,11 +33,11 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, manager.GetState())
|
||||
}
|
||||
|
||||
func handleSetIconFile(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetIconFile(conn net.Conn, req models.Request, manager *Manager) {
|
||||
iconPath, err := params.String(req.Params, "path")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -51,7 +52,7 @@ func handleSetIconFile(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "icon file set"})
|
||||
}
|
||||
|
||||
func handleSetRealName(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetRealName(conn net.Conn, req models.Request, manager *Manager) {
|
||||
name, err := params.String(req.Params, "name")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -66,7 +67,7 @@ func handleSetRealName(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "real name set"})
|
||||
}
|
||||
|
||||
func handleSetEmail(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetEmail(conn net.Conn, req models.Request, manager *Manager) {
|
||||
email, err := params.String(req.Params, "email")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -81,7 +82,7 @@ func handleSetEmail(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "email set"})
|
||||
}
|
||||
|
||||
func handleSetLanguage(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetLanguage(conn net.Conn, req models.Request, manager *Manager) {
|
||||
language, err := params.String(req.Params, "language")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -96,7 +97,7 @@ func handleSetLanguage(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "language set"})
|
||||
}
|
||||
|
||||
func handleSetLocation(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
|
||||
location, err := params.String(req.Params, "location")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -111,7 +112,7 @@ func handleSetLocation(conn *models.Conn, req models.Request, manager *Manager)
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "location set"})
|
||||
}
|
||||
|
||||
func handleGetUserIconFile(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetUserIconFile(conn net.Conn, req models.Request, manager *Manager) {
|
||||
username, err := params.String(req.Params, "username")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -127,7 +128,7 @@ func handleGetUserIconFile(conn *models.Conn, req models.Request, manager *Manag
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Value: iconFile})
|
||||
}
|
||||
|
||||
func handleGetColorScheme(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetColorScheme(conn net.Conn, req models.Request, manager *Manager) {
|
||||
if err := manager.updateSettingsState(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
@@ -137,7 +138,7 @@ func handleGetColorScheme(conn *models.Conn, req models.Request, manager *Manage
|
||||
models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme})
|
||||
}
|
||||
|
||||
func handleSetIconTheme(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSetIconTheme(conn net.Conn, req models.Request, manager *Manager) {
|
||||
iconTheme, err := params.String(req.Params, "iconTheme")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
|
||||
@@ -60,12 +60,11 @@ func mockGetAllAccountsProperties() *dbus.Call {
|
||||
}
|
||||
|
||||
func TestRespondError_Freedesktop(t *testing.T) {
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
models.RespondError(conn, 123, "test error")
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -74,13 +73,12 @@ func TestRespondError_Freedesktop(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRespond_Freedesktop(t *testing.T) {
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
result := models.SuccessResult{Success: true, Message: "test"}
|
||||
models.Respond(conn, 123, result)
|
||||
|
||||
var resp models.Response[models.SuccessResult]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -107,14 +105,13 @@ func TestHandleGetState(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{ID: 123, Method: "freedesktop.getState"}
|
||||
|
||||
handleGetState(conn, req, manager)
|
||||
|
||||
var resp models.Response[FreedeskState]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -133,8 +130,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setIconFile",
|
||||
@@ -144,7 +140,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
||||
handleSetIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -167,8 +163,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
||||
accountsObj: mockAccountsObj,
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setIconFile",
|
||||
@@ -180,7 +175,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
||||
handleSetIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[models.SuccessResult]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -200,8 +195,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setIconFile",
|
||||
@@ -213,7 +207,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
||||
handleSetIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[models.SuccessResult]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -228,8 +222,7 @@ func TestHandleSetRealName(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setRealName",
|
||||
@@ -239,7 +232,7 @@ func TestHandleSetRealName(t *testing.T) {
|
||||
handleSetRealName(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -262,8 +255,7 @@ func TestHandleSetRealName(t *testing.T) {
|
||||
accountsObj: mockAccountsObj,
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setRealName",
|
||||
@@ -275,7 +267,7 @@ func TestHandleSetRealName(t *testing.T) {
|
||||
handleSetRealName(conn, req, manager)
|
||||
|
||||
var resp models.Response[models.SuccessResult]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -293,8 +285,7 @@ func TestHandleSetEmail(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setEmail",
|
||||
@@ -304,7 +295,7 @@ func TestHandleSetEmail(t *testing.T) {
|
||||
handleSetEmail(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -327,8 +318,7 @@ func TestHandleSetEmail(t *testing.T) {
|
||||
accountsObj: mockAccountsObj,
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setEmail",
|
||||
@@ -340,7 +330,7 @@ func TestHandleSetEmail(t *testing.T) {
|
||||
handleSetEmail(conn, req, manager)
|
||||
|
||||
var resp models.Response[models.SuccessResult]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -358,8 +348,7 @@ func TestHandleSetLanguage(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setLanguage",
|
||||
@@ -369,7 +358,7 @@ func TestHandleSetLanguage(t *testing.T) {
|
||||
handleSetLanguage(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -384,8 +373,7 @@ func TestHandleSetLocation(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setLocation",
|
||||
@@ -395,7 +383,7 @@ func TestHandleSetLocation(t *testing.T) {
|
||||
handleSetLocation(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -410,8 +398,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.getUserIconFile",
|
||||
@@ -421,7 +408,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
|
||||
handleGetUserIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -438,8 +425,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.getUserIconFile",
|
||||
@@ -451,7 +437,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
|
||||
handleGetUserIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[models.SuccessResult]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -470,14 +456,13 @@ func TestHandleGetColorScheme(t *testing.T) {
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
|
||||
|
||||
handleGetColorScheme(conn, req, manager)
|
||||
|
||||
var resp models.Response[map[string]uint32]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -502,14 +487,13 @@ func TestHandleGetColorScheme(t *testing.T) {
|
||||
settingsObj: mockSettingsObj,
|
||||
}
|
||||
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
|
||||
|
||||
handleGetColorScheme(conn, req, manager)
|
||||
|
||||
var resp models.Response[map[string]uint32]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -531,8 +515,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("unknown method", func(t *testing.T) {
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.unknown",
|
||||
@@ -541,7 +524,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
HandleRequest(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -549,8 +532,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("valid method - getState", func(t *testing.T) {
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.getState",
|
||||
@@ -559,7 +541,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
HandleRequest(conn, req, manager)
|
||||
|
||||
var resp models.Response[FreedeskState]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
@@ -578,8 +560,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, method := range tests {
|
||||
mc := newMockNetConn()
|
||||
conn := models.NewConn(mc)
|
||||
conn := newMockNetConn()
|
||||
req := models.Request{
|
||||
ID: 123,
|
||||
Method: method,
|
||||
@@ -589,7 +570,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
HandleRequest(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(mc.writeBuf).Decode(&resp)
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/dankgo/dbusutil"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
@@ -11,7 +13,7 @@ type LocationEvent struct {
|
||||
Data State `json:"data"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "location.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
@@ -23,11 +25,11 @@ func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, manager.GetState())
|
||||
}
|
||||
|
||||
func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
@@ -38,7 +40,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
Data: initialState,
|
||||
}
|
||||
|
||||
if err := conn.WriteResponse(models.Response[LocationEvent]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
|
||||
ID: req.ID,
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
@@ -50,7 +52,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
|
||||
Type: "state_changed",
|
||||
Data: state,
|
||||
}
|
||||
if err := conn.WriteResponse(models.Response[LocationEvent]{
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user