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

Compare commits

..

3 Commits

Author SHA1 Message Date
bbedward 990d86d481 flake: update-common 2026-07-18 10:43:43 -04:00
bbedward 526cb157fd i18n: update sync scrirpt for dank-qml-common 2026-07-18 10:43:43 -04:00
bbedward 92ba96d9f9 qs: integrate with dank-qml-common 2026-07-18 10:43:43 -04:00
509 changed files with 35260 additions and 38947 deletions
+1 -32
View File
@@ -50,39 +50,8 @@ jobs:
- name: Build dms - name: Build dms
run: go build -v ./cmd/dms run: go build -v ./cmd/dms
- name: Build dms (embedded shell)
run: make build
- name: Build dms (distropkg) - 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 - name: Build dankinstall
run: go build -v ./cmd/dankinstall run: go build -v ./cmd/dankinstall
build-freebsd:
runs-on: ubuntu-latest
defaults:
run:
working-directory: core
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
- name: Build all packages
env:
GOOS: freebsd
CGO_ENABLED: 0
run: go build -v ./...
- name: Build dms (distropkg, embedded shell)
run: |
make sync-shell
GOOS=freebsd CGO_ENABLED=0 go build -v -tags 'distro_binary withshell' ./cmd/dms
+2
View File
@@ -33,6 +33,8 @@ jobs:
- name: Run NixOS service start test - name: Run NixOS service start test
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L 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 - name: Run home-manager module test
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
+46 -74
View File
@@ -27,11 +27,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
include: arch: [amd64, arm64]
- { goos: linux, arch: amd64 }
- { goos: linux, arch: arm64 }
- { goos: freebsd, arch: amd64 }
- { goos: freebsd, arch: arm64 }
defaults: defaults:
run: run:
@@ -39,8 +35,6 @@ jobs:
env: env:
TAG: ${{ inputs.tag }} 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: steps:
- name: Checkout - name: Checkout
@@ -56,7 +50,6 @@ jobs:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
- name: Format check - name: Format check
if: matrix.goos == 'linux'
run: | run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:" echo "The following files are not formatted:"
@@ -65,43 +58,38 @@ jobs:
fi fi
- name: Run tests - name: Run tests
if: matrix.goos == 'linux'
run: go test -v ./... run: go test -v ./...
- name: Build dankinstall (${{ env.ASSET }}) - name: Build dankinstall (${{ matrix.arch }})
if: matrix.goos == 'linux'
env: env:
GOOS: ${{ matrix.goos }} GOOS: linux
CGO_ENABLED: 0 CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
run: | run: |
set -eux set -eux
cd cmd/dankinstall cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \ go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ env.ASSET }} -o ../../dankinstall-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dankinstall-${{ env.ASSET }} gzip -9 -k dankinstall-${{ matrix.arch }}
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256 sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
- name: Sync embedded shell - name: Build dms (${{ matrix.arch }})
run: make sync-shell
- name: Build dms (${{ env.ASSET }})
env: env:
GOOS: ${{ matrix.goos }} GOOS: linux
CGO_ENABLED: 0 CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
run: | run: |
set -eux set -eux
cd cmd/dms cd cmd/dms
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \ go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ env.ASSET }} -o ../../dms-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dms-${{ env.ASSET }} gzip -9 -k dms-${{ matrix.arch }}
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256 sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
- name: Generate shell completions - name: Generate shell completions
if: matrix.goos == 'linux' && matrix.arch == 'amd64' if: matrix.arch == 'amd64'
run: | run: |
set -eux set -eux
chmod +x dms-amd64 chmod +x dms-amd64
@@ -109,58 +97,46 @@ jobs:
./dms-amd64 completion fish > completion.fish ./dms-amd64 completion fish > completion.fish
./dms-amd64 completion zsh > completion.zsh ./dms-amd64 completion zsh > completion.zsh
- name: Build dms-distropkg (${{ env.ASSET }}) - name: Build dms-distropkg (${{ matrix.arch }})
env: env:
GOOS: ${{ matrix.goos }} GOOS: linux
CGO_ENABLED: 0 CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
run: | run: |
set -eux set -eux
cd cmd/dms cd cmd/dms
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \ go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ env.ASSET }} -o ../../dms-distropkg-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dms-distropkg-${{ env.ASSET }} gzip -9 -k dms-distropkg-${{ matrix.arch }}
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256 sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
- name: Upload artifacts (${{ env.ASSET }}) - name: Upload artifacts (${{ matrix.arch }})
if: matrix.goos == 'linux' && matrix.arch == 'arm64' if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: core-assets-${{ env.ASSET }} name: core-assets-${{ matrix.arch }}
path: | path: |
core/dankinstall-${{ env.ASSET }}.gz core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ env.ASSET }}.gz.sha256 core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ env.ASSET }}.gz core/dms-${{ matrix.arch }}.gz
core/dms-${{ env.ASSET }}.gz.sha256 core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256 core/dms-distropkg-${{ matrix.arch }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts (${{ env.ASSET }}, no installer)
if: matrix.goos != 'linux'
uses: actions/upload-artifact@v5
with:
name: core-assets-${{ env.ASSET }}
path: |
core/dms-${{ env.ASSET }}.gz
core/dms-${{ env.ASSET }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
if-no-files-found: error if-no-files-found: error
- name: Upload artifacts with completions - name: Upload artifacts with completions
if: matrix.goos == 'linux' && matrix.arch == 'amd64' if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: core-assets-${{ env.ASSET }} name: core-assets-${{ matrix.arch }}
path: | path: |
core/dankinstall-${{ env.ASSET }}.gz core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ env.ASSET }}.gz.sha256 core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ env.ASSET }}.gz core/dms-${{ matrix.arch }}.gz
core/dms-${{ env.ASSET }}.gz.sha256 core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ env.ASSET }}.gz core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ env.ASSET }}.gz.sha256 core/dms-distropkg-${{ matrix.arch }}.gz.sha256
core/completion.bash core/completion.bash
core/completion.fish core/completion.fish
core/completion.zsh core/completion.zsh
@@ -269,18 +245,12 @@ jobs:
### Complete Packages ### Complete Packages
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide) - **`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-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 ### Individual Components
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems - **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 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-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-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-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems - **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging) - **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
@@ -365,8 +335,8 @@ jobs:
# Generate checksum for QML package # Generate checksum for QML package
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256) (cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
# Create full packages for each os/architecture # Create full packages for each architecture
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do for arch in amd64 arm64; do
mkdir -p _temp_full/dms mkdir -p _temp_full/dms
mkdir -p _temp_full/bin mkdir -p _temp_full/bin
mkdir -p _temp_full/completions mkdir -p _temp_full/completions
@@ -409,16 +379,18 @@ jobs:
## Installation Steps ## Installation Steps
The Quickshell UI is embedded in the `dms` binary. The bundled `dms/` 1. **Install quickshell assets:**
tree is optional — pass it with `-c` or `DMS_SHELL_DIR` to run a ```bash
modified copy instead of the embedded UI. mkdir -p ~/.config/quickshell
cp -r dms ~/.config/quickshell/
```
1. **Install the DMS CLI binaries:** 2. **Install the DMS CLI binaries:**
```bash ```bash
sudo install -m 755 bin/dms /usr/local/bin/dms sudo install -m 755 bin/dms /usr/local/bin/dms
``` ```
2. **Install shell completions (optional):** 3. **Install shell completions (optional):**
```bash ```bash
# Bash # Bash
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
@@ -430,7 +402,7 @@ jobs:
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
``` ```
3. **Start the shell:** 4. **Start the shell:**
```bash ```bash
dms run dms run
``` ```
+10 -2
View File
@@ -4,12 +4,14 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
package: package:
description: 'Package to build' description: 'Package to build (dms, dms-greeter, or both)'
required: false required: false
default: 'dms' default: 'dms'
type: choice type: choice
options: options:
- dms - dms
- dms-greeter
- both
version: version:
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)' description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
required: false required: false
@@ -29,7 +31,11 @@ jobs:
id: set-packages id: set-packages
run: | run: |
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}" 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: build-and-upload:
needs: determine-packages needs: determine-packages
@@ -157,6 +163,8 @@ jobs:
PACKAGE="${{ matrix.package }}" PACKAGE="${{ matrix.package }}"
if [ "$PACKAGE" = "dms" ]; then if [ "$PACKAGE" = "dms" ]; then
COPR_PROJECT="avengemedia/dms" COPR_PROJECT="avengemedia/dms"
elif [ "$PACKAGE" = "dms-greeter" ]; then
COPR_PROJECT="avengemedia/danklinux"
else else
echo "❌ Unknown package: $PACKAGE" echo "❌ Unknown package: $PACKAGE"
exit 1 exit 1
+57 -9
View File
@@ -9,6 +9,7 @@ on:
type: choice type: choice
options: options:
- dms - dms
- dms-greeter
- dms-git - dms-git
- all - all
default: "dms" default: "dms"
@@ -74,12 +75,27 @@ jobs:
fi 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 # Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}" REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
# Run from tag with no package specified - update stable package # Run from tag with no package specified - update both stable packages
echo "packages=dms" >> $GITHUB_OUTPUT echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}" VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
@@ -105,7 +121,7 @@ jobs:
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)" echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
elif [[ "$PKG" == "all" ]]; then 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=() PACKAGES_TO_UPDATE=()
if check_dms_stable; then if check_dms_stable; then
PACKAGES_TO_UPDATE+=("dms") PACKAGES_TO_UPDATE+=("dms")
@@ -113,6 +129,10 @@ jobs:
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi fi
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 if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -121,7 +141,7 @@ jobs:
else else
echo "packages=" >> $GITHUB_OUTPUT echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date" echo "✓ Both packages up to date"
fi fi
elif [[ "$PKG" == "dms-git" ]]; then elif [[ "$PKG" == "dms-git" ]]; then
@@ -145,6 +165,18 @@ jobs:
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
fi 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 else
# Unknown package - proceed anyway # Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT echo "packages=$PKG" >> $GITHUB_OUTPUT
@@ -189,9 +221,9 @@ jobs:
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch # 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 # 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 "") 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 if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
@@ -257,7 +289,7 @@ jobs:
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE" echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog" } > "distro/debian/dms-git/debian/changelog"
- name: Update stable version (dms) - name: Update stable version (dms + dms-greeter)
if: steps.packages.outputs.version != '' if: steps.packages.outputs.version != ''
run: | run: |
VERSION="${{ steps.packages.outputs.version }}" VERSION="${{ steps.packages.outputs.version }}"
@@ -293,12 +325,25 @@ jobs:
fi fi
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) # Update Debian _service files for packages in upload list (download_url paths)
for service in distro/debian/*/_service; do for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git) # Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service" 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|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service" sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi fi
@@ -359,7 +404,7 @@ jobs:
UPLOADED_PACKAGES=() UPLOADED_PACKAGES=()
SKIPPED_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 # Loop through each package and upload
for PKG in $PACKAGES; do for PKG in $PACKAGES; do
echo "" echo ""
@@ -462,6 +507,9 @@ jobs:
dms-git) 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 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 esac
done done
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
+3
View File
@@ -9,6 +9,7 @@ on:
type: choice type: choice
options: options:
- dms - dms
- dms-greeter
- dms-git - dms-git
- all - all
default: "dms" default: "dms"
@@ -134,6 +135,7 @@ jobs:
case "$PACKAGE" in case "$PACKAGE" in
dms) PPA_NAME="dms" ;; dms) PPA_NAME="dms" ;;
dms-git) PPA_NAME="dms-git" ;; dms-git) PPA_NAME="dms-git" ;;
dms-greeter) PPA_NAME="danklinux" ;;
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;; *) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
esac esac
@@ -148,3 +150,4 @@ jobs:
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY" echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$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 "- **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"
+32 -4
View File
@@ -27,6 +27,11 @@ on:
type: boolean type: boolean
required: false required: false
default: true default: true
build_greeter:
description: 'Build stable dms-greeter package'
type: boolean
required: false
default: true
permissions: permissions:
contents: read contents: read
@@ -101,6 +106,7 @@ jobs:
- name: Inject templates - name: Inject templates
run: | run: |
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/ 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/ cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
- name: Enable unprivileged user namespaces (Ubuntu 24.04) - name: Enable unprivileged user namespaces (Ubuntu 24.04)
@@ -174,16 +180,19 @@ jobs:
mkdir -p r2-repo/current mkdir -p r2-repo/current
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}" BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}"
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}" BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}" FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
if [ "${{ github.event_name }}" = "schedule" ]; then if [ "${{ github.event_name }}" = "schedule" ]; then
BUILD_DMS="false" BUILD_DMS="false"
BUILD_GREETER="false"
BUILD_GIT="true" BUILD_GIT="true"
fi fi
if [ "${{ github.event_name }}" = "release" ]; then if [ "${{ github.event_name }}" = "release" ]; then
BUILD_DMS="true" BUILD_DMS="true"
BUILD_GREETER="true"
BUILD_GIT="false" BUILD_GIT="false"
fi fi
@@ -208,20 +217,23 @@ jobs:
echo "=== Starting Builds ===" echo "=== Starting Builds ==="
echo "DMS stable build enabled: $BUILD_DMS" echo "DMS stable build enabled: $BUILD_DMS"
echo "Greeter stable build enabled: $BUILD_GREETER"
echo "Git build enabled: $BUILD_GIT" echo "Git build enabled: $BUILD_GIT"
echo "Force rebuild: $FORCE_REBUILD" echo "Force rebuild: $FORCE_REBUILD"
cd void-packages cd void-packages
if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then
echo "🔧 Updating stable template for $ARCHIVE_TAG" echo "🔧 Updating stable templates for $ARCHIVE_TAG"
TARBALL="$(mktemp)" 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 }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)" RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
rm -f "$TARBALL" rm -f "$TARBALL"
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template for pkg in dms dms-greeter; do
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template sed -i "s/^version=.*/version=${RELEASE_VER}/" "srcpkgs/${pkg}/template"
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" "srcpkgs/${pkg}/template"
done
fi fi
# 1. Build dms-git (development package) # 1. Build dms-git (development package)
@@ -277,6 +289,22 @@ jobs:
fi fi
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 - name: Index and sign repository
run: | run: |
cd r2-repo/current cd r2-repo/current
-2
View File
@@ -129,5 +129,3 @@ distro/void/masterdir*/
# Often gets built # Often gets built
core/dms core/dms
core/internal/shellembed/dist
-1
View File
@@ -1,4 +1,3 @@
[submodule "dank-qml-common"] [submodule "dank-qml-common"]
path = dank-qml-common path = dank-qml-common
url = https://github.com/AvengeMedia/dank-qml-common.git url = https://github.com/AvengeMedia/dank-qml-common.git
branch = master
-8
View File
@@ -1,13 +1,5 @@
This file is more of a quick reference so I know what to account for before next releases. This file is more of a quick reference so I know what to account for before next releases.
# 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 # 1.5.0
- Overhauled shadows - Overhauled shadows
- App ID changed to com.danklinux.dms - breaking for window rules - App ID changed to com.danklinux.dms - breaking for window rules
-29
View File
@@ -14,12 +14,6 @@ git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.
git submodule update --init git submodule update --init
``` ```
To have `git pull` keep the submodule in sync automatically (moving it to the commit this repo points at, no separate `git submodule update` step), set:
```bash
git config submodule.recurse true
```
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks: Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
```bash ```bash
@@ -42,29 +36,6 @@ This will provide:
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory. 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) ## 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: 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 -11
View File
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build 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 all: build
@@ -27,12 +27,6 @@ build:
@$(MAKE) -C $(CORE_DIR) build @$(MAKE) -C $(CORE_DIR) build
@echo "Build complete" @echo "Build complete"
dev:
@$(MAKE) -C $(CORE_DIR) dev
run: dev
@$(BUILD_DIR)/$(BINARY_NAME) run -c $(CURDIR)/$(SHELL_DIR)
clean: clean:
@echo "Cleaning build artifacts..." @echo "Cleaning build artifacts..."
@$(MAKE) -C $(CORE_DIR) clean @$(MAKE) -C $(CORE_DIR) clean
@@ -72,9 +66,6 @@ install-completions:
@echo "Shell completions installed" @echo "Shell completions installed"
install-systemd: 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..." @echo "Installing systemd user service..."
@mkdir -p $(SYSTEMD_USER_DIR) @mkdir -p $(SYSTEMD_USER_DIR)
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi @if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
@@ -82,7 +73,6 @@ else
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service @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 @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" @echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
endif
install-icon: install-icon:
@echo "Installing icon..." @echo "Installing icon..."
+1 -12
View File
@@ -98,7 +98,7 @@ Notification center with grouping, rich text support, and keyboard navigation.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews. MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management** **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** **Plugin System**
Extend functionality with the [plugin registry](https://plugins.danklinux.com). 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) - **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc) - **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 ## Development
See component-specific documentation: See component-specific documentation:
+26 -50
View File
@@ -3,8 +3,6 @@ BINARY_NAME_INSTALL=dankinstall
SOURCE_DIR=cmd/dms SOURCE_DIR=cmd/dms
SOURCE_DIR_INSTALL=cmd/dankinstall SOURCE_DIR_INSTALL=cmd/dankinstall
BUILD_DIR=bin BUILD_DIR=bin
SHELL_SRC=../quickshell
EMBED_DIR=internal/shellembed/dist
PREFIX ?= /usr/local PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin 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) # Architecture to build for dist target (amd64, arm64, or all)
ARCH ?= all ARCH ?= all
# Target OSes for dist builds
DIST_OSES ?= linux freebsd
ifeq ($(ARCH),all) .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
DIST_ARCHS = amd64 arm64
else
DIST_ARCHS = $(ARCH)
endif
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
# Default target # Default target
all: build 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 the main binary (dms)
build: sync-shell build:
@echo "Building $(BINARY_NAME)..." @echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR) @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)" @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
dankinstall: dankinstall:
@@ -71,18 +40,26 @@ dankinstall:
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL) CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)" @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
# Build distro binaries (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair # Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
dist: sync-shell dist:
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..." ifeq ($(ARCH),all)
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@for os in $(DIST_OSES); do \ @echo "Building for linux/amd64..."
for arch in $(DIST_ARCHS); do \ 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 $$os/$$arch..."; \ @echo "Building for linux/arm64..."
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; \ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \ @echo "Distribution builds complete:"
done; \ @echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
done @echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
@echo "Distribution builds complete" 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 build-all: build dankinstall
@@ -122,7 +99,7 @@ uninstall-dankinstall:
clean: clean:
@echo "Cleaning build artifacts..." @echo "Cleaning build artifacts..."
@rm -rf $(BUILD_DIR) $(EMBED_DIR) @rm -rf $(BUILD_DIR)
@echo "Clean complete" @echo "Clean complete"
test: test:
@@ -164,11 +141,10 @@ print-version:
help: help:
@echo "Available targets:" @echo "Available targets:"
@echo " all - Build the main binary (dms) (default)" @echo " all - Build the main binary (dms) (default)"
@echo " build - Build the main binary (dms) with the embedded UI" @echo " build - Build the main binary (dms)"
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
@echo " dankinstall - Build dankinstall binary" @echo " dankinstall - Build dankinstall binary"
@echo " dist - Build dms for linux/freebsd amd64/arm64 (no update/greeter)" @echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow" @echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
@echo " build-all - Build both binaries" @echo " build-all - Build both binaries"
@echo " install - Install dms to $(INSTALL_DIR)" @echo " install - Install dms to $(INSTALL_DIR)"
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)" @echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
+3 -3
View File
@@ -78,7 +78,7 @@ Custom IPC via unix socket (JSON API) for shell communication.
- `dms brightness [list|set]` - Control display/monitor brightness - `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below) - `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages) - `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 ### Color Picker
@@ -109,7 +109,7 @@ make test # Run tests
**Distribution build:** **Distribution build:**
```bash ```bash
make dist # Build without update features make dist # Build without update/greeter features
``` ```
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64` 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 | | `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) | | `--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`) | | `--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. 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. Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
+1 -30
View File
@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -16,7 +15,7 @@ import (
var authCmd = &cobra.Command{ var authCmd = &cobra.Command{
Use: "auth", Use: "auth",
Short: "Manage DMS authentication sync", 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{ 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` shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
return runCommandInTerminal(shellCmd) 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)")
}
+78 -30
View File
@@ -6,10 +6,10 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" "github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server" "github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -19,6 +19,63 @@ var versionCmd = &cobra.Command{
Run: runVersion, 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{ var ipcCmd = &cobra.Command{
Use: "ipc", Use: "ipc",
Short: "Send IPC commands to running DMS shell", Short: "Send IPC commands to running DMS shell",
@@ -207,41 +264,29 @@ func formatVersion(version string) string {
return fmt.Sprintf("dms %s", version) 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 { func getBaseVersion() string {
if ver := parseBaseVersion(shellembed.Version()); ver != "" { paths := []string{
return ver "/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
} }
for _, path := range shellVersionPaths { for _, path := range paths {
content, err := os.ReadFile(path) if content, err := os.ReadFile(path); err == nil {
if err != nil { ver := strings.TrimSpace(string(content))
continue ver = strings.TrimPrefix(ver, "v")
} if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
if ver := parseBaseVersion(string(content)); ver != "" { if matches := re.FindStringSubmatch(ver); matches != nil {
return ver return matches[1]
}
}
} }
} }
// Fallback
return "1.0.2" 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 { func startDebugServer() error {
server.CLIVersion = Version server.CLIVersion = Version
return server.Start(true) return server.Start(true)
@@ -698,9 +743,12 @@ func checkAllPluginsCLI() error {
} }
func getCommonCommands() []*cobra.Command { func getCommonCommands() []*cobra.Command {
commands := shellApp.Commands() return []*cobra.Command{
return append(commands, []*cobra.Command{
versionCmd, versionCmd,
runCmd,
restartCmd,
restartDetachedCmd,
killCmd,
ipcCmd, ipcCmd,
debugSrvCmd, debugSrvCmd,
pluginsCmd, pluginsCmd,
@@ -727,5 +775,5 @@ func getCommonCommands() []*cobra.Command {
trashCmd, trashCmd,
systemCmd, systemCmd,
switchUserCmd, switchUserCmd,
}...) }
} }
-1
View File
@@ -40,7 +40,6 @@ var resolveIncludeCmd = &cobra.Command{
"layout.kdl", "layout.kdl",
"outputs.kdl", "outputs.kdl",
"binds.kdl", "binds.kdl",
"input.kdl",
"cursor.conf", "cursor.conf",
"layout.conf", "layout.conf",
"outputs.conf", "outputs.conf",
+6 -8
View File
@@ -401,12 +401,12 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
} }
func getDMSShellVersion() (version, path string) { func getDMSShellVersion() (version, path string) {
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" { if err := findConfig(nil, nil); err == nil && configPath != "" {
versionFile := filepath.Join(shellApp.ConfigPath(), "VERSION") versionFile := filepath.Join(configPath, "VERSION")
if data, err := os.ReadFile(versionFile); err == nil { 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 { if dmsPath, err := config.LocateDMSConfig(); err == nil {
@@ -450,8 +450,8 @@ func checkDMSInstallation() []checkResult {
var results []checkResult var results []checkResult
dmsPath := "" dmsPath := ""
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" { if err := findConfig(nil, nil); err == nil && configPath != "" {
dmsPath = shellApp.ConfigPath() dmsPath = configPath
} else if path, err := config.LocateDMSConfig(); err == nil { } else if path, err := config.LocateDMSConfig(); err == nil {
dmsPath = path dmsPath = path
} }
@@ -833,8 +833,6 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
return "systemd-networkd" return "systemd-networkd"
case network.BackendConnMan: case network.BackendConnMan:
return "ConnMan" return "ConnMan"
case network.BackendWpaSupplicant:
return "wpa_supplicant"
default: default:
return "" return ""
} }
+2 -2
View File
@@ -26,7 +26,7 @@ var updateCmd = &cobra.Command{
Use: "update", Use: "update",
Short: "Update DankMaterialShell to the latest version", Short: "Update DankMaterialShell to the latest version",
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution", 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) { Run: func(cmd *cobra.Command, args []string) {
runUpdate() runUpdate()
}, },
@@ -98,7 +98,7 @@ func runUpdate() {
} }
log.Info("Update complete! Restarting DMS...") log.Info("Update complete! Restarting DMS...")
shellApp.Restart() restartShell()
} }
func updateArchLinux() error { func updateArchLinux() error {
File diff suppressed because it is too large Load Diff
+150
View File
@@ -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)
}
}
+60 -1
View File
@@ -1,9 +1,19 @@
package main package main
import ( import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var customConfigPath string
var configPath string
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "dms", Use: "dms",
Short: "dms CLI", Short: "dms CLI",
@@ -11,5 +21,54 @@ var rootCmd = &cobra.Command{
} }
func init() { 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
} }
+3 -3
View File
@@ -231,16 +231,16 @@ func setPopoutScreenshotMode(begin bool) {
fn = "begin" fn = "begin"
} }
cmdArgs := []string{"ipc"} cmdArgs := []string{"ipc"}
if pid, ok := shellApp.SessionPID(); ok { if pid, ok := getFirstDMSPID(); ok {
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid)) cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
} else { } else {
if err := shellApp.ResolveConfig(nil, nil); err != nil { if err := findConfig(nil, nil); err != nil {
return return
} }
if qsHasAnyDisplay() { if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display") cmdArgs = append(cmdArgs, "--any-display")
} }
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath()) cmdArgs = append(cmdArgs, "-p", configPath)
} }
cmdArgs = append(cmdArgs, "call", "screenshot", fn) cmdArgs = append(cmdArgs, "call", "screenshot", fn)
_ = exec.Command("qs", cmdArgs...).Run() _ = exec.Command("qs", cmdArgs...).Run()
+6 -23
View File
@@ -11,6 +11,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "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/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
@@ -143,10 +144,6 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
niriFile: "alttab.kdl", niriFile: "alttab.kdl",
niriContent: func(_ string) string { return config.NiriAlttabConfig }, niriContent: func(_ string) string { return config.NiriAlttabConfig },
}, },
"input": {
niriFile: "input.kdl",
niriContent: func(_ string) string { return config.NiriInputConfig },
},
"outputs": { "outputs": {
niriFile: "outputs.kdl", niriFile: "outputs.kdl",
hyprFile: "outputs.lua", hyprFile: "outputs.lua",
@@ -208,12 +205,7 @@ func detectTerminal() (string, error) {
} }
func detectCompositorForSetup() (string, error) { func detectCompositorForSetup() (string, error) {
var compositors []string compositors := greeter.DetectCompositors()
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
if utils.CommandExists(candidate) {
compositors = append(compositors, candidate)
}
}
switch len(compositors) { switch len(compositors) {
case 0: case 0:
@@ -222,20 +214,11 @@ func detectCompositorForSetup() (string, error) {
return strings.ToLower(compositors[0]), nil return strings.ToLower(compositors[0]), nil
} }
fmt.Println("Multiple compositors detected:") selected, err := greeter.PromptCompositorChoice(compositors)
for i, compositor := range compositors { if err != nil {
fmt.Printf("%d) %s\n", i+1, compositor) return "", err
} }
fmt.Printf("\nChoice (1-%d): ", len(compositors)) return strings.ToLower(selected), nil
var response string
fmt.Scanln(&response)
choice := 0
fmt.Sscanf(strings.TrimSpace(response), "%d", &choice)
if choice < 1 || choice > len(compositors) {
return "", fmt.Errorf("invalid choice")
}
return strings.ToLower(compositors[choice-1]), nil
} }
func runSetupDmsConfig(name string) error { func runSetupDmsConfig(name string) error {
+137
View File
@@ -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"
}
+8
View File
@@ -12,6 +12,14 @@ import (
var Version = "dev" var Version = "dev"
func init() { 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) authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
+8
View File
@@ -12,6 +12,14 @@ import (
var Version = "dev" var Version = "dev"
func init() { 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) authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
+5 -2
View File
@@ -101,8 +101,11 @@ func getServerSocketPath() string {
runtimeDir = os.TempDir() runtimeDir = os.TempDir()
} }
if sessionSock, ok := shellApp.SessionSocketPath(); ok { if parentPID, ok := sessionParentPID(os.Getenv("WAYLAND_DISPLAY")); ok {
return sessionSock sessionSock := filepath.Join(runtimeDir, fmt.Sprintf("danklinux-%d.sock", parentPID))
if _, err := os.Stat(sessionSock); err == nil {
return sessionSock
}
} }
entries, err := os.ReadDir(runtimeDir) entries, err := os.ReadDir(runtimeDir)
+696 -6
View File
@@ -4,20 +4,591 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"path/filepath" "path/filepath"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
) )
type ipcTargets map[string]map[string][]string 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 { var qsHasAnyDisplay = sync.OnceValue(func() bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() defer cancel()
@@ -61,17 +632,17 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
func buildQsIPCBaseArgs() ([]string, error) { func buildQsIPCBaseArgs() ([]string, error) {
cmdArgs := []string{"ipc"} cmdArgs := []string{"ipc"}
switch pid, ok := shellApp.SessionPID(); { switch pid, ok := getSessionDMSPID(); {
case ok: case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid)) cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default: default:
if err := shellApp.ResolveConfig(nil, nil); err != nil { if err := findConfig(nil, nil); err != nil {
return nil, err return nil, err
} }
if qsHasAnyDisplay() { if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display") cmdArgs = append(cmdArgs, "--any-display")
} }
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath()) cmdArgs = append(cmdArgs, "-p", configPath)
} }
return cmdArgs, nil return cmdArgs, nil
} }
@@ -128,6 +699,101 @@ func getShellIPCCompletions(args []string, _ string) []string {
return nil return nil
} }
func getFirstDMSPID() (int, bool) {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
continue
}
proc, err := os.FindProcess(pid)
if err != nil {
continue
}
if proc.Signal(syscall.Signal(0)) != nil {
continue
}
return pid, true
}
return 0, false
}
func 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) { func runShellIPCCommand(args []string) {
if len(args) == 0 { if len(args) == 0 {
printIPCHelp() 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. // logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
func logStartupFailure(exitCode int, uptime time.Duration, stderrTail string) { func logStartupFailure(startTime time.Time, exitCode int, tracker *stderrTracker) {
if uptime >= 5*time.Second || exitCode == 0 || exitCode > 128 { if time.Since(startTime) >= 5*time.Second || exitCode == 0 || exitCode > 128 {
return 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.") log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
} else { } else {
log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode) log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
-82
View File
@@ -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
}
+61
View File
@@ -1,7 +1,9 @@
package main package main
import ( import (
"fmt"
"os/exec" "os/exec"
"slices"
"strings" "strings"
) )
@@ -26,3 +28,62 @@ func isArchPackageInstalled(packageName string) bool {
err := cmd.Run() err := cmd.Run()
return err == nil return err == nil
} }
type systemdServiceState struct {
Name string
EnabledState string
NeedsDisable bool
Exists bool
}
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
cmd := exec.Command("systemctl", "is-enabled", serviceName)
output, err := cmd.Output()
stateStr := strings.TrimSpace(string(output))
if err != nil {
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
isKnownState := slices.Contains(knownStates, stateStr)
if !isKnownState {
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
}
}
shouldDisable := false
switch stateStr {
case "enabled", "enabled-runtime", "static", "indirect", "alias":
shouldDisable = true
case "disabled", "masked", "masked-runtime", "not-found":
shouldDisable = false
default:
shouldDisable = true
}
return stateStr, shouldDisable, nil
}
func getSystemdServiceState(serviceName string) (*systemdServiceState, error) {
state := &systemdServiceState{
Name: serviceName,
Exists: false,
}
enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName)
if err != nil {
return nil, fmt.Errorf("failed to check enabled state: %w", err)
}
state.EnabledState = enabledState
state.NeedsDisable = needsDisable
if enabledState == "not-found" {
state.Exists = false
return state, nil
}
state.Exists = true
return state, nil
}
+27 -27
View File
@@ -1,13 +1,14 @@
module github.com/AvengeMedia/DankMaterialShell/core module github.com/AvengeMedia/DankMaterialShell/core
go 1.26.4 go 1.26.1
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 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/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0
github.com/fsnotify/fsnotify v1.10.1 github.com/fsnotify/fsnotify v1.10.1
github.com/godbus/dbus/v5 v5.2.2 github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847 github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847
@@ -17,12 +18,12 @@ require (
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yeqown/go-qrcode/v2 v2.2.5 github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.3.0 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 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 go4.org/mem v0.0.0-20240501181205-ae6ca9944745
golang.org/x/image v0.44.0 golang.org/x/image v0.39.0
tailscale.com v1.100.0 tailscale.com v1.96.5
) )
require ( require (
@@ -30,28 +31,28 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/akutz/memconn v0.1.0 // 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/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.4 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/coder/websocket v1.8.15 // indirect github.com/coder/websocket v1.8.14 // indirect
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2/v2 v2.5.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/emirpasic/gods v1.18.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect github.com/fogleman/gg v1.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 // indirect github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 // indirect
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mdlayher/netlink v1.11.2 // indirect github.com/mdlayher/netlink v1.11.1 // indirect
github.com/mdlayher/socket v0.6.1 // indirect github.com/mdlayher/socket v0.6.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
@@ -61,15 +62,14 @@ require (
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.54.0 // indirect golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.57.0 // indirect golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.22.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
) )
require ( require (
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // 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/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/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/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 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-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/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.47.0 golang.org/x/sys v0.43.0
golang.org/x/text v0.40.0 golang.org/x/text v0.36.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
+56 -56
View File
@@ -1,7 +1,5 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b h1:UwX1H4BkzazL7ips9ljnHzBXGttYARcIi5Njj9aqIt4=
github.com/AvengeMedia/dankgo v0.0.0-20260730184236-239485829b0b/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= 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 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 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.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs= github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= 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.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 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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/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 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= 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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= 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/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 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= 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.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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c= 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.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2/v2 v2.5.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= 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 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -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/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 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= 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-20260504142752-cb8e9d337266 h1:wH21vHuv323v9x78JNFNJ6P7HEAsdwr9yq2k9/o4zEE=
github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E= 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-alpha.1 h1:gmqi2jvsreu0s8JMLylYDFq4sbjHwwlhktMw0DUg3mA= 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-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrOXjo/0EnvRZx6D++I86UYjPgAQ= 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.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ= 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.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc= 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-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg= github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 h1:2WmHkJINIjgXXYDGik8d3oJvFA3DAwPy00csDJ3vo+o=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= 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 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= 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= 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/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 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= 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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/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 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= 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 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 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.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= github.com/mdlayher/netlink v1.11.1 h1:T136gDS6Gkt+hLncaBwKdW5GpEC8Z0ykqimOebVoal0=
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= github.com/mdlayher/netlink v1.11.1/go.mod h1:ao4LjamyK4Uq9L8+fQzqFYpAncbeCdwbvd9Edv/pYnc=
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A= github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= 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 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 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/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 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= 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-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
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/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 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/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 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 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 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 h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 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.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= 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 h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= 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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= 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 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 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= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE= tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
-1
View File
@@ -264,7 +264,6 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
{"layout.kdl", NiriLayoutConfig}, {"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig}, {"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)}, {"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"input.kdl", NiriInputConfig},
{"outputs.kdl", ""}, {"outputs.kdl", ""},
{"cursor.kdl", ""}, {"cursor.kdl", ""},
{"windowrules.kdl", ""}, {"windowrules.kdl", ""},
@@ -0,0 +1,17 @@
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
@@ -1,15 +0,0 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
input {
mouse {
accel-speed 0.0
}
touchpad {
tap
accel-speed 0.0
natural-scroll
}
}
+25 -2
View File
@@ -34,6 +34,31 @@ input {
numlock numlock
} }
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint { trackpoint {
// off // off
// natural-scroll // natural-scroll
@@ -52,7 +77,6 @@ input {
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen. // Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%" // focus-follows-mouse max-scroll-amount="0%"
} }
// You can configure outputs by their name, which you can find // You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance. // by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1". // The built-in laptop monitor is usually called "eDP-1".
@@ -247,4 +271,3 @@ include optional=true "dms/alttab.kdl"
include optional=true "dms/binds.kdl" include optional=true "dms/binds.kdl"
include optional=true "dms/outputs.kdl" include optional=true "dms/outputs.kdl"
include optional=true "dms/cursor.kdl" include optional=true "dms/cursor.kdl"
include optional=true "dms/input.kdl"
+2 -2
View File
@@ -17,5 +17,5 @@ var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl //go:embed embedded/niri-binds.kdl
var NiriBindsConfig string var NiriBindsConfig string
//go:embed embedded/niri-input.kdl //go:embed embedded/niri-greeter.kdl
var NiriInputConfig string var NiriGreeterConfig string
+15 -13
View File
@@ -1,15 +1,9 @@
package errdefs package errdefs
import ( type ErrorType int
dankerrdefs "github.com/AvengeMedia/dankgo/errdefs"
)
type ErrorType = dankerrdefs.ErrorType
type CustomError = dankerrdefs.CustomError
const ( const (
ErrTypeNotLinux ErrorType = dankerrdefs.AppErrorBase + iota ErrTypeNotLinux ErrorType = iota
ErrTypeInvalidArchitecture ErrTypeInvalidArchitecture
ErrTypeUnsupportedDistribution ErrTypeUnsupportedDistribution
ErrTypeUnsupportedVersion ErrTypeUnsupportedVersion
@@ -28,8 +22,20 @@ const (
ErrTypeGeneric ErrTypeGeneric
) )
type CustomError struct {
Type ErrorType
Message string
}
func (e *CustomError) Error() string {
return e.Message
}
func NewCustomError(errType ErrorType, message string) error { func NewCustomError(errType ErrorType, message string) error {
return dankerrdefs.NewCustomError(errType, message) return &CustomError{
Type: errType,
Message: message,
}
} }
const ( const (
@@ -41,10 +47,6 @@ const (
ErrWifiDisabled = "wifi-disabled" ErrWifiDisabled = "wifi-disabled"
ErrAlreadyConnected = "already-connected" ErrAlreadyConnected = "already-connected"
ErrConnectionFailed = "connection-failed" ErrConnectionFailed = "connection-failed"
ErrHotspotIPConfigFailed = "hotspot-ip-config-failed"
ErrHotspotSupplicantFailed = "hotspot-supplicant-failed"
ErrHotspotFailed = "hotspot-failed"
) )
var ( var (
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/dbusutil" "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "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
+350
View File
@@ -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))
}
}
+200
View File
@@ -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")
}
}
+548
View File
@@ -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"])
}
}
+6 -2
View File
@@ -9,8 +9,8 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "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/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
// ErrConfirmationRequired is returned when --yes is not set and the user // 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) // 9. Greeter setup (if dms-greeter was included)
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") { if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
compositorName := "niri"
if wm == deps.WindowManagerHyprland {
compositorName = "Hyprland"
}
fmt.Fprintln(os.Stdout, "Configuring DMS greeter...") fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
logFunc := func(line string) { logFunc := func(line string) {
r.log(line) r.log(line)
fmt.Fprintf(os.Stdout, " greeter: %s\n", 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 // Non-fatal, matching TUI behavior
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err) fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
} }
+5 -45
View File
@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -225,44 +224,6 @@ func (h *HyprlandProvider) validateAction(action string) error {
return nil return nil
} }
var luaExprActionPattern = regexp.MustCompile(`^(function\s*\(|hl\.)`)
// isRawLuaActionText reports that action is a Lua expression to re-emit
// verbatim rather than freeform dispatcher text to wrap for hyprctl. The
// balance check keeps malformed input from corrupting the generated file.
func isRawLuaActionText(action string) bool {
if !luaExprActionPattern.MatchString(action) {
return false
}
depth := 0
var quote byte
escaped := false
for i := 0; i < len(action); i++ {
c := action[i]
switch {
case escaped:
escaped = false
case quote != 0:
switch c {
case '\\':
escaped = true
case quote:
quote = 0
}
case c == '"' || c == '\'':
quote = c
case c == '(':
depth++
case c == ')':
depth--
if depth < 0 {
return false
}
}
}
return depth == 0 && quote == 0 && !escaped
}
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error { func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
if err := h.ensureWritableConfig(); err != nil { if err := h.ensureWritableConfig(); err != nil {
return err return err
@@ -293,12 +254,11 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
canonicalKey := canonicalHyprlandOverrideKey(key) canonicalKey := canonicalHyprlandOverrideKey(key)
normalizedKey := hyprlandOverrideMapKey(canonicalKey) normalizedKey := hyprlandOverrideMapKey(canonicalKey)
existingBinds[normalizedKey] = &hyprlandOverrideBind{ existingBinds[normalizedKey] = &hyprlandOverrideBind{
Key: canonicalKey, Key: canonicalKey,
Action: action, Action: action,
Description: description, Description: description,
Flags: flags, Flags: flags,
Options: options, Options: options,
RawLuaAction: isRawLuaActionText(action),
} }
return h.writeOverrideBinds(existingBinds) return h.writeOverrideBinds(existingBinds)
@@ -463,56 +463,6 @@ func TestHyprlandSetBindLeavesConfOnlyInstallReadOnly(t *testing.T) {
} }
} }
func TestIsRawLuaActionText(t *testing.T) {
cases := []struct {
action string
want bool
}{
{`function() hl.plugin.scrolloverview.overview("toggle") end`, true},
{`hl.dsp.exec_cmd("foo")`, true},
{`hl.dsp.no_op()`, true},
{"workspace 3", false},
{"exec zeditor", false},
{`function() hl.foo( end`, false},
{`function() hl.foo("a) end`, false},
{`hl.foo()) hl.bar((`, false},
}
for _, tc := range cases {
if got := isRawLuaActionText(tc.action); got != tc.want {
t.Errorf("isRawLuaActionText(%q) = %v, want %v", tc.action, got, tc.want)
}
}
}
func TestHyprlandSetBindPreservesRawLuaAction(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte("-- DMS user keybind overrides\n"), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandProvider(tmpDir)
rawAction := `function() hl.plugin.scrolloverview.overview("toggle") end`
if err := provider.SetBind("SUPER + G", rawAction, "Toggle overview", nil); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
if err != nil {
t.Fatal(err)
}
got := string(data)
if !strings.Contains(got, `hl.bind("SUPER + G", `+rawAction) {
t.Fatalf("expected raw Lua action to be written verbatim, got:\n%s", got)
}
if strings.Contains(got, "hyprctl dispatch function") {
t.Fatalf("expected raw Lua action to not be wrapped in hyprctl dispatch, got:\n%s", got)
}
}
func TestHyprlandSetBindUpdatesSpacedLuaOverrideWithoutDuplicates(t *testing.T) { func TestHyprlandSetBindUpdatesSpacedLuaOverrideWithoutDuplicates(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms") dmsDir := filepath.Join(tmpDir, "dms")
+207 -19
View File
@@ -1,32 +1,220 @@
package log package log
import ( 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() { // Printf routes goose/info-style logs through Infof.
danklog.SetEnvPrefix("DMS") func (l *Logger) Printf(format string, v ...any) { l.Infof(format, v...) }
// Fatalf keeps gooses contract of exiting the program.
func (l *Logger) Fatalf(format string, v ...any) { l.Logger.Fatalf(format, v...) }
var (
logger *Logger
initLogger sync.Once
logMu sync.Mutex
logFile *os.File
logStderr io.Writer = os.Stderr
ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
)
// ansiStripWriter strips ANSI escape sequences before forwarding to w. Used
// for the file sink so colored stderr stays colored while the file stays plain.
type ansiStripWriter struct{ w io.Writer }
func (a *ansiStripWriter) Write(p []byte) (int, error) {
stripped := ansiRe.ReplaceAll(p, nil)
if _, err := a.w.Write(stripped); err != nil {
return 0, err
}
return len(p), nil
} }
func 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...) } base := cblog.New(logStderr)
func Debugf(format string, v ...any) { danklog.Debugf(format, v...) } base.SetStyles(styles)
func Info(msg any, keyvals ...any) { danklog.Info(msg, keyvals...) } base.SetReportTimestamp(false)
func Infof(format string, v ...any) { danklog.Infof(format, v...) }
func Warn(msg any, keyvals ...any) { danklog.Warn(msg, keyvals...) } level := cblog.InfoLevel
func Warnf(format string, v ...any) { danklog.Warnf(format, v...) } if envLevel := os.Getenv("DMS_LOG_LEVEL"); envLevel != "" {
func Error(msg any, keyvals ...any) { danklog.Error(msg, keyvals...) } level = parseLogLevel(envLevel)
func Errorf(format string, v ...any) { danklog.Errorf(format, v...) } }
func Fatal(msg any, keyvals ...any) { danklog.Fatal(msg, keyvals...) } base.SetLevel(level)
func Fatalf(format string, v ...any) { danklog.Fatalf(format, v...) } 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...) }
+55 -123
View File
@@ -44,7 +44,6 @@ type TemplateDef struct {
ID string ID string
Commands []string Commands []string
Flatpaks []string Flatpaks []string
ConfigDirs []string
ConfigFile string ConfigFile string
Kind TemplateKind Kind TemplateKind
RunUnconditionally bool RunUnconditionally bool
@@ -61,9 +60,9 @@ var templateRegistry = []TemplateDef{
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"}, {ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"}, {ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"}, {ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigDirs: []string{"vesktop"}, ConfigFile: "vesktop.toml"}, {ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigDirs: []string{"Vencord"}, ConfigFile: "vencord.toml"}, {ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigFile: "vencord.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigDirs: []string{"equibop"}, ConfigFile: "equibop.toml"}, {ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal}, {ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal}, {ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},
{ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal}, {ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal},
@@ -386,12 +385,8 @@ func buildOnce(opts *Options) (bool, error) {
default: default:
syncAccentColor(primaryDark) syncAccentColor(primaryDark)
} }
refreshGTKTheme(opts.Mode) refreshGTK(opts.Mode)
refreshGTKColorScheme() refreshGTK4()
}
if isDMSKDEColorSchemeActive(opts.ConfigDir) {
applyKDEColorScheme(opts.Mode)
} }
if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) { if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) {
@@ -464,22 +459,25 @@ output_path = '%s'
case TemplateKindGTK: case TemplateKindGTK:
switch opts.Mode { switch opts.Mode {
case ColorModeLight: case ColorModeLight:
appendConfig(opts, cfgFile, nil, nil, nil, "gtk3-light.toml") appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml")
default: default:
appendConfig(opts, cfgFile, nil, nil, nil, "gtk3-dark.toml") appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml")
} }
case TemplateKindTerminal: case TemplateKindTerminal:
appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
case TemplateKindVSCode: case TemplateKindVSCode:
for _, editor := range vscodeEditors { appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, editor.name, editor.extensionsDir(homeDir), opts.ShellDir) appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions"), opts.ShellDir)
} appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs: case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" { if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile) appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
} }
default: default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile) appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
} }
} }
@@ -514,14 +512,13 @@ func appendConfig(
cfgFile *os.File, cfgFile *os.File,
checkCmd []string, checkCmd []string,
checkFlatpaks []string, checkFlatpaks []string,
checkConfigDirs []string,
fileName string, fileName string,
) { ) {
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName) configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
return return
} }
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) && !configDirExists(checkConfigDirs) { if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
return return
} }
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
@@ -602,20 +599,6 @@ func templateSessionActive(tmpl TemplateDef) bool {
return err == nil return err == nil
} }
func configDirExists(names []string) bool {
configHome := utils.XDGConfigHome()
if configHome == "" {
return false
}
for _, name := range names {
info, err := os.Stat(filepath.Join(configHome, name))
if err == nil && info.IsDir() {
return true
}
}
return false
}
func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool { func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool {
// Both nil is treated as "skip check" / unconditionally run // Both nil is treated as "skip check" / unconditionally run
if checkCmd == nil && checkFlatpaks == nil { if checkCmd == nil && checkFlatpaks == nil {
@@ -630,23 +613,6 @@ func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []stri
return false return false
} }
type vscodeEditor struct {
name string
dataDir string
}
var vscodeEditors = []vscodeEditor{
{"vscode", ".vscode"},
{"codium", ".vscode-oss"},
{"cursor", ".cursor"},
{"windsurf", ".windsurf"},
{"vscode-insiders", ".vscode-insiders"},
}
func (e vscodeEditor) extensionsDir(homeDir string) string {
return filepath.Join(homeDir, e.dataDir, "extensions")
}
func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) { func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) {
pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*") pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*")
matches, err := filepath.Glob(pattern) matches, err := filepath.Glob(pattern)
@@ -925,7 +891,7 @@ func generateDank16Variants(primaryDark, primaryLight, surface string, mode Colo
} }
func isDMSGTKActive(configDir string) bool { func isDMSGTKActive(configDir string) bool {
gtkCSS := filepath.Join(configDir, "gtk-4.0", "gtk.css") gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
info, err := os.Lstat(gtkCSS) info, err := os.Lstat(gtkCSS)
if err != nil { if err != nil {
@@ -941,73 +907,11 @@ func isDMSGTKActive(configDir string) bool {
return err == nil && strings.Contains(string(data), "dank-colors.css") return err == nil && strings.Contains(string(data), "dank-colors.css")
} }
// isDMSKDEColorSchemeActive only flips the scheme when the user is already on a func refreshGTK(mode ColorMode) {
// 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 gtkThemeInstalled(theme string) bool {
home, _ := os.UserHomeDir()
candidates := []string{
filepath.Join(home, ".local/share/themes", theme),
filepath.Join(home, ".themes", theme),
filepath.Join("/usr/share/themes", theme),
filepath.Join("/usr/local/share/themes", theme),
}
for _, dir := range candidates {
if info, err := os.Stat(dir); err == nil && info.IsDir() {
return true
}
}
return false
}
func refreshGTKTheme(mode ColorMode) {
theme := mode.GTKTheme()
if !gtkThemeInstalled(theme) {
log.Infof("Skipping gtk-theme refresh: %s is not installed", theme)
return
}
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
log.Warnf("Failed to reset gtk-theme: %v", err) log.Warnf("Failed to reset gtk-theme: %v", err)
} }
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", theme); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil {
log.Warnf("Failed to set gtk-theme: %v", err) log.Warnf("Failed to set gtk-theme: %v", err)
} }
} }
@@ -1028,8 +932,8 @@ func expectColorSchemeEcho(scheme string) {
// apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps // apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps
// following the portal color-scheme (Chromium) can drop the restore signal // following the portal color-scheme (Chromium) can drop the restore signal
// mid-repaint and latch the wrong mode, so this is opt-in. // mid-repaint and latch the wrong mode, so this is opt-in.
func refreshGTKColorScheme() { func refreshGTK4() {
if os.Getenv("DMS_ENABLE_GTK_REFRESH") != "1" { if os.Getenv("DMS_ENABLE_GTK4_REFRESH") != "1" {
return return
} }
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme") output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
@@ -1047,13 +951,13 @@ func refreshGTKColorScheme() {
expectColorSchemeEcho(toggle) expectColorSchemeEcho(toggle)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK refresh: %v", err) log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
return return
} }
time.Sleep(50 * time.Millisecond) time.Sleep(400 * time.Millisecond)
expectColorSchemeEcho(current) expectColorSchemeEcho(current)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK refresh: %v", err) log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
} }
} }
@@ -1076,6 +980,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) { func syncColorScheme(mode ColorMode) {
scheme := "prefer-dark" scheme := "prefer-dark"
if mode == ColorModeLight { if mode == ColorModeLight {
@@ -1172,7 +1096,7 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
case tmpl.Kind == TemplateKindEmacs: case tmpl.Kind == TemplateKindEmacs:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != "" detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
default: default:
detected = (appExists(checker, tmpl.Commands, tmpl.Flatpaks) || configDirExists(tmpl.ConfigDirs)) && templateSessionActive(tmpl) detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && templateSessionActive(tmpl)
} }
checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected}) checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected})
@@ -1182,8 +1106,16 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
} }
func checkVSCodeExtension(homeDir string) bool { func checkVSCodeExtension(homeDir string) bool {
for _, editor := range vscodeEditors { extDirs := []string{
pattern := filepath.Join(editor.extensionsDir(homeDir), "danklinux.dms-theme-*") filepath.Join(homeDir, ".vscode/extensions"),
filepath.Join(homeDir, ".vscode-oss/extensions"),
filepath.Join(homeDir, ".config/Code - OSS/extensions"),
filepath.Join(homeDir, ".cursor/extensions"),
filepath.Join(homeDir, ".windsurf/extensions"),
}
for _, extDir := range extDirs {
pattern := filepath.Join(extDir, "danklinux.dms-theme-*")
if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 { if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 {
return true return true
} }
+8 -95
View File
@@ -38,7 +38,7 @@ func TestAppendConfigBinaryExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"sh"}, nil, nil, "test.toml") appendConfig(opts, cfgFile, []string{"sh"}, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -82,7 +82,7 @@ func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, nil, "test.toml") appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -122,7 +122,7 @@ func TestAppendConfigFlatpakExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, nil, "test.toml") appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -163,7 +163,7 @@ func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, nil, "test.toml") appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -203,7 +203,7 @@ func TestAppendConfigBothExist(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, nil, "test.toml") appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -244,7 +244,7 @@ func TestAppendConfigNeitherExists(t *testing.T) {
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, nil, "test.toml") appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -281,7 +281,7 @@ func TestAppendConfigNoChecks(t *testing.T) {
opts := &Options{ShellDir: shellDir} opts := &Options{ShellDir: shellDir}
appendConfig(opts, cfgFile, nil, nil, nil, "test.toml") appendConfig(opts, cfgFile, nil, nil, "test.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -312,7 +312,7 @@ func TestAppendConfigFileDoesNotExist(t *testing.T) {
opts := &Options{ShellDir: shellDir} opts := &Options{ShellDir: shellDir}
appendConfig(opts, cfgFile, nil, nil, nil, "nonexistent.toml") appendConfig(opts, cfgFile, nil, nil, "nonexistent.toml")
cfgFile.Close() cfgFile.Close()
output, err := os.ReadFile(outFile) output, err := os.ReadFile(outFile)
@@ -487,90 +487,3 @@ func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) {
} }
assert.NotContains(t, string(output), "[templates.dmsmango]") assert.NotContains(t, string(output), "[templates.dmsmango]")
} }
func TestAppendConfigConfigDirExists(t *testing.T) {
tempDir := t.TempDir()
shellDir := filepath.Join(tempDir, "shell")
configsDir := filepath.Join(shellDir, "matugen", "configs")
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
testConfig := "vencord config content"
if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte(testConfig), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
configHome := filepath.Join(tempDir, "config")
if err := os.MkdirAll(filepath.Join(configHome, "Vencord"), 0o755); err != nil {
t.Fatalf("failed to create Vencord config dir: %v", err)
}
t.Setenv("XDG_CONFIG_HOME", configHome)
outFile := filepath.Join(tempDir, "output.toml")
cfgFile, err := os.Create(outFile)
if err != nil {
t.Fatalf("failed to create output file: %v", err)
}
defer cfgFile.Close()
mockChecker := mocks_utils.NewMockAppChecker(t)
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
assert.Equal(t, testConfig+"\n", string(output))
}
func TestAppendConfigConfigDirDoesNotExist(t *testing.T) {
tempDir := t.TempDir()
shellDir := filepath.Join(tempDir, "shell")
configsDir := filepath.Join(shellDir, "matugen", "configs")
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte("vencord config content"), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
configHome := filepath.Join(tempDir, "config")
if err := os.MkdirAll(configHome, 0o755); err != nil {
t.Fatalf("failed to create config home: %v", err)
}
t.Setenv("XDG_CONFIG_HOME", configHome)
outFile := filepath.Join(tempDir, "output.toml")
cfgFile, err := os.Create(outFile)
if err != nil {
t.Fatalf("failed to create output file: %v", err)
}
defer cfgFile.Close()
mockChecker := mocks_utils.NewMockAppChecker(t)
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")
cfgFile.Close()
output, err := os.ReadFile(outFile)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
assert.Empty(t, string(output))
}
-16
View File
@@ -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()
}
-29
View File
@@ -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
View File
@@ -5,20 +5,30 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( 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)" LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH" LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)" LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH" LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
legacyGreeterPamU2FComment = "# DMS greeter U2F"
GreetdPamPath = "/etc/pam.d/greetd"
DankshellPamPath = "/etc/pam.d/dankshell" DankshellPamPath = "/etc/pam.d/dankshell"
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f" DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
) )
@@ -28,8 +38,7 @@ const (
// rest cover distros (or minimal installs) with no /etc/pam.d/login. // rest cover distros (or minimal installs) with no /etc/pam.d/login.
// lockscreenPamBaseDirs mirrors libpam's search order: /etc overrides, then the // lockscreenPamBaseDirs mirrors libpam's search order: /etc overrides, then the
// vendor dir (/usr/lib) and the stateless-distro default (/usr/share). // 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"}
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d", "/usr/local/etc/pam.d"}
// Standalone auth+account services, most universal first. login exists almost // Standalone auth+account services, most universal first. login exists almost
// everywhere (util-linux); system-* cover Fedora/Arch/Gentoo/SUSE-Leap. // 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 // Fallback for distros with no standalone login service, only shared building
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*), FreeBSD // blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*).
// (system holds both stanzas, included by login).
var lockscreenPamSharedIncludePairs = []struct { var lockscreenPamSharedIncludePairs = []struct {
auth string auth string
account string account string
}{ }{
{auth: "common-auth", account: "common-account"}, {auth: "common-auth", account: "common-account"},
{auth: "base-auth", account: "base-account"}, {auth: "base-auth", account: "base-account"},
{auth: "system", account: "system"},
} }
var includedPamAuthFiles = []string{ var includedPamAuthFiles = []string{
@@ -60,28 +67,34 @@ var includedPamAuthFiles = []string{
"system-local-login", "system-local-login",
"common-auth-pc", "common-auth-pc",
"login", "login",
"system",
} }
type AuthSettings struct { type AuthSettings struct {
EnableFprint bool `json:"enableFprint"` EnableFprint bool `json:"enableFprint"`
EnableU2f bool `json:"enableU2f"` EnableU2f bool `json:"enableU2f"`
GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"`
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
} }
type SyncAuthOptions struct { type SyncAuthOptions struct {
HomeDir string HomeDir string
ForceGreeterAuth bool
} }
type syncDeps struct { type syncDeps struct {
pamDir string pamDir string
dankshellPath string greetdPath string
dankshellU2fPath string dankshellPath string
isNixOS func() bool dankshellU2fPath string
readFile func(string) ([]byte, error) isNixOS func() bool
stat func(string) (os.FileInfo, error) readFile func(string) ([]byte, error)
createTemp func(string, string) (*os.File, error) stat func(string) (os.FileInfo, error)
removeFile func(string) error createTemp func(string, string) (*os.File, error)
runSudoCmd func(string, string, ...string) error removeFile func(string) error
runSudoCmd func(string, string, ...string) error
pamModuleExists func(string) bool
fingerprintAvailableForCurrentUser func() bool
} }
type lockscreenPamIncludeDirective struct { type lockscreenPamIncludeDirective struct {
@@ -137,6 +150,7 @@ func (r lockscreenPamResolver) locate(target string) (string, error) {
func defaultSyncDeps() syncDeps { func defaultSyncDeps() syncDeps {
return syncDeps{ return syncDeps{
pamDir: "/etc/pam.d", pamDir: "/etc/pam.d",
greetdPath: GreetdPamPath,
dankshellPath: DankshellPamPath, dankshellPath: DankshellPamPath,
dankshellU2fPath: DankshellU2FPamPath, dankshellU2fPath: DankshellU2FPamPath,
isNixOS: IsNixOS, isNixOS: IsNixOS,
@@ -147,6 +161,8 @@ func defaultSyncDeps() syncDeps {
runSudoCmd: func(password, command string, args ...string) error { runSudoCmd: func(password, command string, args ...string) error {
return privesc.Run(context.Background(), password, append([]string{command}, args...)...) 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 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 { func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps()) 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 { func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
homeDir := strings.TrimSpace(options.HomeDir) homeDir := strings.TrimSpace(options.HomeDir)
if homeDir == "" { if homeDir == "" {
@@ -201,9 +229,99 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S
return err 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 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 { func PamTextIncludesFile(pamText, filename string) bool {
lines := strings.Split(pamText, "\n") lines := strings.Split(pamText, "\n")
for _, line := range lines { for _, line := range lines {
@@ -912,6 +1030,186 @@ func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword strin
return nil 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 { func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
tmpFile, err := deps.createTemp("", "dms-pam-*.conf") tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
if err != nil { if err != nil {
@@ -938,6 +1236,26 @@ func writeManagedPamFile(content string, destPath string, sudoPassword string, d
return nil 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 { func pamModuleExists(module string) bool {
for _, libDir := range []string{ for _, libDir := range []string{
"/usr/lib64/security", "/usr/lib64/security",
@@ -950,9 +1268,6 @@ func pamModuleExists(module string) bool {
"/usr/lib/aarch64-linux-gnu/security", "/usr/lib/aarch64-linux-gnu/security",
"/run/current-system/sw/lib64/security", "/run/current-system/sw/lib64/security",
"/run/current-system/sw/lib/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 { if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
return true return true
@@ -960,3 +1275,60 @@ func pamModuleExists(module string) bool {
} }
return false 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
View File
@@ -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) { func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
t.Helper() t.Helper()
writeTestFile(t, filepath.Join(e.pamDir, name), content) 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) 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 { func readFileString(t *testing.T, path string) string {
t.Helper() t.Helper()
data, err := os.ReadFile(path) 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) { func TestListLockscreenPamServices(t *testing.T) {
t.Parallel() t.Parallel()
@@ -900,98 +1081,10 @@ func containsSubstr(items []string, substr string) bool {
return false 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) { func TestSyncAuthConfigWithDeps(t *testing.T) {
t.Parallel() 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() t.Parallel()
env := newPamTestEnv(t) 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, "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, "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 { if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err) t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
} }
@@ -1010,24 +1106,105 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() { if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
t.Fatalf("unexpected dankshell-u2f content:\n%s", got) 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() t.Parallel()
env := newPamTestEnv(t) 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, "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, "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)) err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil { if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err) 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) { 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" "unsafe"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" "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) { 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)
}
}
}
@@ -22,16 +22,6 @@ func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") { if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") {
t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control") t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control")
} }
// Wayland IMEs commit unconsumed printable keys as text-input text rather
// than forwarding raw keys, so the lock screen needs an IME commit sink
// alongside raw key handling.
if !strings.Contains(content, "id: imeCommitSink") {
t.Fatalf("passwordField must keep the imeCommitSink TextInput so IME-routed keyboards can type (#2950)")
}
if !strings.Contains(content, "Qt.ImhSensitiveData") {
t.Fatalf("imeCommitSink must advertise hidden-text hints so IMEs treat it as a password field")
}
} }
func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) { func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) {
+4 -2
View File
@@ -1,12 +1,14 @@
package apppicker package apppicker
import ( import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/desktop" "github.com/AvengeMedia/DankMaterialShell/core/internal/desktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
) )
func HandleRequest(conn *models.Conn, req models.Request, manager *Manager) { func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method { switch req.Method {
case "apppicker.open", "browser.open": case "apppicker.open", "browser.open":
handleOpen(conn, req, manager) 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) log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
target, ok := models.Get[string](req, "target") target, ok := models.Get[string](req, "target")
+1 -1
View File
@@ -3,7 +3,7 @@ package apppicker
import ( import (
"sync" "sync"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type Manager struct { type Manager struct {
+19 -17
View File
@@ -1,10 +1,12 @@
package bluez package bluez
import ( import (
"encoding/json"
"fmt" "fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "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 { type BluetoothEvent struct {
@@ -12,7 +14,7 @@ type BluetoothEvent struct {
Data BluetoothState `json:"data"` 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 { switch req.Method {
case "bluetooth.getState": case "bluetooth.getState":
handleGetState(conn, req, manager) 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()) 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 { if err := manager.StartDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return 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"}) 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 { if err := manager.StopDiscovery(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return 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"}) 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") powered, err := params.Bool(req.Params, "powered")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") devicePath, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") devicePath, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") devicePath, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") devicePath, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") devicePath, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") devicePath, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") token, err := params.String(req.Params, "token")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") token, err := params.String(req.Params, "token")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
@@ -214,7 +216,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Data: initialState, Data: initialState,
} }
if err := conn.WriteResponse(models.Response[BluetoothEvent]{ if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
ID: req.ID, ID: req.ID,
Result: &event, Result: &event,
}); err != nil { }); err != nil {
@@ -226,7 +228,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Type: "state_changed", Type: "state_changed",
Data: state, Data: state,
} }
if err := conn.WriteResponse(models.Response[BluetoothEvent]{ if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
Result: &event, Result: &event,
}); err != nil { }); err != nil {
return return
+7 -12
View File
@@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/dbusutil" "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -43,10 +43,10 @@ func NewManager() (*Manager, error) {
broker := NewSubscriptionBroker(m.broadcastPairingPrompt) broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
m.promptBroker = broker m.promptBroker = broker
adapter, err := findAdapter(conn) adapter, err := m.findAdapter()
if err != nil { if err != nil {
conn.Close() conn.Close()
return nil, err return nil, fmt.Errorf("no bluetooth adapter found: %w", err)
} }
m.adapterPath = adapter m.adapterPath = adapter
@@ -74,12 +74,12 @@ func NewManager() (*Manager, error) {
return m, nil return m, nil
} }
func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) { func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
obj := conn.Object(bluezService, dbus.ObjectPath("/")) obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/"))
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil { if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
return "", fmt.Errorf("%w: %v", ErrNoAdapter, err) return "", err
} }
for path, interfaces := range objects { for path, interfaces := range objects {
@@ -89,7 +89,7 @@ func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) {
} }
} }
return "", ErrNoAdapter return "", fmt.Errorf("no adapter found")
} }
func (m *Manager) initialize() error { func (m *Manager) initialize() error {
@@ -487,11 +487,6 @@ func (m *Manager) StopDiscovery() error {
} }
func (m *Manager) SetPowered(powered bool) 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) obj := m.dbusConn.Object(bluezService, m.adapterPath)
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
} }
@@ -1,5 +0,0 @@
package bluez
func rfkillUnblockBluetooth() error {
return nil
}
@@ -1,19 +0,0 @@
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" "fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type SubscriptionBroker struct { type SubscriptionBroker struct {
+1 -1
View File
@@ -3,7 +3,7 @@ package bluez
import ( import (
"sync" "sync"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
-46
View File
@@ -1,46 +0,0 @@
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)
}
+12 -10
View File
@@ -1,13 +1,15 @@
package brightness package brightness
import ( import (
"encoding/json"
"fmt" "fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "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 { switch req.Method {
case "brightness.getState": case "brightness.getState":
handleGetState(conn, req, m) 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()) 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") device, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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()) 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") device, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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()) 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") device, err := params.String(req.Params, "device")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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()) 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() m.Rescan()
models.Respond(conn, req.ID, m.GetState()) 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) clientID := fmt.Sprintf("brightness-%d", req.ID)
ch := m.Subscribe(clientID) ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID) defer m.Unsubscribe(clientID)
initialState := m.GetState() initialState := m.GetState()
if err := conn.WriteResponse(models.Response[State]{ if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID, ID: req.ID,
Result: &initialState, Result: &initialState,
}); err != nil { }); err != nil {
@@ -112,7 +114,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
} }
for state := range ch { for state := range ch {
if err := conn.WriteResponse(models.Response[State]{ if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID, ID: req.ID,
Result: &state, Result: &state,
}); err != nil { }); err != nil {
+45 -15
View File
@@ -20,7 +20,7 @@ func NewManagerWithOptions(exponential bool) (*Manager, error) {
} }
go m.initLogind() go m.initLogind()
go m.initNative() go m.initSysfs()
go m.initDDC() go m.initDDC()
return m, nil return m, nil
@@ -40,6 +40,39 @@ func (m *Manager) initLogind() {
log.Info("Logind backend initialized - will use for brightness control") 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() { func (m *Manager) initDDC() {
ddc, err := NewDDCBackend() ddc, err := NewDDCBackend()
if err != nil { if err != nil {
@@ -63,9 +96,9 @@ func (m *Manager) Rescan() {
} }
} }
if m.nativeReady && m.nativeBackend != nil { if m.sysfsReady && m.sysfsBackend != nil {
if err := m.nativeBackend.Rescan(); err != nil { if err := m.sysfsBackend.Rescan(); err != nil {
log.Debugf("Native backend rescan failed: %v", err) log.Debugf("Sysfs rescan failed: %v", err)
} }
} }
@@ -117,10 +150,10 @@ func stateChanged(old, new State) bool {
func (m *Manager) updateState() { func (m *Manager) updateState() {
allDevices := make([]Device, 0) allDevices := make([]Device, 0)
if m.nativeReady && m.nativeBackend != nil { if m.sysfsReady && m.sysfsBackend != nil {
devices, err := m.nativeBackend.GetDevices() devices, err := m.sysfsBackend.GetDevices()
if err != nil { 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 { if err == nil {
allDevices = append(allDevices, devices...) allDevices = append(allDevices, devices...)
@@ -199,21 +232,18 @@ func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, expone
m.stateMutex.Unlock() m.stateMutex.Unlock()
var err error var err error
switch { if deviceClass == ClassDDC {
case deviceClass == ClassDDC:
log.Debugf("Calling DDC backend for %s", deviceID) log.Debugf("Calling DDC backend for %s", deviceID)
err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() { err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() {
m.updateState() m.updateState()
m.debouncedBroadcast(deviceID) m.debouncedBroadcast(deviceID)
}) })
case m.logindReady && m.logindBackend != nil: } else if m.logindReady && m.logindBackend != nil {
log.Debugf("Calling logind backend for %s", deviceID) log.Debugf("Calling logind backend for %s", deviceID)
err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent) err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent)
case m.nativeBackend != nil: } else {
log.Debugf("Calling native backend for %s", deviceID) log.Debugf("Calling sysfs backend for %s", deviceID)
err = m.nativeBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent) err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
default:
err = fmt.Errorf("no brightness backend for %s", deviceID)
} }
if err != nil { 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)
}
@@ -43,7 +43,7 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
nativeReady: true, sysfsReady: true,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
@@ -114,7 +114,7 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
nativeReady: true, sysfsReady: true,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
@@ -180,9 +180,8 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
m := &Manager{ m := &Manager{
logindBackend: nil, logindBackend: nil,
sysfsBackend: sysfs, sysfsBackend: sysfs,
nativeBackend: sysfs,
logindReady: false, logindReady: false,
nativeReady: true, sysfsReady: true,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
@@ -244,7 +243,7 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
logindBackend: mockLogind, logindBackend: mockLogind,
sysfsBackend: sysfs, sysfsBackend: sysfs,
logindReady: true, logindReady: true,
nativeReady: true, sysfsReady: true,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
+5 -16
View File
@@ -4,7 +4,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type DeviceClass string type DeviceClass string
@@ -33,25 +33,14 @@ type DeviceUpdate struct {
Device Device `json:"device"` 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 { type Manager struct {
logindBackend *LogindBackend logindBackend *LogindBackend
sysfsBackend *SysfsBackend sysfsBackend *SysfsBackend
nativeBackend Backend
ddcBackend *DDCBackend ddcBackend *DDCBackend
monitor deviceMonitor udevMonitor *UdevMonitor
logindReady bool logindReady bool
nativeReady bool sysfsReady bool
ddcReady bool ddcReady bool
exponential bool exponential bool
@@ -181,8 +170,8 @@ func (m *Manager) Close() {
return true return true
}) })
if m.monitor != nil { if m.udevMonitor != nil {
m.monitor.Close() m.udevMonitor.Close()
} }
if m.logindBackend != nil { if m.logindBackend != nil {
@@ -33,7 +33,7 @@ func setupTestManager(t *testing.T) (*Manager, string) {
m := &Manager{ m := &Manager{
sysfsBackend: sysfs, sysfsBackend: sysfs,
nativeReady: true, sysfsReady: true,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
+3 -1
View File
@@ -1,10 +1,12 @@
package browser package browser
import ( import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "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 { switch req.Method {
case "browser.open": case "browser.open":
url, ok := models.Get[string](req, "url") url, ok := models.Get[string](req, "url")
+1 -1
View File
@@ -3,7 +3,7 @@ package browser
import ( import (
"sync" "sync"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type Manager struct { type Manager struct {
+25 -23
View File
@@ -1,15 +1,17 @@
package clipboard package clipboard
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net"
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "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 { switch req.Method {
case "clipboard.getState": case "clipboard.getState":
handleGetState(conn, req, m) 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()) 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() history := m.GetHistory()
for i := range history { for i := range history {
history[i].Data = nil history[i].Data = nil
@@ -68,7 +70,7 @@ func handleGetHistory(conn *models.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, history) 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") id, err := params.Int(req.Params, "id")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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") id, err := params.Int(req.Params, "id")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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() m.ClearHistory()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "history cleared"}) 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") text, err := params.String(req.Params, "text")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") id, err := params.Int(req.Params, "id")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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() text, err := m.PasteText()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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}) 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") shift, _ := models.Get[bool](req, "shift")
if err := clipboardstore.SendPasteKeystroke(shift); err != nil { 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"}) 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) clientID := fmt.Sprintf("clipboard-%d", req.ID)
ch := m.Subscribe(clientID) ch := m.Subscribe(clientID)
defer m.Unsubscribe(clientID) defer m.Unsubscribe(clientID)
initialState := m.GetState() initialState := m.GetState()
if err := conn.WriteResponse(models.Response[State]{ if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID, ID: req.ID,
Result: &initialState, Result: &initialState,
}); err != nil { }); err != nil {
@@ -207,7 +209,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, m *Manager) {
} }
for state := range ch { for state := range ch {
if err := conn.WriteResponse(models.Response[State]{ if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID, ID: req.ID,
Result: &state, Result: &state,
}); err != nil { }); 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{ p := SearchParams{
Query: params.StringOpt(req.Params, "query", ""), Query: params.StringOpt(req.Params, "query", ""),
MimeType: params.StringOpt(req.Params, "mimeType", ""), 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)) 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()) 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() cfg := m.GetConfig()
if v, ok := models.Get[float64](req, "maxHistory"); ok { 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"}) 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") data, err := params.String(req.Params, "data")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") id, err := params.Int(req.Params, "id")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") id, err := params.Int(req.Params, "id")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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() pinned := m.GetPinnedEntries()
models.Respond(conn, req.ID, pinned) 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() count := m.GetPinnedCount()
models.Respond(conn, req.ID, map[string]int{"count": count}) 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") filePath, err := params.String(req.Params, "filePath")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
@@ -311,15 +311,14 @@ func TestHandleGetEntry_ReturnsExistingEntry(t *testing.T) {
history := m.GetHistory() history := m.GetHistory()
require.Len(t, history, 1) require.Len(t, history, 1)
mc := newClipboardTestConn() conn := newClipboardTestConn()
conn := models.NewConn(mc)
handleGetEntry(conn, models.Request{ handleGetEntry(conn, models.Request{
ID: 1, ID: 1,
Params: map[string]any{"id": float64(history[0].ID)}, Params: map[string]any{"id": float64(history[0].ID)},
}, m) }, m)
var resp models.Response[Entry] 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) assert.Empty(t, resp.Error)
require.NotNil(t, resp.Result) require.NotNil(t, resp.Result)
assert.Equal(t, history[0].ID, resp.Result.ID) 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) { func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
m := newTestManagerWithDB(t) m := newTestManagerWithDB(t)
mc := newClipboardTestConn() conn := newClipboardTestConn()
conn := models.NewConn(mc)
handleGetEntry(conn, models.Request{ handleGetEntry(conn, models.Request{
ID: 1, ID: 1,
@@ -337,7 +335,7 @@ func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
}, m) }, m)
var resp models.Response[any] 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.Empty(t, resp.Error)
assert.Nil(t, resp.Result) assert.Nil(t, resp.Result)
} }
+31 -29
View File
@@ -1,10 +1,12 @@
package cups package cups
import ( import (
"encoding/json"
"fmt" "fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "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 { type CUPSEvent struct {
@@ -18,7 +20,7 @@ type TestPageResult struct {
Message string `json:"message"` 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 { switch req.Method {
case "cups.subscribe": case "cups.subscribe":
handleSubscribe(conn, req, manager) 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() printers, err := manager.GetPrinters()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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") printerName, err := params.String(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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") printerName, err := params.String(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.String(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") jobID, err := params.Int(req.Params, "jobID")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.String(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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) clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID) stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID) defer manager.Unsubscribe(clientID)
@@ -166,7 +168,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Data: initialState, Data: initialState,
} }
if err := conn.WriteResponse(models.Response[CUPSEvent]{ if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
ID: req.ID, ID: req.ID,
Result: &event, Result: &event,
}); err != nil { }); err != nil {
@@ -178,7 +180,7 @@ func handleSubscribe(conn *models.Conn, req models.Request, manager *Manager) {
Type: "state_changed", Type: "state_changed",
Data: state, Data: state,
} }
if err := conn.WriteResponse(models.Response[CUPSEvent]{ if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
Result: &event, Result: &event,
}); err != nil { }); err != nil {
return 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() devices, err := manager.GetDevices()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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() ppds, err := manager.GetPPDs()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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() classes, err := manager.GetClasses()
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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") name, err := params.StringNonEmpty(req.Params, "name")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") jobID, err := params.Int(req.Params, "jobID")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") printerName, err := params.StringNonEmpty(req.Params, "printerName")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") className, err := params.StringNonEmpty(req.Params, "className")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") className, err := params.StringNonEmpty(req.Params, "className")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") className, err := params.StringNonEmpty(req.Params, "className")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") jobID, err := params.Int(req.Params, "jobID")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") jobID, err := params.Int(req.Params, "jobID")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") host, err := params.StringNonEmpty(req.Params, "host")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
+28 -28
View File
@@ -41,7 +41,7 @@ func TestHandleGetPrinters(t *testing.T) {
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -66,7 +66,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -98,7 +98,7 @@ func TestHandleGetJobs(t *testing.T) {
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -125,7 +125,7 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -150,7 +150,7 @@ func TestHandlePausePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -177,7 +177,7 @@ func TestHandleResumePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -204,7 +204,7 @@ func TestHandleCancelJob(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -231,7 +231,7 @@ func TestHandlePurgeJobs(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -258,7 +258,7 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -285,7 +285,7 @@ func TestHandleGetDevices(t *testing.T) {
m := &Manager{client: mockClient} m := &Manager{client: mockClient}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getDevices"} req := models.Request{ID: 1, Method: "cups.getDevices"}
handleGetDevices(conn, req, m) handleGetDevices(conn, req, m)
@@ -307,7 +307,7 @@ func TestHandleGetPPDs(t *testing.T) {
m := &Manager{client: mockClient} m := &Manager{client: mockClient}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getPPDs"} req := models.Request{ID: 1, Method: "cups.getPPDs"}
handleGetPPDs(conn, req, m) handleGetPPDs(conn, req, m)
@@ -330,7 +330,7 @@ func TestHandleGetClasses(t *testing.T) {
m := &Manager{client: mockClient} m := &Manager{client: mockClient}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "cups.getClasses"} req := models.Request{ID: 1, Method: "cups.getClasses"}
handleGetClasses(conn, req, m) handleGetClasses(conn, req, m)
@@ -351,7 +351,7 @@ func TestHandleCreatePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -375,7 +375,7 @@ func TestHandleCreatePrinter_MissingParams(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
m := &Manager{client: mockClient} m := &Manager{client: mockClient}
buf := &bytes.Buffer{} 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{}} req := models.Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
handleCreatePrinter(conn, req, m) handleCreatePrinter(conn, req, m)
@@ -394,7 +394,7 @@ func TestHandleDeletePrinter(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -417,7 +417,7 @@ func TestHandleAcceptJobs(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -440,7 +440,7 @@ func TestHandleRejectJobs(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -463,7 +463,7 @@ func TestHandleSetPrinterShared(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -486,7 +486,7 @@ func TestHandleSetPrinterLocation(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -509,7 +509,7 @@ func TestHandleSetPrinterInfo(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -532,7 +532,7 @@ func TestHandleMoveJob(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -555,7 +555,7 @@ func TestHandlePrintTestPage(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -579,7 +579,7 @@ func TestHandleAddPrinterToClass(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -602,7 +602,7 @@ func TestHandleRemovePrinterFromClass(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -625,7 +625,7 @@ func TestHandleDeleteClass(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -648,7 +648,7 @@ func TestHandleRestartJob(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -671,7 +671,7 @@ func TestHandleHoldJob(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -694,7 +694,7 @@ func TestHandleHoldJob_WithHoldUntil(t *testing.T) {
m := NewTestManager(mockClient, nil) m := NewTestManager(mockClient, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
+10 -23
View File
@@ -229,9 +229,6 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState { func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64) ch := make(chan CUPSState, 64)
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
wasEmpty := true wasEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool { m.subscribers.Range(func(key string, ch chan CUPSState) bool {
wasEmpty = false wasEmpty = false
@@ -240,25 +237,19 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
m.subscribers.Store(id, ch) m.subscribers.Store(id, ch)
if !wasEmpty || m.subscription == nil { if wasEmpty && m.subscription != nil {
return ch 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 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 return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
if val, ok := m.subscribers.LoadAndDelete(id); ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val) close(val)
} }
@@ -269,22 +260,18 @@ func (m *Manager) Unsubscribe(id string) {
return false return false
}) })
if !isEmpty || m.subscription == nil { if isEmpty && m.subscription != nil {
return m.subscription.Stop()
m.eventWG.Wait()
} }
m.subscription.Stop()
m.eventWG.Wait()
} }
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.subLifecycleMu.Lock()
if m.subscription != nil { if m.subscription != nil {
m.subscription.Stop() m.subscription.Stop()
} }
m.subLifecycleMu.Unlock()
m.eventWG.Wait() m.eventWG.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
-64
View File
@@ -1,9 +1,6 @@
package cups package cups
import ( import (
"errors"
"fmt"
"sync"
"testing" "testing"
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
@@ -78,67 +75,6 @@ func TestManager_Subscribe(t *testing.T) {
assert.Equal(t, 0, count) 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) { func TestManager_Close(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
@@ -302,7 +302,7 @@ func TestHandleTestConnection_Success(t *testing.T) {
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -325,7 +325,7 @@ func TestHandleTestConnection_Success(t *testing.T) {
func TestHandleTestConnection_MissingHost(t *testing.T) { func TestHandleTestConnection_MissingHost(t *testing.T) {
m := NewTestManager(nil, nil) m := NewTestManager(nil, nil)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -351,7 +351,7 @@ func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
@@ -379,7 +379,7 @@ func TestHandleRequest_TestConnection(t *testing.T) {
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
conn := models.NewConn(&mockConn{Buffer: buf}) conn := &mockConn{Buffer: buf}
req := models.Request{ req := models.Request{
ID: 1, ID: 1,
+1 -2
View File
@@ -6,7 +6,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
type CUPSState struct { type CUPSState struct {
@@ -79,7 +79,6 @@ type Manager struct {
client CUPSClientInterface client CUPSClientInterface
pkHelper PkHelper pkHelper PkHelper
subscription SubscriptionManagerInterface subscription SubscriptionManagerInterface
subLifecycleMu sync.Mutex
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan CUPSState] subscribers syncmap.Map[string, chan CUPSState]
stopChan chan struct{} stopChan chan struct{}
+11 -10
View File
@@ -2,9 +2,10 @@ package dbus
import ( import (
"fmt" "fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "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 { 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 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 { switch req.Method {
case "dbus.call": case "dbus.call":
handleCall(conn, req, m) 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) op, err := extractObjectParams(req.Params, true)
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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) op, err := extractObjectParams(req.Params, true)
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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) op, err := extractObjectParams(req.Params, true)
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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}) 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) op, err := extractObjectParams(req.Params, true)
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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") bus, err := params.String(req.Params, "bus")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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") bus, err := params.String(req.Params, "bus")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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) 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 != "" { if id := params.StringOpt(req.Params, "clientId", ""); id != "" {
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) 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") subID, err := params.String(req.Params, "subscriptionId")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/dbusutil" "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
+1 -1
View File
@@ -1,7 +1,7 @@
package dbus package dbus
import ( import (
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
+4 -2
View File
@@ -1,10 +1,12 @@
package evdev package evdev
import ( import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "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 { switch req.Method {
case "evdev.getState": case "evdev.getState":
handleGetState(conn, req, m) 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()) models.Respond(conn, req.ID, m.GetState())
} }
+6 -9
View File
@@ -52,8 +52,7 @@ func TestHandleRequest(t *testing.T) {
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "evdev.getState", Method: "evdev.getState",
@@ -63,7 +62,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, m) HandleRequest(conn, req, m)
var resp models.Response[State] var resp models.Response[State]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -82,8 +81,7 @@ func TestHandleRequest(t *testing.T) {
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 456, ID: 456,
Method: "evdev.unknownMethod", Method: "evdev.unknownMethod",
@@ -93,7 +91,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, m) HandleRequest(conn, req, m)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 456, resp.ID) assert.Equal(t, 456, resp.ID)
@@ -112,8 +110,7 @@ func TestHandleGetState(t *testing.T) {
closeChan: make(chan struct{}), closeChan: make(chan struct{}),
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 789, ID: 789,
Method: "evdev.getState", Method: "evdev.getState",
@@ -123,7 +120,7 @@ func TestHandleGetState(t *testing.T) {
handleGetState(conn, req, m) handleGetState(conn, req, m)
var resp models.Response[State] var resp models.Response[State]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 789, resp.ID) assert.Equal(t, 789, resp.ID)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
evdev "github.com/holoplot/go-evdev" evdev "github.com/holoplot/go-evdev"
) )
+12 -11
View File
@@ -2,12 +2,13 @@ package freedesktop
import ( import (
"fmt" "fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "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 { switch req.Method {
case "freedesktop.getState": case "freedesktop.getState":
handleGetState(conn, req, manager) 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()) 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") iconPath, err := params.String(req.Params, "path")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") name, err := params.String(req.Params, "name")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") email, err := params.String(req.Params, "email")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") language, err := params.String(req.Params, "language")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") location, err := params.String(req.Params, "location")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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"}) 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") username, err := params.String(req.Params, "username")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) 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}) 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 { if err := manager.updateSettingsState(); err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return 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}) 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") iconTheme, err := params.String(req.Params, "iconTheme")
if err != nil { if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
@@ -60,12 +60,11 @@ func mockGetAllAccountsProperties() *dbus.Call {
} }
func TestRespondError_Freedesktop(t *testing.T) { func TestRespondError_Freedesktop(t *testing.T) {
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
models.RespondError(conn, 123, "test error") models.RespondError(conn, 123, "test error")
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -74,13 +73,12 @@ func TestRespondError_Freedesktop(t *testing.T) {
} }
func TestRespond_Freedesktop(t *testing.T) { func TestRespond_Freedesktop(t *testing.T) {
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
result := models.SuccessResult{Success: true, Message: "test"} result := models.SuccessResult{Success: true, Message: "test"}
models.Respond(conn, 123, result) models.Respond(conn, 123, result)
var resp models.Response[models.SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -107,14 +105,13 @@ func TestHandleGetState(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ID: 123, Method: "freedesktop.getState"} req := models.Request{ID: 123, Method: "freedesktop.getState"}
handleGetState(conn, req, manager) handleGetState(conn, req, manager)
var resp models.Response[FreedeskState] var resp models.Response[FreedeskState]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -133,8 +130,7 @@ func TestHandleSetIconFile(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
@@ -144,7 +140,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager) handleSetIconFile(conn, req, manager)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -167,8 +163,7 @@ func TestHandleSetIconFile(t *testing.T) {
accountsObj: mockAccountsObj, accountsObj: mockAccountsObj,
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
@@ -180,7 +175,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager) handleSetIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -200,8 +195,7 @@ func TestHandleSetIconFile(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setIconFile", Method: "freedesktop.accounts.setIconFile",
@@ -213,7 +207,7 @@ func TestHandleSetIconFile(t *testing.T) {
handleSetIconFile(conn, req, manager) handleSetIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -228,8 +222,7 @@ func TestHandleSetRealName(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setRealName", Method: "freedesktop.accounts.setRealName",
@@ -239,7 +232,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager) handleSetRealName(conn, req, manager)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -262,8 +255,7 @@ func TestHandleSetRealName(t *testing.T) {
accountsObj: mockAccountsObj, accountsObj: mockAccountsObj,
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setRealName", Method: "freedesktop.accounts.setRealName",
@@ -275,7 +267,7 @@ func TestHandleSetRealName(t *testing.T) {
handleSetRealName(conn, req, manager) handleSetRealName(conn, req, manager)
var resp models.Response[models.SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -293,8 +285,7 @@ func TestHandleSetEmail(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setEmail", Method: "freedesktop.accounts.setEmail",
@@ -304,7 +295,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager) handleSetEmail(conn, req, manager)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -327,8 +318,7 @@ func TestHandleSetEmail(t *testing.T) {
accountsObj: mockAccountsObj, accountsObj: mockAccountsObj,
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setEmail", Method: "freedesktop.accounts.setEmail",
@@ -340,7 +330,7 @@ func TestHandleSetEmail(t *testing.T) {
handleSetEmail(conn, req, manager) handleSetEmail(conn, req, manager)
var resp models.Response[models.SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -358,8 +348,7 @@ func TestHandleSetLanguage(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setLanguage", Method: "freedesktop.accounts.setLanguage",
@@ -369,7 +358,7 @@ func TestHandleSetLanguage(t *testing.T) {
handleSetLanguage(conn, req, manager) handleSetLanguage(conn, req, manager)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -384,8 +373,7 @@ func TestHandleSetLocation(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.setLocation", Method: "freedesktop.accounts.setLocation",
@@ -395,7 +383,7 @@ func TestHandleSetLocation(t *testing.T) {
handleSetLocation(conn, req, manager) handleSetLocation(conn, req, manager)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -410,8 +398,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.getUserIconFile", Method: "freedesktop.accounts.getUserIconFile",
@@ -421,7 +408,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager) handleGetUserIconFile(conn, req, manager)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -438,8 +425,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.accounts.getUserIconFile", Method: "freedesktop.accounts.getUserIconFile",
@@ -451,7 +437,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
handleGetUserIconFile(conn, req, manager) handleGetUserIconFile(conn, req, manager)
var resp models.Response[models.SuccessResult] var resp models.Response[models.SuccessResult]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -470,14 +456,13 @@ func TestHandleGetColorScheme(t *testing.T) {
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"} req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager) handleGetColorScheme(conn, req, manager)
var resp models.Response[map[string]uint32] 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) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -502,14 +487,13 @@ func TestHandleGetColorScheme(t *testing.T) {
settingsObj: mockSettingsObj, settingsObj: mockSettingsObj,
} }
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"} req := models.Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
handleGetColorScheme(conn, req, manager) handleGetColorScheme(conn, req, manager)
var resp models.Response[map[string]uint32] 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) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -531,8 +515,7 @@ func TestHandleRequest(t *testing.T) {
} }
t.Run("unknown method", func(t *testing.T) { t.Run("unknown method", func(t *testing.T) {
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.unknown", Method: "freedesktop.unknown",
@@ -541,7 +524,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager) HandleRequest(conn, req, manager)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -549,8 +532,7 @@ func TestHandleRequest(t *testing.T) {
}) })
t.Run("valid method - getState", func(t *testing.T) { t.Run("valid method - getState", func(t *testing.T) {
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: "freedesktop.getState", Method: "freedesktop.getState",
@@ -559,7 +541,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager) HandleRequest(conn, req, manager)
var resp models.Response[FreedeskState] var resp models.Response[FreedeskState]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
@@ -578,8 +560,7 @@ func TestHandleRequest(t *testing.T) {
} }
for _, method := range tests { for _, method := range tests {
mc := newMockNetConn() conn := newMockNetConn()
conn := models.NewConn(mc)
req := models.Request{ req := models.Request{
ID: 123, ID: 123,
Method: method, Method: method,
@@ -589,7 +570,7 @@ func TestHandleRequest(t *testing.T) {
HandleRequest(conn, req, manager) HandleRequest(conn, req, manager)
var resp models.Response[any] var resp models.Response[any]
err := json.NewDecoder(mc.writeBuf).Decode(&resp) err := json.NewDecoder(conn.writeBuf).Decode(&resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, resp.ID) assert.Equal(t, 123, resp.ID)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/dankgo/dbusutil" "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/dankgo/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )

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