Compare commits

..
906 changed files with 88207 additions and 110550 deletions
-23
View File
@@ -1,23 +0,0 @@
# Categories for GitHub's auto-generated release notes
changelog:
exclude:
authors:
- github-actions[bot]
- dependabot[bot]
labels:
- internal
categories:
- title: Breaking Changes
labels: [breaking]
- title: Features
labels: [feature]
- title: Fixes
labels: [fix]
- title: Packaging
labels: [packaging]
- title: Internationalization
labels: [i18n]
- title: Documentation
labels: [docs]
- title: Other Changes
labels: ["*"]
-170
View File
@@ -1,170 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "usage: $0 download|publish" >&2
exit 2
}
require_env() {
local name
for name in "$@"; do
if [[ -z "${!name:-}" ]]; then
echo "error: $name is required" >&2
exit 2
fi
done
}
require_env R2_BUCKET R2_ENDPOINT R2_PREFIX REPOSITORY_DIR
aws_r2() {
aws --endpoint-url "$R2_ENDPOINT" "$@"
}
download() {
mkdir -p "$REPOSITORY_DIR/current" "$REPOSITORY_DIR/previous-current"
aws_r2 s3 sync \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/" \
"$REPOSITORY_DIR/current/" \
--only-show-errors
cp -a "$REPOSITORY_DIR/current/." "$REPOSITORY_DIR/previous-current/"
}
build_manifest() {
local packages='[]'
local file filename pkgver name version revision sha size
for file in "$REPOSITORY_DIR"/current/*.xbps; do
filename="$(basename "$file")"
pkgver="$(xbps-uhelper binpkgver "$filename")"
name="$(xbps-uhelper getpkgname "$pkgver")"
version="$(xbps-uhelper getpkgversion "$pkgver")"
revision="$(xbps-uhelper getpkgrevision "$pkgver")"
version="${version%_"${revision}"}"
sha="$(sha256sum "$file" | cut -d' ' -f1)"
size="$(stat -c '%s' "$file")"
packages="$(jq \
--arg name "$name" \
--arg version "$version" \
--arg revision "$revision" \
--arg filename "$filename" \
--arg sha256 "$sha" \
--argjson size "$size" \
'. + [{name: $name, version: $version, revision: $revision, filename: $filename, sha256: $sha256, size: $size}]' \
<<<"$packages")"
done
jq -n \
--arg repository "$R2_PREFIX" \
--arg source_commit "$SOURCE_COMMIT" \
--arg published_at "$PUBLISHED_AT" \
--argjson packages "$packages" \
'{schema: 1, repository: $repository, source_commit: $source_commit, published_at: $published_at, packages: $packages}' \
> "$REPOSITORY_DIR/current/manifest.json"
}
verify_immutable_packages() {
local file previous
for file in "$REPOSITORY_DIR"/current/*.xbps "$REPOSITORY_DIR"/current/*.sig2; do
previous="$REPOSITORY_DIR/previous-current/$(basename "$file")"
if [[ -f "$previous" ]] && ! cmp -s "$previous" "$file"; then
echo "error: refusing to replace immutable object $(basename "$file")" >&2
echo "bump the XBPS revision or version before publishing a changed build" >&2
exit 1
fi
done
}
archive_retired() {
local old filename
local archive_prefix="archive/${R2_PREFIX}/${PUBLISHED_AT//:/-}"
shopt -s nullglob
for old in "$REPOSITORY_DIR"/previous-current/*.xbps "$REPOSITORY_DIR"/previous-current/*.sig2; do
filename="$(basename "$old")"
if [[ ! -e "$REPOSITORY_DIR/current/$filename" ]]; then
aws_r2 s3 cp \
"$old" \
"s3://${R2_BUCKET}/${archive_prefix}/${filename}" \
--cache-control 'private,no-store' \
--only-show-errors
fi
done
}
upload_current() {
local file filename old
# Versioned package objects must exist before repodata can reference them.
for file in "$REPOSITORY_DIR"/current/*.xbps "$REPOSITORY_DIR"/current/*.sig2; do
filename="$(basename "$file")"
aws_r2 s3 cp \
"$file" \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/${filename}" \
--cache-control 'public,max-age=31536000,immutable' \
--only-show-errors
done
aws_r2 s3 cp \
"$REPOSITORY_DIR/current/x86_64-repodata" \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/x86_64-repodata" \
--cache-control 'no-cache' \
--only-show-errors
# The manifest is the publication marker and is always uploaded last.
aws_r2 s3 cp \
"$REPOSITORY_DIR/current/manifest.json" \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/manifest.json" \
--cache-control 'no-cache' \
--only-show-errors
# Once the new index and marker are live, remove objects no longer referenced.
shopt -s nullglob
for old in "$REPOSITORY_DIR"/previous-current/*; do
filename="$(basename "$old")"
if [[ ! -e "$REPOSITORY_DIR/current/$filename" ]]; then
aws_r2 s3 rm \
"s3://${R2_BUCKET}/${R2_PREFIX}/current/${filename}" \
--only-show-errors
fi
done
}
publish() {
require_env SOURCE_COMMIT
PUBLISHED_AT="${PUBLISHED_AT:-$(date -u +'%Y-%m-%dT%H:%M:%SZ')}"
export PUBLISHED_AT
shopt -s nullglob
local packages=("$REPOSITORY_DIR"/current/*.xbps)
if (( ${#packages[@]} == 0 )); then
echo "error: refusing to publish an empty XBPS repository" >&2
exit 1
fi
[[ -s "$REPOSITORY_DIR/current/x86_64-repodata" ]] || {
echo "error: x86_64-repodata is missing or empty" >&2
exit 1
}
for file in "${packages[@]}"; do
[[ -s "${file}.sig2" ]] || {
echo "error: signature is missing for $(basename "$file")" >&2
exit 1
}
done
verify_immutable_packages
build_manifest
archive_retired
upload_current
}
case "${1:-}" in
download) download ;;
publish) publish ;;
*) usage ;;
esac
-56
View File
@@ -1,56 +0,0 @@
name: Sync flake.lock to dank-qml-common
on:
workflow_dispatch:
push:
paths:
- "dank-qml-common"
branches:
- master
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Point flake input at the submodule commit
run: |
set -euo pipefail
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only)
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])")
[ "$submodule_rev" = "$flake_rev" ] && { echo "flake.lock already matches $submodule_rev"; exit 0; }
nix flake lock --override-input dank-qml-common "github:AvengeMedia/dank-qml-common/$submodule_rev"
- name: Commit and push flake.lock update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if git diff --quiet flake.lock; then
echo "No changes to flake.lock"
exit 0
fi
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.lock
git commit -m "nix: sync flake.lock to dank-qml-common submodule"
git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
+2 -55
View File
@@ -7,17 +7,14 @@ on:
permissions: permissions:
contents: write contents: write
issues: write
jobs: jobs:
update-stable: update-stable:
# skip prerelease tags
if: ${{ !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Create GitHub App token - name: Create GitHub App token
id: app_token id: app_token
uses: actions/create-github-app-token@v3 uses: actions/create-github-app-token@v1
with: with:
app-id: ${{ secrets.APP_ID }} app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
@@ -31,54 +28,4 @@ jobs:
- name: Push to stable branch - name: Push to stable branch
env: env:
GH_TOKEN: ${{ steps.app_token.outputs.token }} GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: | run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force
set -euo pipefail
# don't roll stable backwards
if git fetch origin stable --quiet 2>/dev/null; then
stable_tag=$(git describe --tags --abbrev=0 FETCH_HEAD 2>/dev/null || echo "v0.0.0")
newest=$(printf '%s\n%s\n' "$stable_tag" "${GITHUB_REF_NAME}" | sort -V | tail -1)
if [ "$newest" != "${GITHUB_REF_NAME}" ]; then
echo "skipping: ${GITHUB_REF_NAME} is older than stable (${stable_tag})"
exit 0
fi
fi
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" HEAD:refs/heads/stable --force
cut-release-branch:
# create stable-X.Y at each vX.Y.0 tag
if: ${{ !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Create release branch
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if [[ ! "${GITHUB_REF_NAME}" =~ ^v([0-9]+)\.([0-9]+)\.0$ ]]; then
echo "not a vX.Y.0 tag, no release branch to cut"
exit 0
fi
branch="stable-${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
if git ls-remote --exit-code origin "refs/heads/${branch}" >/dev/null 2>&1; then
echo "${branch} already exists"
exit 0
fi
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:refs/heads/${branch}"
echo "created ${branch} at ${GITHUB_REF_NAME}"
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh label create "port ${branch}" \
--repo "${{ github.repository }}" \
--description "Auto-port merged PR to ${branch}" --color 0e8a16 ||
echo "label 'port ${branch}' may already exist"
+2 -35
View File
@@ -8,7 +8,7 @@ on:
- "core/**" - "core/**"
- ".github/workflows/go-ci.yml" - ".github/workflows/go-ci.yml"
pull_request: pull_request:
branches: [master, main, "stable-*"] branches: [master, main]
paths: paths:
- "core/**" - "core/**"
- ".github/workflows/go-ci.yml" - ".github/workflows/go-ci.yml"
@@ -27,8 +27,6 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak - name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak run: sudo apt update && sudo apt install -y flatpak
@@ -50,39 +48,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
+3 -1
View File
@@ -2,7 +2,7 @@ name: Nix flake and NixOS tests
on: on:
pull_request: pull_request:
branches: [master, main, "stable-*"] branches: [master, main]
paths: paths:
- "flake.*" - "flake.*"
- "distro/nix/**" - "distro/nix/**"
@@ -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
-107
View File
@@ -1,107 +0,0 @@
name: Point release
# Cuts vX.Y.Z from stable-X.Y: runs the port audit (warn-only), bumps
# quickshell/VERSION, tags, and dispatches the Release workflow. Distro
# builds are dispatched separately.
on:
workflow_dispatch:
inputs:
version:
description: "Point release version (e.g. 1.5.1)"
required: true
type: string
notify_issues:
description: "Comment on open related/fixes issues asking reporters to retest"
required: false
type: boolean
default: true
permissions:
contents: write
actions: write
concurrency:
group: point-release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
env:
VERSION: ${{ inputs.version }}
steps:
- name: Validate version and derive branch
id: derive
run: |
set -euo pipefail
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::version must be X.Y.Z (got '$VERSION')"; exit 1
fi
echo "branch=stable-${VERSION%.*}" >> "$GITHUB_OUTPUT"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout release branch
uses: actions/checkout@v6
with:
ref: ${{ steps.derive.outputs.branch }}
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Preflight — verify Release workflow is dispatchable
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh api "repos/${{ github.repository }}/actions/workflows/release.yml" \
--jq '.state' | grep -qx active ||
{ echo "::error::release.yml is not dispatchable; aborting before any push"; exit 1; }
- name: Port audit (informational)
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
bash scripts/port-audit.sh "${{ steps.derive.outputs.branch }}" ||
echo "::warning::port audit failed; continuing"
- name: Bump VERSION, tag, and push
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
TAG: ${{ steps.derive.outputs.tag }}
BRANCH: ${{ steps.derive.outputs.branch }}
run: |
set -euo pipefail
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
echo "::error::tag ${TAG} already exists"; exit 1
fi
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
echo "${TAG}" > quickshell/VERSION
git add quickshell/VERSION
git commit -m "bump VERSION to ${TAG}"
git tag "${TAG}"
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH}" "refs/tags/${TAG}"
- name: Dispatch Release workflow
env:
GH_TOKEN: ${{ github.token }}
run: |
gh workflow run release.yml --ref "${{ steps.derive.outputs.tag }}" \
-f tag="${{ steps.derive.outputs.tag }}" \
-f notify_issues="${{ inputs.notify_issues }}"
- name: Next steps
run: |
{
echo "## ${{ steps.derive.outputs.tag }} tagged on ${{ steps.derive.outputs.branch }} — Release workflow dispatched"
echo ""
echo "Distro builds are manual: run the per-distro workflows (COPR/OBS/PPA/XBPS) once the release is published."
} >> "$GITHUB_STEP_SUMMARY"
-30
View File
@@ -1,30 +0,0 @@
name: Port audit
# On-demand report of master commits not yet ported to a release branch.
# Updates the "Port status: <branch>" tracking issue and the step summary.
on:
workflow_dispatch:
inputs:
target:
description: "Release branch to audit (default: newest release/*)"
required: false
type: string
permissions:
contents: read
issues: write
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run audit
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash scripts/port-audit.sh ${{ inputs.target }} --issue
-101
View File
@@ -1,101 +0,0 @@
name: Port to release branch
# Ports flagged commits from master onto stable-X.Y branches:
# - "port 1.5" flag in a commit message pushed to master — own line or
# mid-line; separators : / - or space; "stable-1.5" also accepted;
# comma lists ("Port: 1.5, 1.4") work in the own-line form
# - "port stable-1.5" label on a merged PR
# Conflicts are reported to the "Port status: <branch>" tracking issue.
on:
push:
branches: [master]
pull_request_target:
types: [closed, labeled]
permissions:
contents: write
issues: write
concurrency:
group: port-engine
cancel-in-progress: false
jobs:
port:
name: Port flagged commits
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Base-repo code only; PR head code is never checked out or executed.
- name: Checkout
uses: actions/checkout@v6
with:
ref: master
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Port trailer-flagged commits
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
COMMITS: ${{ toJSON(github.event.commits) }}
run: |
set -euo pipefail
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
for sha in $(jq -r '.[].id' <<<"$COMMITS"); do
git cat-file -e "$sha" 2>/dev/null || continue
# skip merge commits (handled by the label path)
[ "$(git rev-list --no-walk --count --min-parents=2 "$sha")" -eq 0 ] || continue
# own-line form (supports comma lists), validated version-shaped
t1=$(git log -1 --format=%B "$sha" |
{ grep -iE '^Port[:/ -]' || true; } | sed -E 's|^port[-:/ ]+||I' | tr ',' '\n' |
sed 's/[[:space:]]//g; /^$/d' | sed -E 's,^stable-,,I' |
{ grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?$' || true; })
# mid-line form, e.g. "fix thing fixes #2802 port 1.5"
t2=$(git log -1 --format=%B "$sha" |
{ grep -oiE '\bport[-: /]+(stable-)?[0-9]+\.[0-9]+(\.[0-9]+)?\b' || true; } |
sed -E 's|^port[-:/ ]+||I' | sed -E 's,^stable-,,I')
targets=$(printf '%s\n%s\n' "$t1" "$t2" | sed '/^$/d' | sort -u)
for ver in $targets; do
echo "::group::port $sha -> stable-$ver"
bash scripts/port.sh "stable-$ver" "$sha"
echo "::endgroup::"
done
done
- name: Port label-flagged PR
if: >
github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
(github.event.action == 'closed' ||
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'port ')))
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
PORT_SOURCE_PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
targets=$(jq -r '.[] | select(startswith("port ")) | sub("^port +"; "")' <<<"$LABELS" |
sed -E 's,^stable-,,' | sort -u)
[ -n "$targets" ] || { echo "no port labels, nothing to do"; exit 0; }
[ -n "$MERGE_SHA" ] || { echo "::error::PR has no merge commit sha"; exit 1; }
for ver in $targets; do
echo "::group::port PR #${PORT_SOURCE_PR} ($MERGE_SHA) -> stable-$ver"
bash scripts/port.sh "stable-$ver" "$MERGE_SHA"
echo "::endgroup::"
done
+1 -3
View File
@@ -3,15 +3,13 @@ name: Pre-commit Checks
on: on:
push: push:
pull_request: pull_request:
branches: [master, main, "stable-*"] branches: [master, main]
jobs: jobs:
pre-commit-check: pre-commit-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak - name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak run: sudo apt update && sudo apt install -y flatpak
+50 -130
View File
@@ -7,16 +7,10 @@ on:
description: "Tag to release (e.g., v1.0.1)" description: "Tag to release (e.g., v1.0.1)"
required: true required: true
type: string type: string
notify_issues:
description: "Comment on open related/fixes issues asking reporters to retest"
required: false
type: boolean
default: false
permissions: permissions:
contents: write contents: write
actions: write actions: write
issues: write
concurrency: concurrency:
group: release-${{ inputs.tag }} group: release-${{ inputs.tag }}
@@ -27,11 +21,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 +29,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
@@ -48,7 +36,6 @@ jobs:
with: with:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@@ -56,7 +43,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 +51,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 +90,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
@@ -216,13 +185,11 @@ jobs:
with: with:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Fetch updated tag after version bump - name: Fetch updated tag after version bump
run: | run: |
git fetch origin --force tag ${TAG} git fetch origin --force tag ${TAG}
git checkout ${TAG} git checkout ${TAG}
git submodule update --init --recursive
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@@ -238,23 +205,13 @@ jobs:
- name: Generate Changelog - name: Generate Changelog
id: changelog id: changelog
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -e set -e
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "") PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
CHANGELOG="" if [ -z "$PREVIOUS_TAG" ]; then
if [ -n "$PREVIOUS_TAG" ]; then CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
# PR-based notes with author credits; falls back to raw git log below else
CHANGELOG=$(python3 scripts/release-notes.py "${PREVIOUS_TAG}..${TAG}" --format github --bare 2>/dev/null || true) CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
fi
if [ -z "$CHANGELOG" ]; then
echo "release-notes.py unavailable or empty, using git log fallback"
if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
else
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
fi
fi fi
cat > RELEASE_BODY.md << 'EOF' cat > RELEASE_BODY.md << 'EOF'
@@ -269,23 +226,16 @@ 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)
- **`dms-qml.tar.gz`** - QML source code only - **`dms-qml.tar.gz`** - QML source code only
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging)
### Checksums ### Checksums
- **`*.sha256`** - SHA256 checksums for verifying download integrity - **`*.sha256`** - SHA256 checksums for verifying download integrity
@@ -333,22 +283,6 @@ jobs:
# Copy completions # Copy completions
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Replace the DankCommon symlink with real submodule content for packaging
rm quickshell/DankCommon
cp -r dank-qml-common/DankCommon quickshell/DankCommon
# Create full source tarball (GitHub tag archives never contain submodule content)
VERSION_NUM=${TAG#v}
tar --exclude='.git' \
--exclude='.github' \
--exclude='dank-qml-common' \
--exclude='_release_assets' \
--exclude='_core_assets' \
--exclude='RELEASE_BODY.md' \
--transform "s,^\.,DankMaterialShell-${VERSION_NUM},S" \
-czf _release_assets/dms-source.tar.gz .
(cd _release_assets && sha256sum dms-source.tar.gz > dms-source.tar.gz.sha256)
# Create QML source package (exclude build artifacts and git files) # Create QML source package (exclude build artifacts and git files)
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging # Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/ cp LICENSE CONTRIBUTING.md quickshell/
@@ -365,8 +299,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 +343,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 +366,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
``` ```
@@ -481,19 +417,3 @@ jobs:
prerelease: ${{ contains(env.TAG, '-') }} prerelease: ${{ contains(env.TAG, '-') }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Ask reporters on still-open "related/fixes #N" issues to retest.
# Opt-in via notify_issues (Point release defaults it on). Skipped for
# prereleases. Preview: scripts/notify-issues.sh <prev> <tag> --dry-run
- name: Notify open issues to retest
if: ${{ inputs.notify_issues && !contains(env.TAG, '-') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)
if [ -z "$PREVIOUS_TAG" ]; then
echo "no previous tag; skipping issue notify"
exit 0
fi
bash scripts/notify-issues.sh "$PREVIOUS_TAG" "$TAG"
+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 -11
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"
@@ -35,7 +36,6 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Check for updates - name: Check for updates
id: check id: check
@@ -74,12 +74,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 +120,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 +128,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 +140,7 @@ jobs:
else else
echo "packages=" >> $GITHUB_OUTPUT echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ 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 +164,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
@@ -168,7 +199,6 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Wait before OBS upload - name: Wait before OBS upload
run: sleep 3 run: sleep 3
@@ -189,9 +219,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 +287,7 @@ jobs:
echo " -- Avenge Media <[email protected]> $CHANGELOG_DATE" echo " -- Avenge Media <[email protected]> $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 +323,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 <[email protected]> $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 +402,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 +505,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 -2
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"
@@ -35,7 +36,6 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Install dependencies - name: Install dependencies
run: | run: |
@@ -94,7 +94,6 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@@ -134,6 +133,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 +148,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"
+63 -108
View File
@@ -3,15 +3,10 @@ name: Void Linux XBPS Repository
on: on:
schedule: schedule:
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown) - cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
# release trigger disabled; dispatch manually after a release release:
# release: types: [published]
# types: [published]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version:
description: 'Stable release tag/version to build (e.g. v1.5.0 or 1.5.0). Leave blank to use the selected ref/template version'
type: string
required: false
force_rebuild: force_rebuild:
description: 'Force rebuilding packages even if they already exist in the repository' description: 'Force rebuilding packages even if they already exist in the repository'
type: boolean type: boolean
@@ -27,66 +22,37 @@ 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: write
concurrency:
group: void-xbps-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
preflight:
name: Check for unpublished changes
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
steps:
- name: Check published commit
id: check
env:
MANIFEST_URL: https://void.danklinux.com/dms/current/manifest.json
run: |
SHOULD_BUILD=true
if [ "${{ github.event_name }}" = "schedule" ]; then
PUBLISHED_COMMIT="$(curl -fsSL --retry 3 "$MANIFEST_URL" | jq -r '.source_commit // empty' 2>/dev/null || true)"
if [ "$PUBLISHED_COMMIT" = "${{ github.sha }}" ]; then
SHOULD_BUILD=false
echo "${{ github.sha }} is already published; skipping the scheduled build."
fi
fi
echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT"
build-and-deploy: build-and-deploy:
name: Build & Deploy XBPS packages name: Build & Deploy XBPS packages
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: preflight if: github.repository == 'AvengeMedia/DankMaterialShell'
if: github.repository == 'AvengeMedia/DankMaterialShell' && needs.preflight.outputs.should_build == 'true'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: true
R2_BUCKET: danklinux-void
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
R2_PREFIX: dms
REPOSITORY_DIR: ${{ github.workspace }}/r2-repo
SOURCE_COMMIT: ${{ github.sha }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.release.tag_name || (github.event.inputs.version && (startsWith(github.event.inputs.version, 'v') && github.event.inputs.version || format('v{0}', github.event.inputs.version))) || github.ref }}
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Record checked-out source commit - name: Checkout gh-pages branch
run: echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
- name: Download current R2 repository
run: | run: |
aws --version git clone --branch gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages-repo || {
bash .github/scripts/publish-void-r2.sh download echo "⚠️ gh-pages branch not found or empty, initializing a new one..."
mkdir gh-pages-repo
cd gh-pages-repo
git init
git checkout -b gh-pages
git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
cd ..
}
- name: Install XBPS Static Utilities - name: Install XBPS Static Utilities
run: | run: |
@@ -101,6 +67,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)
@@ -159,71 +126,45 @@ jobs:
- name: Configure repositories - name: Configure repositories
run: | run: |
# Append the repository to repos-remote templates so xbps-src translates it automatically # Append the repository to repos-remote templates so xbps-src translates it automatically
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote.conf echo "repository=https://avengemedia.github.io/DankLinux/current" >> void-packages/etc/xbps.d/repos-remote.conf
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf echo "repository=https://avengemedia.github.io/DankLinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
# Add any existing compiled packages to the build cache directory to avoid rebuilds # Add any existing compiled packages to the build cache directory to avoid rebuilds
if [ -d "r2-repo/current" ]; then if [ -d "gh-pages-repo/current" ]; then
mkdir -p void-packages/hostdir/binpkgs mkdir -p void-packages/hostdir/binpkgs
cp -L r2-repo/current/*.xbps void-packages/hostdir/binpkgs/ 2>/dev/null || true cp -L gh-pages-repo/current/*.xbps void-packages/hostdir/binpkgs/ 2>/dev/null || true
xbps-rindex -a void-packages/hostdir/binpkgs/*.xbps 2>/dev/null || true xbps-rindex -a void-packages/hostdir/binpkgs/*.xbps 2>/dev/null || true
fi fi
- name: Build packages - name: Build packages
run: | run: |
mkdir -p r2-repo/current mkdir -p gh-pages-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
RELEASE_TAG=""
if [ "${{ github.event_name }}" = "release" ]; then
RELEASE_TAG="${{ github.event.release.tag_name }}"
elif [ -n "${{ github.event.inputs.version || '' }}" ]; then
RELEASE_TAG="${{ github.event.inputs.version }}"
elif [ "${{ github.ref_type }}" = "tag" ]; then
RELEASE_TAG="${{ github.ref_name }}"
fi
if [ -n "$RELEASE_TAG" ]; then
RELEASE_VER="${RELEASE_TAG#v}"
ARCHIVE_TAG="v${RELEASE_VER}"
echo "Stable release tag: $ARCHIVE_TAG"
else
RELEASE_VER=""
ARCHIVE_TAG=""
echo "Stable release tag: (template version)"
fi
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
echo "🔧 Updating stable template for $ARCHIVE_TAG"
TARBALL="$(mktemp)"
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
rm -f "$TARBALL"
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
fi
# 1. Build dms-git (development package) # 1. Build dms-git (development package)
if [ "$BUILD_GIT" = "true" ]; then if [ "$BUILD_GIT" = "true" ]; then
# Calculate dynamic git version (tag.commits.hash) # Calculate dynamic git version (tag.commits.hash)
@@ -235,9 +176,9 @@ jobs:
# into $wrksrc (create_wrksrc=yes handles the rest). # into $wrksrc (create_wrksrc=yes handles the rest).
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}" SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
mkdir -p "$SRC_CACHE" mkdir -p "$SRC_CACHE"
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \ tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
--exclude=void-packages \ --exclude=void-packages \
--exclude=r2-repo \ --exclude=gh-pages-repo \
--exclude=.git \ --exclude=.git \
--exclude=danklinux \ --exclude=danklinux \
-C .. . -C .. .
@@ -251,13 +192,13 @@ jobs:
EXPECTED_GIT_FILE="dms-git-${GIT_VER}_1.x86_64.xbps" EXPECTED_GIT_FILE="dms-git-${GIT_VER}_1.x86_64.xbps"
if [ -f "../r2-repo/current/$EXPECTED_GIT_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then if [ -f "../gh-pages-repo/current/$EXPECTED_GIT_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_GIT_FILE already exists, skipping build." echo "✅ $EXPECTED_GIT_FILE already exists, skipping build."
else else
echo "🔨 Compiling dms-git..." echo "🔨 Compiling dms-git..."
./xbps-src pkg dms-git ./xbps-src pkg dms-git
rm -f "../r2-repo/current/${EXPECTED_GIT_FILE}" rm -f "../gh-pages-repo/current/${EXPECTED_GIT_FILE}"
cp -L "hostdir/binpkgs/${EXPECTED_GIT_FILE}" ../r2-repo/current/ cp -L hostdir/binpkgs/dms-git-*.xbps ../gh-pages-repo/current/
fi fi
fi fi
@@ -267,21 +208,38 @@ jobs:
STABLE_REV=$(grep -E '^revision=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"') STABLE_REV=$(grep -E '^revision=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
EXPECTED_DMS_FILE="dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps" EXPECTED_DMS_FILE="dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps"
if [ -f "../r2-repo/current/$EXPECTED_DMS_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then if [ -f "../gh-pages-repo/current/$EXPECTED_DMS_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
echo "✅ $EXPECTED_DMS_FILE already exists, skipping build." echo "✅ $EXPECTED_DMS_FILE already exists, skipping build."
else else
echo "🔨 Compiling dms ($STABLE_VER)..." echo "🔨 Compiling dms ($STABLE_VER)..."
./xbps-src pkg dms ./xbps-src pkg dms
rm -f "../r2-repo/current/${EXPECTED_DMS_FILE}" rm -f "../gh-pages-repo/current/${EXPECTED_DMS_FILE}"
cp -L hostdir/binpkgs/dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps ../r2-repo/current/ cp -L hostdir/binpkgs/dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps ../gh-pages-repo/current/
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 "../gh-pages-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 "../gh-pages-repo/current/${EXPECTED_GREETER_FILE}"
cp -L hostdir/binpkgs/dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps ../gh-pages-repo/current/
fi fi
fi fi
- name: Index and sign repository - name: Index and sign repository
run: | run: |
cd r2-repo/current cd gh-pages-repo/current
rm -f *.sig # Clean up any stale or dangling signature files to prevent O_CREAT ENOENT errors
rm -f *.sig2 *.sig
# Guard: nothing to index if no .xbps files exist # Guard: nothing to index if no .xbps files exist
if ! ls *.xbps 1>/dev/null 2>&1; then if ! ls *.xbps 1>/dev/null 2>&1; then
@@ -291,14 +249,6 @@ jobs:
# Regenerate repo index # Regenerate repo index
xbps-rindex -a $(pwd)/*.xbps xbps-rindex -a $(pwd)/*.xbps
xbps-rindex --remove-obsoletes $(pwd)
# Remove signatures left behind by obsolete packages. Signatures for
# unchanged immutable packages are retained byte-for-byte.
for sig in *.sig2; do
[ -e "$sig" ] || break
[ -f "${sig%.sig2}" ] || rm -f "$sig"
done
# Sign repository # Sign repository
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
@@ -309,5 +259,10 @@ jobs:
rm -f /tmp/xbps_privkey.pem rm -f /tmp/xbps_privkey.pem
- name: Publish repository to R2 - name: Deploy to gh-pages branch
run: bash .github/scripts/publish-void-r2.sh publish run: |
cd gh-pages-repo
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add current/
git diff --quiet && git diff --staged --quiet || (git commit -m "Update XBPS packages [skip ci]" && git push origin gh-pages)
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps: steps:
- name: Create GitHub App token - name: Create GitHub App token
id: app_token id: app_token
uses: actions/create-github-app-token@v3 uses: actions/create-github-app-token@v1
with: with:
app-id: ${{ secrets.APP_ID }} app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
-5
View File
@@ -126,8 +126,3 @@ __pycache__
distro/void/temp/ distro/void/temp/
distro/void/hostdir/ distro/void/hostdir/
distro/void/masterdir*/ distro/void/masterdir*/
# Often gets built
core/dms
core/internal/shellembed/dist
-4
View File
@@ -1,4 +0,0 @@
[submodule "dank-qml-common"]
path = dank-qml-common
url = https://github.com/AvengeMedia/dank-qml-common.git
branch = master
-8
View File
@@ -36,14 +36,6 @@ repos:
language: system language: system
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$ files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
pass_filenames: false pass_filenames: false
- repo: local
hooks:
- id: i18n-term-variants
name: i18n term variants (no case/punctuation duplicates)
entry: python3 quickshell/translations/check_term_variants.py
language: system
files: ^quickshell/(.*\.qml|translations/(check_term_variants\.py|extract_translations\.py))$
pass_filenames: false
- repo: local - repo: local
hooks: hooks:
- id: no-console-in-qml - id: no-console-in-qml
-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
-57
View File
@@ -6,20 +6,6 @@ To contribute fork this repository, make your changes, and open a pull request.
## Setup ## Setup
Clone with submodules — the shared widget library ([dank-qml-common](https://github.com/AvengeMedia/dank-qml-common)) is vendored at `dank-qml-common/` and symlinked into `quickshell/DankCommon`:
```bash
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git
# or, in an existing clone:
git submodule update --init
```
To have `git pull` keep the submodule in sync automatically (moving it to the commit this repo points at, no separate `git submodule update` step), set:
```bash
git config submodule.recurse true
```
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks: Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
```bash ```bash
@@ -42,47 +28,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)
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
2. Commit and PR those changes in the `dank-qml-common` repo: `cd dank-qml-common && git switch -c my-change`, push, open the PR there.
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
The submodule URL in `.gitmodules` is HTTPS so CI and anonymous clones keep working. To push over SSH instead of being prompted for credentials, add a push rewrite to your git config — fetches stay HTTPS, pushes use SSH:
```bash
git config --global url."[email protected]:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/"
```
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
## VSCode Setup ## VSCode Setup
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on. This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
@@ -159,8 +104,6 @@ Text {
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated) Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
### GO (`core` directory) ### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go) 1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
+5 -22
View File
@@ -6,8 +6,8 @@ BINARY_NAME=dms
CORE_DIR=core CORE_DIR=core
BUILD_DIR=$(CORE_DIR)/bin BUILD_DIR=$(CORE_DIR)/bin
PREFIX ?= /usr/local PREFIX ?= /usr/local
INSTALL_DIR=$(DESTDIR)$(PREFIX)/bin INSTALL_DIR=$(PREFIX)/bin
DATA_DIR=$(DESTDIR)$(PREFIX)/share DATA_DIR=$(PREFIX)/share
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME)) USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
@@ -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
@@ -41,12 +35,6 @@ clean:
lint-qml: lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh @./quickshell/scripts/qmllint-entrypoints.sh
# Pull the latest dank-qml-common and pin it everywhere it is consumed
# (submodule pointer + nix flake input). Commit both in one change.
update-common:
git submodule update --remote --merge dank-qml-common
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
# Installation targets # Installation targets
install-bin: install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@@ -55,9 +43,8 @@ install-bin:
install-shell: install-shell:
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..." @echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
@test -e $(SHELL_DIR)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
@mkdir -p $(SHELL_INSTALL_DIR) @mkdir -p $(SHELL_INSTALL_DIR)
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/ @cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github @rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@echo "Shell files installed" @echo "Shell files installed"
@@ -72,17 +59,13 @@ 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
@sed 's|/usr/bin/dms|$(PREFIX)/bin/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service @sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
@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..."
+3 -18
View File
@@ -83,7 +83,7 @@ One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, o
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16. Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
**System Monitoring** **System Monitoring**
Real-time CPU, RAM, GPU metrics and temperatures built into the dms daemon (powered by [dgop](https://github.com/AvengeMedia/dgop)). Process list with search and management. Real-time CPU, RAM, GPU metrics and temperatures with [dgop](https://github.com/AvengeMedia/dgop). Process list with search and management.
**Powerful Launcher** **Powerful Launcher**
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins. Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
@@ -98,12 +98,10 @@ 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). DMS keeps Extend functionality with the [plugin registry](https://plugins.danklinux.com).
`~/.config/DankMaterialShell/plugins.lock.json` synchronized with managed plugin installs and
their exact Git commits, so the same plugins can be reproduced on another machine.
## Supported Compositors ## Supported Compositors
@@ -122,8 +120,6 @@ dms ipc call audio setvolume 50
dms ipc call wallpaper set /path/to/image.jpg dms ipc call wallpaper set /path/to/image.jpg
dms brightness list # List available displays dms brightness list # List available displays
dms plugins search # Browse plugin registry dms plugins search # Browse plugin registry
dms plugins lock # Refresh the portable plugin lockfile
dms plugins restore ~/plugins.lock.json
``` ```
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc) [Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
@@ -136,17 +132,6 @@ dms plugins restore ~/plugins.lock.json
- **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 TUI and Go library; its library powers the process list and dashboard widgets inside the dms daemon.
- [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:
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=DMS Status Notifier Watcher (early tray)
# The tray watcher starts in parallel with the compositor and owns org.kde.StatusNotifierWatcher before
# graphical-session.target, i.e. before any XDG autostart app launches.
PartOf=graphical-session.target
Before=graphical-session.target
[Service]
Type=dbus
BusName=org.kde.StatusNotifierWatcher
ExecStart=/usr/bin/dms tray-watcher
Restart=on-failure
RestartSec=1
Slice=session.slice
[Install]
WantedBy=graphical-session.target
+5
View File
@@ -93,6 +93,11 @@ linters:
- linters: - linters:
- errcheck - errcheck
text: "Error return value of `.+\\.AddMatchSignal` is not checked" text: "Error return value of `.+\\.AddMatchSignal` is not checked"
# Exclude wayland pkg from errcheck and ineffassign (generated code patterns)
- linters:
- errcheck
- ineffassign
path: pkg/go-wayland/
# Exclude proto pkg from ineffassign (generated protocol code) # Exclude proto pkg from ineffassign (generated protocol code)
- linters: - linters:
- ineffassign - ineffassign
+1 -1
View File
@@ -52,7 +52,7 @@ packages:
pkgname: mocks_version pkgname: mocks_version
interfaces: interfaces:
VersionFetcher: {} VersionFetcher: {}
github.com/AvengeMedia/dankgo/wayland/client: github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
config: config:
dir: internal/mocks/wlclient dir: internal/mocks/wlclient
pkgname: mocks_wlclient pkgname: mocks_wlclient
+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)"
+7 -10
View File
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
Command-line interface and daemon for shell management and system control. Command-line interface and daemon for shell management and system control.
**dankinstall** **dankinstall**
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags. Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
## System Integration ## System Integration
@@ -74,11 +74,11 @@ Custom IPC via unix socket (JSON API) for shell communication.
- `dms run [-d]` - Start shell (optionally as daemon) - `dms run [-d]` - Start shell (optionally as daemon)
- `dms restart` / `dms kill` - Manage running processes - `dms restart` / `dms kill` - Manage running processes
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.) - `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
- `dms plugins [install|browse|search|lock|restore]` - Plugin management and portable exact-revision lockfiles - `dms plugins [install|browse|search]` - Plugin management
- `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.
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
## Supported Distributions ## Supported Distributions
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives) Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
**Arch Linux** **Arch Linux**
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency. Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
@@ -206,7 +206,7 @@ Uses COPR repositories (`avengemedia/danklinux`, `avengemedia/dms`).
Requires PPA support. Most packages built from source (slow first install). Requires PPA support. Most packages built from source (slow first install).
**Debian** **Debian**
Debian 13+ (Trixie), testing, and sid. Stable quickshell from Debian (trixie-backports on 13). Other companions via OBS. Debian 13+ (Trixie). niri only, no Hyprland support. Builds from source.
**openSUSE** **openSUSE**
Most packages available in standard repos. Minimal building required. Most packages available in standard repos. Minimal building required.
@@ -214,7 +214,4 @@ Most packages available in standard repos. Minimal building required.
**Gentoo** **Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration. Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
**Void Linux**
Uses XBPS with the DMS and DankLinux self-hosted repositories.
See installer output for distribution-specific details during installation. See installer output for distribution-specific details during installation.
-8
View File
@@ -23,8 +23,6 @@ var (
replaceConfigs []string replaceConfigs []string
replaceConfigsAll bool replaceConfigsAll bool
yes bool yes bool
danksearch bool
dankcalendar bool
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -51,8 +49,6 @@ func init() {
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)") rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations") rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts") rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
rootCmd.Flags().BoolVar(&danksearch, "danksearch", false, "Install danksearch and enable its user indexing service")
rootCmd.Flags().BoolVar(&dankcalendar, "dankcalendar", false, "Install dankcalendar")
} }
func main() { func main() {
@@ -78,8 +74,6 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
"replace-configs", "replace-configs",
"replace-configs-all", "replace-configs-all",
"yes", "yes",
"danksearch",
"dankcalendar",
} }
var set []string var set []string
for _, name := range headlessOnly { for _, name := range headlessOnly {
@@ -115,8 +109,6 @@ func runHeadless() error {
ReplaceConfigs: replaceConfigs, ReplaceConfigs: replaceConfigs,
ReplaceConfigsAll: replaceConfigsAll, ReplaceConfigsAll: replaceConfigsAll,
Yes: yes, Yes: yes,
DankSearch: danksearch,
DankCalendar: dankcalendar,
} }
runner := headless.NewRunner(cfg) runner := headless.NewRunner(cfg)
+1 -166
View File
@@ -1,11 +1,7 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"os/exec"
"path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -16,7 +12,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{
@@ -39,142 +35,9 @@ var authSyncCmd = &cobra.Command{
}, },
} }
var authResolveLockCmd = &cobra.Command{
Use: "resolve-lock",
Short: "Generate the lock-screen PAM config from the system auth stack",
Long: "Resolve the distribution's PAM auth stack into a self-contained lock-screen config under the user state directory.\n" +
"Runs unprivileged (reads /etc/pam.d, writes to the user's state dir) and is used by the shell as a fallback when /etc/pam.d/dankshell is not managed.\n" +
"Prints the path of the generated file.",
Run: func(cmd *cobra.Command, args []string) {
quiet, _ := cmd.Flags().GetBool("quiet")
logFunc := func(msg string) {
if !quiet {
fmt.Println(msg)
}
}
path, err := sharedpam.WriteUserLockscreenPamConfig(logFunc)
if err != nil {
log.Fatalf("Error resolving lock-screen PAM config: %v", err)
}
fmt.Println(path)
},
}
var authListServicesCmd = &cobra.Command{
Use: "list-services",
Short: "List candidate lock-screen PAM services available on this system",
Long: "Enumerate the lock-screen PAM services that exist on this system and report their resolved auth stack (whether it has an auth directive and whether fingerprint/U2F modules appear inline).",
Run: func(cmd *cobra.Command, args []string) {
asJSON, _ := cmd.Flags().GetBool("json")
services := sharedpam.ListLockscreenPamServices()
if asJSON {
payload := struct {
Services []sharedpam.LockscreenPamServiceInfo `json:"services"`
}{Services: services}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("Error encoding services: %v", err)
}
fmt.Println(string(data))
return
}
if len(services) == 0 {
fmt.Println("No candidate lock-screen PAM services found.")
return
}
for _, s := range services {
fmt.Printf("%-20s %-30s auth=%-5t fingerprint=%-5t u2f=%t\n", s.Name, s.Path, s.HasAuth, s.InlineFingerprint, s.InlineU2f)
}
},
}
var authValidateCmd = &cobra.Command{
Use: "validate",
Short: "Validate a PAM service file for use by the DMS lock screen",
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password or dedicated U2F stack. Exits 1 when the file is not usable.",
Run: func(cmd *cobra.Command, args []string) {
path, _ := cmd.Flags().GetString("path")
service, _ := cmd.Flags().GetString("service")
purpose, _ := cmd.Flags().GetString("purpose")
asJSON, _ := cmd.Flags().GetBool("json")
if (path == "") == (service == "") {
log.Fatalf("Error: exactly one of --path or --service is required")
}
if purpose != "password" && purpose != "u2f" {
log.Fatalf("Error: --purpose must be password or u2f")
}
var result sharedpam.LockscreenPamValidation
switch {
case service != "":
if purpose == "u2f" {
result = sharedpam.ValidateLockscreenU2fPamService(service)
} else {
result = sharedpam.ValidateLockscreenPamService(service)
}
case !filepath.IsAbs(path):
result = sharedpam.LockscreenPamValidation{
Path: path,
MissingModules: []string{},
Warnings: []string{},
Errors: []string{"--path must be an absolute file path"},
}
default:
if purpose == "u2f" {
result = sharedpam.ValidateLockscreenU2fPamPath(path)
} else {
result = sharedpam.ValidateLockscreenPamPath(path)
}
}
if asJSON {
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("Error encoding validation: %v", err)
}
fmt.Println(string(data))
} else {
printLockscreenPamValidation(result)
}
if !result.Valid {
os.Exit(1)
}
},
}
func printLockscreenPamValidation(result sharedpam.LockscreenPamValidation) {
fmt.Printf("Path: %s\n", result.Path)
fmt.Printf("Valid: %t\n", result.Valid)
fmt.Printf("Has auth: %t\n", result.HasAuth)
fmt.Printf("Inline fingerprint: %t\n", result.InlineFingerprint)
fmt.Printf("Inline U2F: %t\n", result.InlineU2f)
if len(result.MissingModules) > 0 {
fmt.Printf("Missing modules: %s\n", strings.Join(result.MissingModules, ", "))
}
for _, w := range result.Warnings {
fmt.Println("⚠ " + w)
}
for _, e := range result.Errors {
fmt.Println("✗ " + e)
}
}
func init() { func init() {
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts") authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)") authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
authListServicesCmd.Flags().Bool("json", false, "Output as JSON")
authValidateCmd.Flags().String("path", "", "Absolute path to a PAM service file to validate")
authValidateCmd.Flags().String("service", "", "Name of a PAM service to resolve across the system PAM dirs")
authValidateCmd.Flags().String("purpose", "password", "Validation purpose: password or u2f")
authValidateCmd.Flags().Bool("json", false, "Output as JSON")
} }
func syncAuth(nonInteractive bool) error { func syncAuth(nonInteractive bool) error {
@@ -212,31 +75,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)")
}
-62
View File
@@ -1,62 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/backup"
"github.com/spf13/cobra"
)
var backupOutputPath string
var backupCmd = &cobra.Command{
Use: "backup",
Short: "Backup and restore DMS configuration",
}
var backupCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a tar.gz backup of ~/.config/DankMaterialShell (settings, plugins, themes)",
Args: cobra.NoArgs,
Run: runBackupCreate,
}
var backupRestoreCmd = &cobra.Command{
Use: "restore <archive>",
Short: "Restore a DMS configuration backup; the current configuration is moved aside first",
Args: cobra.ExactArgs(1),
Run: runBackupRestore,
}
func init() {
backupCreateCmd.Flags().StringVarP(&backupOutputPath, "output", "o", "", "Output archive path (default dms-backup-<timestamp>.tar.gz)")
backupCmd.AddCommand(backupCreateCmd, backupRestoreCmd)
rootCmd.AddCommand(backupCmd)
}
func runBackupCreate(cmd *cobra.Command, args []string) {
output := backupOutputPath
if output == "" {
output = backup.DefaultArchiveName()
}
if err := backup.Create(output); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(output)
}
func runBackupRestore(cmd *cobra.Command, args []string) {
previous, err := backup.Restore(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if previous != "" {
fmt.Printf("Previous configuration moved to %s\n", previous)
}
fmt.Println("Restore complete. Restart the shell to apply.")
}
+8 -9
View File
@@ -31,7 +31,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"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"
"github.com/AvengeMedia/dankgo/wlclipboard"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -284,8 +283,8 @@ func runClipCopy(cmd *cobra.Command, args []string) {
} }
} }
func parseMultiOffers(data []byte) ([]wlclipboard.Offer, error) { func parseMultiOffers(data []byte) ([]clipboard.Offer, error) {
var offers []wlclipboard.Offer var offers []clipboard.Offer
pos := 0 pos := 0
for pos < len(data) { for pos < len(data) {
@@ -312,14 +311,14 @@ func parseMultiOffers(data []byte) ([]wlclipboard.Offer, error) {
offerData := data[pos : pos+dataLen] offerData := data[pos : pos+dataLen]
pos += dataLen pos += dataLen
offers = append(offers, wlclipboard.Offer{MimeType: mimeType, Data: offerData}) offers = append(offers, clipboard.Offer{MimeType: mimeType, Data: offerData})
} }
return offers, nil return offers, nil
} }
func runClipPaste(cmd *cobra.Command, args []string) { func runClipPaste(cmd *cobra.Command, args []string) {
data, _, err := wlclipboard.Paste() data, _, err := clipboard.Paste()
if err != nil { if err != nil {
log.Fatalf("paste: %v", err) log.Fatalf("paste: %v", err)
} }
@@ -345,13 +344,13 @@ func runClipWatch(cmd *cobra.Command, args []string) {
switch { switch {
case len(args) > 0: case len(args) > 0:
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) { if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
runCommand(args, data) runCommand(args, data)
}); err != nil && err != context.Canceled { }); err != nil && err != context.Canceled {
log.Fatalf("Watch error: %v", err) log.Fatalf("Watch error: %v", err)
} }
case clipWatchStore: case clipWatchStore:
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) { if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
if err := clipboard.Store(data, mimeType); err != nil { if err := clipboard.Store(data, mimeType); err != nil {
log.Errorf("store: %v", err) log.Errorf("store: %v", err)
} }
@@ -383,7 +382,7 @@ func runClipWatch(cmd *cobra.Command, args []string) {
log.Fatalf("Watch error: %v", err) log.Fatalf("Watch error: %v", err)
} }
case clipJSONOutput: case clipJSONOutput:
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) { if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
out := map[string]any{ out := map[string]any{
"data": string(data), "data": string(data),
"mimeType": mimeType, "mimeType": mimeType,
@@ -396,7 +395,7 @@ func runClipWatch(cmd *cobra.Command, args []string) {
log.Fatalf("Watch error: %v", err) log.Fatalf("Watch error: %v", err)
} }
default: default:
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) { if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
os.Stdout.Write(data) os.Stdout.Write(data)
os.Stdout.WriteString("\n") os.Stdout.WriteString("\n")
}); err != nil && err != context.Canceled { }); err != nil && err != context.Canceled {
+79 -98
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",
@@ -51,8 +108,6 @@ func init() {
}) })
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins") pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them") pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
pluginsLockCmd.Flags().StringP("output", "o", "", "Also write the lockfile to this path")
pluginsRestoreCmd.Flags().Bool("prune", false, "Remove managed plugins that are not in the lockfile")
} }
var debugSrvCmd = &cobra.Command{ var debugSrvCmd = &cobra.Command{
@@ -180,36 +235,6 @@ var pluginsUpdateCmd = &cobra.Command{
}, },
} }
var pluginsLockCmd = &cobra.Command{
Use: "lock",
Short: "Record installed plugins and their exact revisions",
Long: "Write a portable plugins.lock.json containing every managed user plugin and its current Git commit.",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
if err := lockPluginsCLI(output); err != nil {
log.Fatalf("Error writing plugin lockfile: %v", err)
}
},
}
var pluginsRestoreCmd = &cobra.Command{
Use: "restore [lockfile]",
Short: "Restore plugins from exact revisions in a lockfile",
Long: "Install or reset all plugins in a portable plugins.lock.json. Existing managed plugins are retained unless --prune is specified.",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
path := ""
if len(args) == 1 {
path = args[0]
}
prune, _ := cmd.Flags().GetBool("prune")
if err := restorePluginsCLI(path, prune); err != nil {
log.Fatalf("Error restoring plugins: %v", err)
}
},
}
func runVersion(cmd *cobra.Command, args []string) { func runVersion(cmd *cobra.Command, args []string) {
fmt.Printf("%s\n", formatVersion(Version)) fmt.Printf("%s\n", formatVersion(Version))
} }
@@ -239,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)
@@ -465,40 +478,6 @@ func installPluginCLI(idOrName string) error {
return nil return nil
} }
func lockPluginsCLI(outputPath string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
warnings, err := manager.WriteCurrentLockfile(outputPath)
if err != nil {
return err
}
for _, warning := range warnings {
fmt.Fprintf(os.Stderr, "Warning: %s\n", warning)
}
fmt.Printf("Plugin lockfile written: %s\n", manager.GetLockfilePath())
if outputPath != "" && outputPath != manager.GetLockfilePath() {
fmt.Printf("Plugin lockfile exported: %s\n", outputPath)
}
return nil
}
func restorePluginsCLI(path string, prune bool) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
if err := manager.RestoreFromLockfile(path, prune); err != nil {
return err
}
if path == "" {
path = manager.GetLockfilePath()
}
fmt.Printf("Plugins restored from lockfile: %s\n", path)
return nil
}
func getAvailablePluginIDs() []string { func getAvailablePluginIDs() []string {
registry, err := plugins.NewRegistry() registry, err := plugins.NewRegistry()
if err != nil { if err != nil {
@@ -764,13 +743,15 @@ 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,
registryCmd,
dank16Cmd, dank16Cmd,
brightnessCmd, brightnessCmd,
dpmsCmd, dpmsCmd,
@@ -778,7 +759,6 @@ func getCommonCommands() []*cobra.Command {
greeterCmd, greeterCmd,
setupCmd, setupCmd,
colorCmd, colorCmd,
qrCmd,
screenshotCmd, screenshotCmd,
notifyActionCmd, notifyActionCmd,
notifyCmd, notifyCmd,
@@ -794,5 +774,6 @@ func getCommonCommands() []*cobra.Command {
trashCmd, trashCmd,
systemCmd, systemCmd,
switchUserCmd, switchUserCmd,
}...) trayWatcherCmd,
}
} }
-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",
+87 -191
View File
@@ -320,7 +320,6 @@ func checkEnvironmentVars() []checkResult {
var results []checkResult var results []checkResult
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...) results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
results = append(results, checkEnvVar("QS_ICON_THEME")...) results = append(results, checkEnvVar("QS_ICON_THEME")...)
results = append(results, checkXDGMenuPrefix()...)
return results return results
} }
@@ -335,29 +334,6 @@ func checkEnvVar(name string) []checkResult {
return nil return nil
} }
func checkXDGMenuPrefix() []checkResult {
menuPrefix := os.Getenv("XDG_MENU_PREFIX")
if menuPrefix != "" {
if checkXDGMenuFile(menuPrefix) {
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, menuPrefix, "", doctorDocsURL + "#xdg-menu-prefix"}}
}
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, fmt.Sprintf("%s (menu file not found)", menuPrefix), fmt.Sprintf("Dolphin 'Open with…' dialog may be empty. Ensure /etc/xdg/menus/%sapplications.menu exists.", menuPrefix), doctorDocsURL + "#xdg-menu-prefix"}}
}
if _, err := exec.LookPath("keditfiletype"); err == nil {
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, "Not set", "Dolphin file associations and 'Open with…' dialog may be empty. Set XDG_MENU_PREFIX=plasma- in your compositor's environment block.", doctorDocsURL + "#xdg-menu-prefix"}}
}
if doctorVerbose {
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, "Not set", "", doctorDocsURL + "#xdg-menu-prefix"}}
}
return nil
}
func checkXDGMenuFile(prefix string) bool {
menuPath := fmt.Sprintf("/etc/xdg/menus/%sapplications.menu", prefix)
_, err := os.Stat(menuPath)
return err == nil
}
func readOSRelease() map[string]string { func readOSRelease() map[string]string {
result := make(map[string]string) result := make(map[string]string)
data, err := os.ReadFile("/etc/os-release") data, err := os.ReadFile("/etc/os-release")
@@ -401,12 +377,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 {
@@ -449,7 +425,12 @@ func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
func checkDMSInstallation() []checkResult { func checkDMSInstallation() []checkResult {
var results []checkResult var results []checkResult
dmsPath := resolveDoctorShellPath() dmsPath := ""
if err := findConfig(nil, nil); err == nil && configPath != "" {
dmsPath = configPath
} else if path, err := config.LocateDMSConfig(); err == nil {
dmsPath = path
}
if dmsPath == "" { if dmsPath == "" {
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}} return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
@@ -828,8 +809,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 ""
} }
@@ -903,10 +882,10 @@ func checkOptionalDependencies() []checkResult {
important bool important bool
}{ }{
{"matugen", "matugen", "Dynamic theming", true}, {"matugen", "matugen", "Dynamic theming", true},
{"dgop", "dgop", "System monitoring", true},
{"cava", "cava", "Audio visualizer", true}, {"cava", "cava", "Audio visualizer", true},
{"khal", "khal", "Calendar events", false}, {"khal", "khal", "Calendar events", false},
{"danksearch", "dsearch", "File search", false}, {"danksearch", "dsearch", "File search", false},
{"dankcalendar", "dcal", "Calendar app", false},
{"fprintd", "fprintd-list", "Fingerprint auth", false}, {"fprintd", "fprintd-list", "Fingerprint auth", false},
} }
@@ -1163,182 +1142,99 @@ func formatResultsPlain(results []checkResult) string {
return sb.String() return sb.String()
} }
const (
defaultDoctorFontFamily = "Inter Variable"
defaultDoctorMonoFontFamily = "Fira Code"
)
// bundledFontRelPaths maps settings/default family names to font files shipped with
// the shell and loaded via Qt FontLoader (not registered with fontconfig).
var bundledFontRelPaths = map[string][]string{
"inter variable": {
"DankCommon/assets/fonts/inter/InterVariable.ttf",
"assets/fonts/inter/InterVariable.ttf",
},
"fira code": {
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
},
"firacode nerd font": {
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
},
}
func resolveDoctorShellPath() string {
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
return shellApp.ConfigPath()
}
if path, err := config.LocateDMSConfig(); err == nil {
return path
}
return ""
}
func findBundledFontFile(shellPath, family string) string {
if shellPath == "" {
return ""
}
relPaths, ok := bundledFontRelPaths[strings.ToLower(strings.TrimSpace(family))]
if !ok {
return ""
}
for _, rel := range relPaths {
path := filepath.Join(shellPath, rel)
if info, err := os.Stat(path); err == nil && !info.IsDir() {
return path
}
}
return ""
}
func isBundledDefaultFont(family string) bool {
_, ok := bundledFontRelPaths[strings.ToLower(strings.TrimSpace(family))]
return ok
}
func fontInFcList(name, cacheLower string) bool {
target := strings.ToLower(strings.TrimSpace(name))
if target == "" {
return false
}
for _, line := range strings.Split(cacheLower, "\n") {
for _, fam := range strings.Split(strings.TrimSpace(line), ",") {
if strings.TrimSpace(fam) == target {
return true
}
}
}
return false
}
func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailable bool, url string) checkResult {
if bundled := findBundledFontFile(shellPath, family); bundled != "" {
details := "Bundled (Qt FontLoader)"
if doctorVerbose {
details = bundled
}
return checkResult{catFonts, label, statusOK, family, details, url}
}
if isBundledDefaultFont(family) {
if shellPath == "" {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not verified", family),
"Could not locate shell config to verify bundled font files.",
url,
}
}
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' bundled file missing", family),
"Expected font file missing from shell install. Reinstall DMS or check DankCommon assets.",
url,
}
}
if !fcListAvailable {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not verified", family),
"fc-list not installed; cannot verify custom fonts in fontconfig.",
url,
}
}
if fcCache == "" {
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not found", family),
"Fontconfig cache is empty or unreadable. Try running 'fc-cache -fv'.",
url,
}
}
if fontInFcList(family, fcCache) {
return checkResult{catFonts, label, statusOK, family, "Available via fontconfig", url}
}
return checkResult{
catFonts, label, statusWarn,
fmt.Sprintf("'%s' not found", family),
"Font is not registered with fontconfig. Try running 'fc-cache -fv' or install the font.",
url,
}
}
func checkFonts() []checkResult { func checkFonts() []checkResult {
var results []checkResult var results []checkResult
url := doctorDocsURL + "#fonts" url := doctorDocsURL + "#fonts"
fontFamily := defaultDoctorFontFamily configDir, err := os.UserConfigDir()
monoFontFamily := defaultDoctorMonoFontFamily if err != nil {
return nil
}
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
if configDir, err := os.UserConfigDir(); err == nil { fontFamily := "Inter Variable"
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json") monoFontFamily := "Fira Code"
if data, err := os.ReadFile(settingsPath); err == nil {
var settings struct { if data, err := os.ReadFile(settingsPath); err == nil {
FontFamily string `json:"fontFamily"` var settings struct {
MonoFontFamily string `json:"monoFontFamily"` FontFamily string `json:"fontFamily"`
MonoFontFamily string `json:"monoFontFamily"`
}
if err := json.Unmarshal(data, &settings); err == nil {
if settings.FontFamily != "" {
fontFamily = settings.FontFamily
} }
if err := json.Unmarshal(data, &settings); err == nil { if settings.MonoFontFamily != "" {
if settings.FontFamily != "" { monoFontFamily = settings.MonoFontFamily
fontFamily = settings.FontFamily
}
if settings.MonoFontFamily != "" {
monoFontFamily = settings.MonoFontFamily
}
} }
} }
} }
shellPath := resolveDoctorShellPath() if !utils.CommandExists("fc-list") {
needFontconfig := !isBundledDefaultFont(fontFamily) || !isBundledDefaultFont(monoFontFamily) results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify if fonts are cached.", url})
return results
}
fcListAvailable := utils.CommandExists("fc-list") // Retrieve font list
fcCache := "" output, err := exec.Command("fc-list", ":", "family").Output()
if err != nil {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url})
return results
}
if needFontconfig { outStr := string(output)
if !fcListAvailable { if len(strings.TrimSpace(outStr)) == 0 {
results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify custom fonts in fontconfig cache.", url}) results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url})
} else { return results
output, err := exec.Command("fc-list", ":", "family").Output() }
if err != nil {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url}) lowerFonts := strings.ToLower(outStr)
} else {
fcCache = strings.ToLower(string(output)) // Helper to check if a font exists
if len(strings.TrimSpace(fcCache)) == 0 { hasFont := func(name string) bool {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url}) target := strings.ToLower(strings.TrimSpace(name))
if target == "" {
return false
}
for _, line := range strings.Split(lowerFonts, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Each line can have comma-separated families
families := strings.Split(line, ",")
for _, fam := range families {
if strings.TrimSpace(fam) == target {
return true
} }
} }
} }
return false
} }
results = append(results, // Normal Font Check
checkConfiguredFont("Normal Font", fontFamily, shellPath, fcCache, fcListAvailable, url), if hasFont(fontFamily) {
checkConfiguredFont("Monospace Font", monoFontFamily, shellPath, fcCache, fcListAvailable, url), results = append(results, checkResult{catFonts, "Normal Font", statusOK, fontFamily, "Available", url})
) } else {
results = append(results, checkResult{
catFonts, "Normal Font", statusWarn,
fmt.Sprintf("'%s' not found", fontFamily),
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
url,
})
}
// Monospace Font Check
if hasFont(monoFontFamily) {
results = append(results, checkResult{catFonts, "Monospace Font", statusOK, monoFontFamily, "Available", url})
} else {
results = append(results, checkResult{
catFonts, "Monospace Font", statusWarn,
fmt.Sprintf("'%s' not found", monoFontFamily),
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
url,
})
}
return results return results
} }
+3 -22
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 {
@@ -250,10 +250,6 @@ func updateOtherDistros() error {
return fmt.Errorf("failed to fetch changes: %w", err) return fmt.Errorf("failed to fetch changes: %w", err)
} }
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
if currentTag != "" { if currentTag != "" {
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname") latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
latestTagOutput, err := latestTagCmd.Output() latestTagOutput, err := latestTagCmd.Output()
@@ -295,10 +291,6 @@ func updateOtherDistros() error {
return fmt.Errorf("update cancelled") return fmt.Errorf("update cancelled")
} }
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag) fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
return nil return nil
} }
@@ -328,21 +320,10 @@ func updateOtherDistros() error {
return fmt.Errorf("update cancelled") return fmt.Errorf("update cancelled")
} }
if err := updateSubmodules(); err != nil {
return fmt.Errorf("failed to update submodules: %w", err)
}
fmt.Println("\nUpdate complete!") fmt.Println("\nUpdate complete!")
return nil return nil
} }
func updateSubmodules() error {
submoduleCmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
submoduleCmd.Stdout = os.Stdout
submoduleCmd.Stderr = os.Stderr
return submoduleCmd.Run()
}
func offerReclone(dmsPath string) bool { func offerReclone(dmsPath string) bool {
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ") fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@@ -361,7 +342,7 @@ func offerReclone(dmsPath string) bool {
} }
fmt.Println("Cloning fresh copy...") fmt.Println("Cloning fresh copy...")
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath) cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd.Stdout = os.Stdout cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr cloneCmd.Stderr = os.Stderr
if err := cloneCmd.Run(); err != nil { if err := cloneCmd.Run(); err != nil {
File diff suppressed because it is too large Load Diff
+143
View File
@@ -0,0 +1,143 @@
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 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)
}
}
-265
View File
@@ -1,265 +0,0 @@
package main
import (
"fmt"
"io"
"os"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/qrcode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
)
var (
qrEcc string
qrVersion int
qrOutput string
qrStdout bool
qrClipboard bool
qrCopyText bool
qrRender bool
qrNoRender bool
qrInvert bool
qrQuietZone int
qrModuleSize int
qrFg string
qrBg string
qrTransparent bool
qrBorder int
qrShape string
qrLogo string
qrLogoScale int
qrWifiPassword string
qrWifiSecurity string
qrWifiHidden bool
)
var qrCmd = &cobra.Command{
Use: "qr [text]",
Short: "Generate QR codes",
Long: `Generate a QR code from text, stdin, or a WiFi network.
By default the code is rendered to the terminal when stdout is a TTY, or
written as PNG bytes to stdout when piped. Use flags to also copy to the
clipboard, save a PNG, or tune encoding and colors.
Input:
dms qr "https://example.com" # encode an argument
echo -n "data" | dms qr # encode stdin
dms qr - # encode stdin explicitly
Output (combine freely):
dms qr "text" --clipboard # copy PNG image to clipboard
dms qr "text" --copy-text # copy the source text to clipboard
dms qr "text" -o code.png # save a PNG file
dms qr "text" > code.png # PNG to stdout (piped)
dms qr "text" --render # force terminal render
Encoding & style:
--ecc L|M|Q|H # error correction (default M)
--qr-version 10 # force symbol version (1-40)
--module-size 12 --fg '#000' ... # PNG sizing and colors
--shape circle # round modules
--logo icon.png # center logo (bumps --ecc to H)
--invert # flip colors for light terminals
WiFi:
dms qr wifi MySSID -p secret # build from an explicit password
dms qr wifi MySSID # pull the saved secret from the shell`,
Args: cobra.ArbitraryArgs,
Run: runQR,
}
var qrWifiCmd = &cobra.Command{
Use: "wifi <ssid>",
Short: "Generate a WiFi QR code",
Long: `Generate a QR code that joins a WiFi network when scanned.
With --password the code is built entirely offline. Without it, the saved
credentials are fetched from the running DMS shell (like the network panel).`,
Args: cobra.ExactArgs(1),
Run: runQRWifi,
}
func init() {
qrCmd.PersistentFlags().StringVar(&qrEcc, "ecc", "", "Error correction level: L, M, Q, H (default M, or H with --logo)")
qrCmd.PersistentFlags().IntVar(&qrVersion, "qr-version", 0, "Force QR symbol version 1-40 (0 = auto)")
qrCmd.PersistentFlags().StringVarP(&qrOutput, "output", "o", "", "Write a PNG to this file")
qrCmd.PersistentFlags().BoolVar(&qrStdout, "stdout", false, "Write PNG bytes to stdout")
qrCmd.PersistentFlags().BoolVar(&qrClipboard, "clipboard", false, "Copy the PNG image to the clipboard")
qrCmd.PersistentFlags().BoolVar(&qrCopyText, "copy-text", false, "Copy the source text to the clipboard")
qrCmd.PersistentFlags().BoolVar(&qrRender, "render", false, "Force terminal rendering")
qrCmd.PersistentFlags().BoolVar(&qrNoRender, "no-render", false, "Never render to the terminal")
qrCmd.PersistentFlags().BoolVar(&qrInvert, "invert", false, "Swap colors (for light terminals)")
qrCmd.PersistentFlags().IntVar(&qrQuietZone, "quiet-zone", 2, "Terminal margin in modules")
qrCmd.PersistentFlags().IntVar(&qrModuleSize, "module-size", 0, "PNG pixels per module (0 = auto)")
qrCmd.PersistentFlags().StringVar(&qrFg, "fg", "", "Dark module color (#RGB or #RRGGBB)")
qrCmd.PersistentFlags().StringVar(&qrBg, "bg", "", "Light module color (#RGB or #RRGGBB)")
qrCmd.PersistentFlags().BoolVar(&qrTransparent, "transparent", false, "Transparent PNG background")
qrCmd.PersistentFlags().IntVar(&qrBorder, "border", -1, "PNG border in pixels (-1 = auto)")
qrCmd.PersistentFlags().StringVar(&qrShape, "shape", "square", "PNG module shape (square, circle)")
qrCmd.PersistentFlags().StringVar(&qrLogo, "logo", "", "Center a PNG/JPEG logo on the PNG output")
qrCmd.PersistentFlags().IntVar(&qrLogoScale, "logo-scale", 0, "Max logo size as 1/N of the code (0 = library default of 5)")
qrWifiCmd.Flags().StringVarP(&qrWifiPassword, "password", "p", "", "WiFi password (offline build)")
qrWifiCmd.Flags().StringVar(&qrWifiSecurity, "security", "WPA", "Security type (WPA, WEP, nopass)")
qrWifiCmd.Flags().BoolVar(&qrWifiHidden, "hidden", false, "Mark the network as hidden")
qrCmd.AddCommand(qrWifiCmd)
}
func runQR(cmd *cobra.Command, args []string) {
text := strings.Join(args, " ")
if text == "" || text == "-" {
data, err := io.ReadAll(os.Stdin)
if err != nil {
fatalf("Error reading stdin: %v", err)
}
text = strings.TrimRight(string(data), "\n")
}
if text == "" {
fatalf("Error: no input (provide text, pipe stdin, or use a subcommand)")
}
emitQR(text)
}
func runQRWifi(cmd *cobra.Command, args []string) {
ssid := args[0]
if qrWifiPassword != "" || strings.EqualFold(qrWifiSecurity, "nopass") {
emitQR(qrcode.WiFiString(qrWifiSecurity, ssid, qrWifiPassword, qrWifiHidden))
return
}
content, err := fetchWifiQRContent(ssid)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
fmt.Fprintln(os.Stderr, "Hint: pass --password to build the code without the shell.")
os.Exit(1)
}
if qrWifiHidden {
content = strings.TrimSuffix(content, ";") + "H:true;;"
}
emitQR(content)
}
func emitQR(text string) {
renderTerm := shouldRenderTerminal()
pngToStdout := qrStdout || (!renderTerm && qrOutput == "" && !qrClipboard && !qrCopyText)
if pngToStdout || qrOutput != "" || qrClipboard {
png, err := qrcode.RenderPNG(text, qrcode.ImageOptions{
ECC: effectiveEcc(),
Version: qrVersion,
ModuleSize: qrModuleSize,
Fg: qrFg,
Bg: qrBg,
Transparent: qrTransparent,
Border: qrBorder,
Shape: qrShape,
Logo: qrLogo,
LogoScale: qrLogoScale,
})
if err != nil {
fatalf("Error encoding QR: %v", err)
}
emitPNG(png, pngToStdout)
}
if qrCopyText {
if err := clipboard.CopyText(text); err != nil {
fatalf("Error copying text: %v", err)
}
}
if !renderTerm {
return
}
out, err := qrcode.RenderTerminal(text, qrcode.TermOptions{
ECC: effectiveEcc(),
Version: qrVersion,
QuietZone: qrQuietZone,
Invert: qrInvert,
Fg: qrFg,
Bg: qrBg,
})
if err != nil {
fatalf("Error rendering QR: %v", err)
}
dst := os.Stdout
if pngToStdout {
dst = os.Stderr
}
fmt.Fprint(dst, out)
}
func emitPNG(png []byte, toStdout bool) {
if qrOutput != "" {
if err := os.WriteFile(qrOutput, png, 0o644); err != nil {
fatalf("Error writing file: %v", err)
}
fmt.Fprintln(os.Stderr, qrOutput)
}
if toStdout {
os.Stdout.Write(png)
}
if qrClipboard {
if err := clipboard.Copy(png, "image/png"); err != nil {
fatalf("Error copying image: %v", err)
}
}
}
func shouldRenderTerminal() bool {
switch {
case qrNoRender:
return false
case qrRender:
return true
case qrStdout, qrOutput != "", qrClipboard, qrCopyText:
return false
default:
return isatty.IsTerminal(os.Stdout.Fd())
}
}
func effectiveEcc() string {
switch {
case qrEcc != "":
return qrEcc
case qrLogo != "":
return "H"
default:
return "M"
}
}
func fatalf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
func fetchWifiQRContent(ssid string) (string, error) {
resp, err := sendServerRequest(models.Request{
ID: 1,
Method: "network.qrcode-content",
Params: map[string]any{"ssid": ssid},
})
if err != nil {
return "", err
}
if resp.Error != "" {
return "", fmt.Errorf("%s", resp.Error)
}
if resp.Result == nil {
return "", fmt.Errorf("empty response")
}
content, ok := (*resp.Result).(string)
if !ok {
return "", fmt.Errorf("unexpected response format")
}
return content, nil
}
-67
View File
@@ -1,67 +0,0 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/registries"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
var registryCmd = &cobra.Command{
Use: "registry",
Short: "Manage plugin and theme registries",
Long: "Manage the registries DMS fetches plugins and themes from. The official registry is always active; additional registries can be added by name and git URL.",
}
var registryListCmd = &cobra.Command{
Use: "list",
Short: "List configured registries",
Run: func(cmd *cobra.Command, args []string) {
for _, s := range registries.Load(afero.NewOsFs()) {
suffix := ""
if s.Official() {
suffix = " (official)"
}
fmt.Printf("%s%s\n %s\n", s.Name, suffix, s.URL)
}
},
}
var registryAddCmd = &cobra.Command{
Use: "add <name> <url>",
Short: "Add a registry",
Long: "Add a registry by name and git URL. The repository must contain a plugins/ or themes/ directory in the registry format.",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
if err := registries.Add(afero.NewOsFs(), args[0], args[1]); err != nil {
log.Fatalf("Error adding registry: %v", err)
}
fmt.Printf("Registry added: %s\n", args[0])
},
}
var registryRemoveCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove a registry",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var names []string
for _, s := range registries.Load(afero.NewOsFs()) {
if !s.Official() {
names = append(names, s.Name)
}
}
return names, cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := registries.Remove(afero.NewOsFs(), args[0]); err != nil {
log.Fatalf("Error removing registry: %v", err)
}
fmt.Printf("Registry removed: %s\n", args[0])
},
}
+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
} }
+17 -111
View File
@@ -2,7 +2,6 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -28,19 +27,8 @@ var (
ssNoConfirm bool ssNoConfirm bool
ssReset bool ssReset bool
ssStdout bool ssStdout bool
ssJSON bool
) )
type screenshotMetadata struct {
Status string `json:"status"`
Path string `json:"path,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Scale float64 `json:"scale,omitempty"`
Mime string `json:"mime,omitempty"`
Error string `json:"error,omitempty"`
}
var screenshotCmd = &cobra.Command{ var screenshotCmd = &cobra.Command{
Use: "screenshot", Use: "screenshot",
Short: "Capture screenshots", Short: "Capture screenshots",
@@ -51,9 +39,8 @@ Modes:
full - Capture the focused output full - Capture the focused output
all - Capture all outputs combined all - Capture all outputs combined
output - Capture a specific output by name output - Capture a specific output by name
window - Capture the focused window (Hyprland/Mango/niri) window - Capture the focused window (Hyprland/Mango)
last - Capture the last selected region last - Capture the last selected region
scroll - Select a region, then scroll to capture a stitched tall image
Output format (--format): Output format (--format):
png - PNG format (default) png - PNG format (default)
@@ -72,10 +59,7 @@ Examples:
dms screenshot --no-file # Clipboard only dms screenshot --no-file # Clipboard only
dms screenshot --no-confirm # Region capture on mouse release dms screenshot --no-confirm # Region capture on mouse release
dms screenshot --cursor=on # Include cursor dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85 dms screenshot -f jpg -q 85 # JPEG with quality 85`,
dms screenshot --json # Print capture metadata as JSON
dms screenshot scroll # Scroll capture, Enter finishes / Esc cancels
dms screenshot scroll --interval 250`,
} }
var ssRegionCmd = &cobra.Command{ var ssRegionCmd = &cobra.Command{
@@ -113,33 +97,10 @@ If no previous region exists, falls back to interactive selection.`,
var ssWindowCmd = &cobra.Command{ var ssWindowCmd = &cobra.Command{
Use: "window", Use: "window",
Short: "Capture the focused window", Short: "Capture the focused window",
Long: `Capture the currently focused window. Supported on Hyprland, Mango, and niri.`, Long: `Capture the currently focused window. Supported on Hyprland and Mango.`,
Run: runScreenshotWindow, Run: runScreenshotWindow,
} }
var ssScrollInterval int
var ssScrollCmd = &cobra.Command{
Use: "scroll",
Short: "Capture a scrolling region stitched into one tall image",
Long: `Select a region, then scroll the content beneath with the mouse wheel or
touchpad while frames are captured and stitched vertically. Finish with the
on-screen done button; cancel with the cancel button. Enter and Esc work
everywhere: most compositors hold the keyboard on the overlay (keyboard
scrolling does not reach the app there), while Hyprland leaves the keyboard
with the application — keyboard scrolling works, and Enter/Esc act through
temporary global binds for the session. The cursor is never included in
frames.
Frames are stitched continuously while scrolling, and revisited content is
never duplicated — scrolling up past the starting point extends the image
upward. Content jumped past faster than capture can follow is skipped rather
than stitched incorrectly.
Rotated outputs are not supported.`,
Run: runScreenshotScroll,
}
var ssListCmd = &cobra.Command{ var ssListCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List available outputs", Short: "List available outputs",
@@ -167,12 +128,8 @@ func init() {
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation") screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing") screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)") screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON")
ssScrollCmd.Flags().IntVar(&ssScrollInterval, "interval", 45, "Capture interval in milliseconds (30-1000)")
screenshotCmd.AddCommand(ssRegionCmd) screenshotCmd.AddCommand(ssRegionCmd)
screenshotCmd.AddCommand(ssScrollCmd)
screenshotCmd.AddCommand(ssFullCmd) screenshotCmd.AddCommand(ssFullCmd)
screenshotCmd.AddCommand(ssAllCmd) screenshotCmd.AddCommand(ssAllCmd)
screenshotCmd.AddCommand(ssOutputCmd) screenshotCmd.AddCommand(ssOutputCmd)
@@ -231,54 +188,25 @@ 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()
} }
func writeScreenshotJSON(meta screenshotMetadata) {
_ = json.NewEncoder(os.Stdout).Encode(meta)
}
func exitScreenshotError(context string, err error) {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "error", Error: err.Error()})
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Error%s: %v\n", context, err)
os.Exit(1)
}
func formatMime(format screenshot.Format) string {
switch format {
case screenshot.FormatJPEG:
return "image/jpeg"
case screenshot.FormatPPM:
return "image/x-portable-pixmap"
default:
return "image/png"
}
}
func runScreenshot(config screenshot.Config) { func runScreenshot(config screenshot.Config) {
if ssJSON && config.Stdout {
fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout")
os.Exit(1)
}
// Region select needs the keyboard; drop popout grabs for its duration. // Region select needs the keyboard; drop popout grabs for its duration.
result, err := func() (*screenshot.CaptureResult, error) { result, err := func() (*screenshot.CaptureResult, error) {
interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion || config.Mode == screenshot.ModeScroll interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion
if interactive { if interactive {
setPopoutScreenshotMode(true) setPopoutScreenshotMode(true)
defer setPopoutScreenshotMode(false) defer setPopoutScreenshotMode(false)
@@ -287,13 +215,11 @@ func runScreenshot(config screenshot.Config) {
}() }()
if err != nil { if err != nil {
exitScreenshotError("", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
if result == nil { if result == nil {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"})
}
os.Exit(0) os.Exit(0)
} }
@@ -305,7 +231,8 @@ func runScreenshot(config screenshot.Config) {
if config.Stdout { if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil { if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
exitScreenshotError(" writing to stdout", err) fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
os.Exit(1)
} }
return return
} }
@@ -325,37 +252,22 @@ func runScreenshot(config screenshot.Config) {
filePath = filepath.Join(outputDir, filename) filePath = filepath.Join(outputDir, filename)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil { if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
exitScreenshotError(" writing file", err) fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
} os.Exit(1)
if !ssJSON {
fmt.Println(filePath)
} }
fmt.Println(filePath)
} }
if config.Clipboard { if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil { if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
exitScreenshotError(" copying to clipboard", err) fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
os.Exit(1)
} }
if !ssJSON && !config.SaveFile { if !config.SaveFile {
fmt.Println("Copied to clipboard") fmt.Println("Copied to clipboard")
} }
} }
if ssJSON {
scale := result.Scale
if scale <= 0 {
scale = 1.0
}
writeScreenshotJSON(screenshotMetadata{
Status: "success",
Path: filePath,
Width: result.Buffer.Width,
Height: result.Buffer.Height,
Scale: scale,
Mime: formatMime(config.Format),
})
}
if config.Notify { if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format) thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
screenshot.SendNotification(screenshot.NotifyResult{ screenshot.SendNotification(screenshot.NotifyResult{
@@ -466,12 +378,6 @@ func runScreenshotRegion(cmd *cobra.Command, args []string) {
runScreenshot(config) runScreenshot(config)
} }
func runScreenshotScroll(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeScroll)
config.IntervalMs = min(max(ssScrollInterval, 30), 1000)
runScreenshot(config)
}
func runScreenshotFull(cmd *cobra.Command, args []string) { func runScreenshotFull(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeFullScreen) config := getScreenshotConfig(screenshot.ModeFullScreen)
runScreenshot(config) runScreenshot(config)
+6 -36
View File
@@ -10,7 +10,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/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 +143,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 +204,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 +213,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 {
@@ -316,9 +298,6 @@ func runSetup() error {
if wmSelected { if wmSelected {
if wm == deps.WindowManagerMango { if wm == deps.WindowManagerMango {
useSystemd = false useSystemd = false
} else if isVoidSetup() {
useSystemd = false
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
} else { } else {
useSystemd = promptSystemd() useSystemd = promptSystemd()
} }
@@ -393,15 +372,6 @@ func runSetup() error {
return nil return nil
} }
func isVoidSetup() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
// Add user to the input group for the evdev manager for inut state tracking. // Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator. // Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() { func ensureInputGroup() {
+2 -12
View File
@@ -47,7 +47,6 @@ var (
sysUpdateJSON bool sysUpdateJSON bool
sysUpdateNoFlatpak bool sysUpdateNoFlatpak bool
sysUpdateNoAUR bool sysUpdateNoAUR bool
sysUpdateIgnore []string
sysUpdateIntervalS int sysUpdateIntervalS int
sysUpdateListPmTime = 5 * time.Minute sysUpdateListPmTime = 5 * time.Minute
) )
@@ -59,7 +58,6 @@ func init() {
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)") systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay") systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)") systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)")
systemUpdateCmd.Flags().StringSliceVar(&sysUpdateIgnore, "ignore", nil, "Skip specific packages (repeatable or comma-separated)")
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)") systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
systemCmd.AddCommand(systemUpdateCmd) systemCmd.AddCommand(systemUpdateCmd)
@@ -194,7 +192,6 @@ func runSystemUpdateApply() {
Targets: pkgs, Targets: pkgs,
IncludeFlatpak: !sysUpdateNoFlatpak, IncludeFlatpak: !sysUpdateNoFlatpak,
IncludeAUR: !sysUpdateNoAUR, IncludeAUR: !sysUpdateNoAUR,
Ignored: sysUpdateIgnore,
DryRun: sysUpdateDry, DryRun: sysUpdateDry,
UseSudo: true, UseSudo: true,
} }
@@ -237,19 +234,12 @@ func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupd
} }
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package { func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 { if !sysUpdateNoAUR {
return pkgs return pkgs
} }
ignored := make(map[string]bool, len(sysUpdateIgnore))
for _, name := range sysUpdateIgnore {
ignored[name] = true
}
out := pkgs[:0] out := pkgs[:0]
for _, p := range pkgs { for _, p := range pkgs {
if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR { if p.Repo == sysupdate.RepoAUR {
continue
}
if ignored[p.Name] {
continue continue
} }
out = append(out, p) out = append(out, p)
+24
View File
@@ -0,0 +1,24 @@
package main
import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/traywatcher"
"github.com/spf13/cobra"
)
var trayWatcherCmd = &cobra.Command{
Use: "tray-watcher",
Short: "Run a minimal StatusNotifierWatcher for early tray registration",
Long: `Run a minimal org.kde.StatusNotifierWatcher daemon.
Started early in the session (Before=graphical-session.target via
dms-tray-watcher.service), it lets tray apps launched by XDG autostart
register their items before the shell finishes loading, and keeps them
registered across shell restarts. The shell's tray host picks items up from
this watcher; if it is not running, the shell's built-in watcher takes over.`,
Run: func(cmd *cobra.Command, args []string) {
if err := traywatcher.Run(); err != nil {
log.Fatalf("%v", err)
}
},
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power"
wlclient "github.com/AvengeMedia/dankgo/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
type cmd struct { type cmd struct {
+114
View File
@@ -0,0 +1,114 @@
package main
import (
"context"
"fmt"
"os"
"strings"
"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)
}
}
// ensureGreetdPamRundir adds pam_rundir to the greetd PAM stack so the post-login
// session gets an XDG_RUNTIME_DIR on systems without logind (Void with seatd).
// Appended outside DMS's managed auth block so it survives `dms greeter sync`.
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"
}
+10 -3
View File
@@ -12,11 +12,18 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd) 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)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd, pluginsLockCmd, pluginsRestoreCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
registryCmd.AddCommand(registryListCmd, registryAddCmd, registryRemoveCmd)
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(authCmd) rootCmd.AddCommand(authCmd)
+10 -2
View File
@@ -12,9 +12,17 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd) 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)
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, pluginsLockCmd, pluginsRestoreCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(authCmd) rootCmd.AddCommand(authCmd)
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/AvengeMedia/dankgo/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
type randrOutput struct { type randrOutput struct {
+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)
+688 -6
View File
@@ -4,20 +4,583 @@ 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 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 = 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 = 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 +624,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 +691,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 +938,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
}
+29 -39
View File
@@ -1,14 +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/AvengeMedia/dgop v0.2.4-0.20260808141551-f559f2f31d2a
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
@@ -18,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 (
@@ -31,55 +31,45 @@ 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/danielgtaylor/huma/v2 v2.39.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
github.com/dlclark/regexp2/v2 v2.5.1 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/ebitengine/purego v0.10.2 // 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/go-ole/go-ole v1.3.0 // 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/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect github.com/mdlayher/netlink v1.11.1 // indirect
github.com/mdlayher/netlink v1.11.2 // indirect github.com/mdlayher/socket v0.6.0 // indirect
github.com/mdlayher/socket v0.6.1 // 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
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.7 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.12.0 // indirect
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
github.com/yusufpapurcu/wmi v1.2.4 // 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-20260727155853-b88d891fe743 // 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-20260812195758-5935f34d40ce
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
@@ -89,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.1 github.com/lucasb-eyer/go-colorful v1.4.0
github.com/mattn/go-isatty v0.0.24 github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.27 // 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
) )
+58 -86
View File
@@ -1,9 +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-20260812195758-5935f34d40ce h1:vNccNlfXze7Fmg9PXrsyxhfAMaifK04bsQHWUjVfe2w=
github.com/AvengeMedia/dankgo v0.0.0-20260812195758-5935f34d40ce/go.mod h1:xt8RldAfti0QCWidwYIzsSSoJWsE61WgEhTu2H9UpD4=
github.com/AvengeMedia/dgop v0.2.4-0.20260808141551-f559f2f31d2a h1:bGwK4G22ixHUs5NBeNy4JYMKUMMFoZtQz180OnbSFKE=
github.com/AvengeMedia/dgop v0.2.4-0.20260808141551-f559f2f31d2a/go.mod h1:2kt1u9YgHwi2npqB5ago8kwZdFrHlPGXv3PM/vwRHfY=
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=
@@ -15,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=
@@ -52,27 +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/danielgtaylor/huma/v2 v2.39.1 h1:0kwF4ltQoYZ+IU55VPy+BcGekzgF44R64daTGde1H+g= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/danielgtaylor/huma/v2 v2.39.1/go.mod h1:zcnQ38duIJ3VUHwFaBoZ6x8T+KN/mr33oyqxcj0HTug= 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/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
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=
@@ -89,23 +83,18 @@ github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 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-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
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/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
@@ -116,8 +105,6 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUv
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -130,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=
@@ -139,20 +126,18 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 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.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= 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=
@@ -170,8 +155,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -181,8 +164,6 @@ github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4Xl
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28= github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
@@ -199,12 +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/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
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=
@@ -216,41 +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=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 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=
@@ -263,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=
-198
View File
@@ -1,198 +0,0 @@
package backup
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
const dirName = "DankMaterialShell"
func ConfigDir() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(configDir, dirName), nil
}
func DefaultArchiveName() string {
return fmt.Sprintf("dms-backup-%s.tar.gz", time.Now().Format("20060102-150405"))
}
func Create(outputPath string) error {
srcDir, err := ConfigDir()
if err != nil {
return err
}
if _, err := os.Stat(srcDir); err != nil {
return fmt.Errorf("no DMS configuration found at %s: %w", srcDir, err)
}
out, err := os.Create(outputPath)
if err != nil {
return err
}
defer out.Close()
gz := gzip.NewWriter(out)
defer gz.Close()
tw := tar.NewWriter(gz)
defer tw.Close()
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
if rel == "." {
return nil
}
var link string
if info.Mode()&os.ModeSymlink != 0 {
if link, err = os.Readlink(path); err != nil {
return err
}
}
header, err := tar.FileInfoHeader(info, link)
if err != nil {
return err
}
header.Name = filepath.ToSlash(filepath.Join(dirName, rel))
if err := tw.WriteHeader(header); err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(tw, f)
return err
})
}
func Restore(archivePath string) (string, error) {
dstDir, err := ConfigDir()
if err != nil {
return "", err
}
if err := validateArchive(archivePath); err != nil {
return "", err
}
previous := ""
if _, err := os.Stat(dstDir); err == nil {
previous = dstDir + ".pre-restore-" + time.Now().Format("20060102-150405")
if err := os.Rename(dstDir, previous); err != nil {
return "", fmt.Errorf("failed to move existing configuration aside: %w", err)
}
}
if err := extract(archivePath, filepath.Dir(dstDir)); err != nil {
if previous != "" {
os.RemoveAll(dstDir)
os.Rename(previous, dstDir)
}
return "", err
}
return previous, nil
}
func validateArchive(archivePath string) error {
hasSettings := false
err := walkArchive(archivePath, func(header *tar.Header, _ *tar.Reader) error {
name := filepath.ToSlash(filepath.Clean(header.Name))
if strings.HasPrefix(name, "..") || filepath.IsAbs(header.Name) {
return fmt.Errorf("unsafe path in archive: %s", header.Name)
}
if !strings.HasPrefix(name, dirName+"/") && name != dirName {
return fmt.Errorf("not a DMS backup: unexpected entry %s", header.Name)
}
if name == dirName+"/settings.json" {
hasSettings = true
}
return nil
})
if err != nil {
return err
}
if !hasSettings {
return fmt.Errorf("not a DMS backup: settings.json missing from archive")
}
return nil
}
func extract(archivePath, destParent string) error {
return walkArchive(archivePath, func(header *tar.Header, tr *tar.Reader) error {
target := filepath.Join(destParent, filepath.Clean(header.Name))
switch header.Typeflag {
case tar.TypeDir:
return os.MkdirAll(target, os.FileMode(header.Mode))
case tar.TypeSymlink:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
return os.Symlink(header.Linkname, target)
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, tr)
return err
default:
return nil
}
})
}
func walkArchive(archivePath string, visit func(*tar.Header, *tar.Reader) error) error {
f, err := os.Open(archivePath)
if err != nil {
return err
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
return fmt.Errorf("not a gzip archive: %w", err)
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
header, err := tr.Next()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if err := visit(header, tr); err != nil {
return err
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ package blur
import ( import (
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
client "github.com/AvengeMedia/dankgo/wayland/client" client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
const extBackgroundEffectInterface = "ext_background_effect_manager_v1" const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
+143 -8
View File
@@ -8,8 +8,7 @@ import (
"path/filepath" "path/filepath"
"syscall" "syscall"
"github.com/AvengeMedia/dankgo/wayland/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
"github.com/AvengeMedia/dankgo/wlclipboard"
) )
const envServe = "_DMS_CLIPBOARD_SERVE" const envServe = "_DMS_CLIPBOARD_SERVE"
@@ -17,6 +16,37 @@ const envMime = "_DMS_CLIPBOARD_MIME"
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE" const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
const envCacheFile = "_DMS_CLIPBOARD_CACHE" const envCacheFile = "_DMS_CLIPBOARD_CACHE"
type Offer struct {
MimeType string
Data []byte
}
// textMimeAliases are offered alongside plain-text content so legacy X11
// clients bridged through XWayland find a target they can convert.
var textMimeAliases = []string{
"text/plain",
"text/plain;charset=utf-8",
"UTF8_STRING",
"STRING",
"TEXT",
}
// ExpandOffers turns raw clipboard data into the full offer list to serve,
// adding the standard alias set for text content.
func ExpandOffers(data []byte, mimeType string) []Offer {
offers := []Offer{{MimeType: mimeType, Data: data}}
if mimeType != "text/plain" && mimeType != "text/plain;charset=utf-8" {
return offers
}
for _, alias := range textMimeAliases {
if alias == mimeType {
continue
}
offers = append(offers, Offer{MimeType: alias, Data: data})
}
return offers
}
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard // MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
// child. Reads source data into memory, deletes any cache file, then serves. // child. Reads source data into memory, deletes any cache file, then serves.
func MaybeServeAndExit() { func MaybeServeAndExit() {
@@ -44,7 +74,7 @@ func MaybeServeAndExit() {
os.Exit(1) os.Exit(1)
} }
if err := serveOffers(wlclipboard.ExpandOffers(data, mimeType), pasteOnce); err != nil { if err := serveOffers(ExpandOffers(data, mimeType), pasteOnce); err != nil {
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err) fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
os.Exit(1) os.Exit(1)
} }
@@ -61,7 +91,7 @@ func CopyText(text string) error {
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground { if foreground {
return serveOffers(wlclipboard.ExpandOffers(data, mimeType), pasteOnce) return serveOffers(ExpandOffers(data, mimeType), pasteOnce)
} }
return copyForkCached(data, mimeType, pasteOnce) return copyForkCached(data, mimeType, pasteOnce)
} }
@@ -74,10 +104,10 @@ func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) err
if err != nil { if err != nil {
return fmt.Errorf("read source: %w", err) return fmt.Errorf("read source: %w", err)
} }
return serveOffers(wlclipboard.ExpandOffers(buf, mimeType), pasteOnce) return serveOffers(ExpandOffers(buf, mimeType), pasteOnce)
} }
func CopyMulti(offers []wlclipboard.Offer, foreground, pasteOnce bool) error { func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
if foreground { if foreground {
return serveOffers(offers, pasteOnce) return serveOffers(offers, pasteOnce)
} }
@@ -176,7 +206,7 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
return nil return nil
} }
func copyMultiFork(offers []wlclipboard.Offer, pasteOnce bool) error { func copyMultiFork(offers []Offer, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"} args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
if pasteOnce { if pasteOnce {
args = append(args, "--paste-once") args = append(args, "--paste-once")
@@ -238,7 +268,7 @@ func createClipboardCacheFile() (*os.File, error) {
// serveOffers owns the Wayland selection until cancelled (or first paste when // serveOffers owns the Wayland selection until cancelled (or first paste when
// pasteOnce is set), answering every offered mime type with its data. // pasteOnce is set), answering every offered mime type with its data.
func serveOffers(offers []wlclipboard.Offer, pasteOnce bool) error { func serveOffers(offers []Offer, pasteOnce bool) error {
if len(offers) == 0 { if len(offers) == 0 {
return fmt.Errorf("no offers to serve") return fmt.Errorf("no offers to serve")
} }
@@ -318,6 +348,111 @@ func serveOffers(offers []wlclipboard.Offer, pasteOnce bool) error {
} }
} }
func Paste() ([]byte, string, error) {
s, err := connectSession()
if err != nil {
return nil, "", err
}
defer s.Close()
dataControlMgr, err := s.requireDataControl()
if err != nil {
return nil, "", err
}
device, err := dataControlMgr.GetDataDevice(s.seat)
if err != nil {
return nil, "", fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
if e.Id == nil {
return
}
offerMimeTypes[e.Id] = nil
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
})
})
var selectionOffer *ext_data_control.ExtDataControlOfferV1
gotSelection := false
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
selectionOffer = e.Id
gotSelection = true
})
s.display.Roundtrip()
s.display.Roundtrip()
if !gotSelection || selectionOffer == nil {
return nil, "", fmt.Errorf("no clipboard data")
}
selectedMime := selectPreferredMimeType(offerMimeTypes[selectionOffer])
if selectedMime == "" {
return nil, "", fmt.Errorf("no supported mime type")
}
r, w, err := os.Pipe()
if err != nil {
return nil, "", fmt.Errorf("create pipe: %w", err)
}
defer r.Close()
if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil {
w.Close()
return nil, "", fmt.Errorf("receive: %w", err)
}
w.Close()
s.display.Roundtrip()
data, err := io.ReadAll(r)
if err != nil {
return nil, "", fmt.Errorf("read: %w", err)
}
return data, selectedMime, nil
}
func PasteText() (string, error) {
data, _, err := Paste()
if err != nil {
return "", err
}
return string(data), nil
}
func selectPreferredMimeType(mimes []string) string {
preferred := []string{
"text/plain;charset=utf-8",
"text/plain",
"UTF8_STRING",
"STRING",
"TEXT",
"image/png",
"image/jpeg",
}
for _, pref := range preferred {
for _, mime := range mimes {
if mime == pref {
return mime
}
}
}
if len(mimes) > 0 {
return mimes[0]
}
return ""
}
func IsImageMimeType(mime string) bool { func IsImageMimeType(mime string) bool {
return len(mime) > 6 && mime[:6] == "image/" return len(mime) > 6 && mime[:6] == "image/"
} }
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
wlclient "github.com/AvengeMedia/dankgo/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -4,7 +4,7 @@ import (
"os" "os"
"testing" "testing"
wlclient "github.com/AvengeMedia/dankgo/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
+25
View File
@@ -78,3 +78,28 @@ func TestResolveKeycodes(t *testing.T) {
}) })
} }
} }
func TestExpandOffers(t *testing.T) {
text := ExpandOffers([]byte("hi"), "text/plain;charset=utf-8")
if len(text) != 5 {
t.Fatalf("expected 5 text offers, got %d", len(text))
}
seen := map[string]bool{}
for _, o := range text {
if string(o.Data) != "hi" {
t.Errorf("offer %s has wrong data", o.MimeType)
}
if seen[o.MimeType] {
t.Errorf("duplicate offer %s", o.MimeType)
}
seen[o.MimeType] = true
}
if !seen["UTF8_STRING"] || !seen["STRING"] || !seen["TEXT"] || !seen["text/plain"] {
t.Errorf("missing X11 alias offers: %v", seen)
}
img := ExpandOffers([]byte{1}, "image/png")
if len(img) != 1 || img[0].MimeType != "image/png" {
t.Errorf("non-text mime should not expand, got %+v", img)
}
}
+202 -38
View File
@@ -8,24 +8,191 @@ import (
"os" "os"
"time" "time"
"github.com/AvengeMedia/dankgo/wayland/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
// WatchAll subscribes to clipboard selection changes like wlclipboard.Watch, type ClipboardChange struct {
// additionally reporting every mime type each selection offers. Data []byte
MimeType string
MimeTypes []string
}
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
wlCtx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
if e.Id == nil {
return
}
offerMimeTypes[e.Id] = nil
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
})
})
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
if e.Id == nil {
return
}
mimes := offerMimeTypes[e.Id]
selectedMime := selectPreferredMimeType(mimes)
if selectedMime == "" {
return
}
r, w, err := os.Pipe()
if err != nil {
return
}
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
w.Close()
r.Close()
return
}
w.Close()
go func() {
defer r.Close()
data, err := io.ReadAll(r)
if err != nil || len(data) == 0 {
return
}
callback(data, selectedMime)
}()
})
display.Roundtrip()
display.Roundtrip()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
return fmt.Errorf("set read deadline: %w", err)
}
if err := wlCtx.Dispatch(); err != nil {
if isTimeoutError(err) {
continue
}
return fmt.Errorf("dispatch: %w", err)
}
}
}
}
func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error { func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error {
s, err := connectSession() display, err := wlclient.Connect("")
if err != nil { if err != nil {
return err return fmt.Errorf("wayland connect: %w", err)
} }
defer s.Close() defer display.Destroy()
dataControlMgr, err := s.requireDataControl() wlCtx := display.Context()
registry, err := display.GetRegistry()
if err != nil { if err != nil {
return err return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(wlCtx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
} }
device, err := dataControlMgr.GetDataDevice(s.seat) device, err := dataControlMgr.GetDataDevice(seat)
if err != nil { if err != nil {
return fmt.Errorf("get data device: %w", err) return fmt.Errorf("get data device: %w", err)
} }
@@ -79,18 +246,18 @@ func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, a
}() }()
}) })
s.display.Roundtrip() display.Roundtrip()
s.display.Roundtrip() display.Roundtrip()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
if err := s.ctx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil { if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
return fmt.Errorf("set read deadline: %w", err) return fmt.Errorf("set read deadline: %w", err)
} }
if err := s.ctx.Dispatch(); err != nil { if err := wlCtx.Dispatch(); err != nil {
if isTimeoutError(err) { if isTimeoutError(err) {
continue continue
} }
@@ -100,31 +267,6 @@ func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, a
} }
} }
func selectPreferredMimeType(mimes []string) string {
preferred := []string{
"text/plain;charset=utf-8",
"text/plain",
"UTF8_STRING",
"STRING",
"TEXT",
"image/png",
"image/jpeg",
}
for _, pref := range preferred {
for _, mime := range mimes {
if mime == pref {
return mime
}
}
}
if len(mimes) > 0 {
return mimes[0]
}
return ""
}
func isTimeoutError(err error) bool { func isTimeoutError(err error) bool {
if err == nil { if err == nil {
return false return false
@@ -137,3 +279,25 @@ func isTimeoutError(err error) bool {
} }
return false return false
} }
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
ch := make(chan ClipboardChange, 16)
errCh := make(chan error, 1)
go func() {
defer close(ch)
err := Watch(ctx, func(data []byte, mimeType string) {
select {
case ch <- ClipboardChange{Data: data, MimeType: mimeType}:
default:
}
})
if err != nil && err != context.Canceled {
errCh <- err
}
close(errCh)
}()
time.Sleep(50 * time.Millisecond)
return ch, errCh
}
+2 -2
View File
@@ -3,9 +3,9 @@ package clipboard
import ( import (
"fmt" "fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
wlclient "github.com/AvengeMedia/dankgo/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/dankgo/wayland/ext_data_control"
) )
type session struct { type session struct {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/dankgo/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
type Config struct { type Config struct {
+3 -7
View File
@@ -61,10 +61,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true) return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
} }
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult
@@ -264,7 +260,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", ""},
@@ -885,8 +880,9 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars) config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
spawnDms := `spawn-at-startup "dms" "run"` // Watcher spawns first so it owns the SNI name before dms/tray apps start.
if !strings.Contains(config, spawnDms) { spawnDms := "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\""
if !strings.Contains(config, `spawn-at-startup "dms" "run"`) {
// Insert spawn-at-startup for dms after the environment block // Insert spawn-at-startup for dms after the environment block
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`) envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
if loc := envBlockEnd.FindStringIndex(config); loc != nil { if loc := envBlockEnd.FindStringIndex(config); loc != nil {
+12
View File
@@ -520,9 +520,21 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandLuaConfig, "input =") assert.Contains(t, HyprlandLuaConfig, "input =")
} }
// In non-systemd mode dms is launched from the compositor config, so the tray
// watcher must be launched there too, before dms, to own the SNI name first.
func TestNonSystemdLaunchesTrayWatcherBeforeShell(t *testing.T) {
hypr := transformHyprlandLuaForNonSystemd(HyprlandLuaConfig, "ghostty")
assert.Contains(t, hypr, "hl.exec_cmd(\"dms tray-watcher\")\n\thl.exec_cmd(\"dms run\")")
niri := (&ConfigDeployer{}).transformNiriConfigForNonSystemd(NiriConfig, "ghostty")
assert.Contains(t, niri, "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\"")
}
func TestMangoConfigStructure(t *testing.T) { func TestMangoConfigStructure(t *testing.T) {
assert.Contains(t, MangoConfig, "exec-once=dms run") assert.Contains(t, MangoConfig, "exec-once=dms run")
assert.NotContains(t, MangoConfig, "exec_once=dms run") assert.NotContains(t, MangoConfig, "exec_once=dms run")
// Tray watcher must start before the shell so it owns the SNI name first.
assert.Contains(t, MangoConfig, "exec-once=dms tray-watcher\nexec-once=dms run")
assert.Contains(t, MangoConfig, "source=./dms/binds.conf") assert.Contains(t, MangoConfig, "source=./dms/binds.conf")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left") assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down") assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
+2 -2
View File
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
copy-on-select = false copy-on-select = false
confirm-close-surface = false confirm-close-surface = false
# Disable in-app Ghostty toast notifications # Disable annoying copied to clipboard
app-notifications = false app-notifications = no-clipboard-copy,no-config-reload
# Key bindings for common actions # Key bindings for common actions
#keybind = ctrl+c=copy_to_clipboard #keybind = ctrl+c=copy_to_clipboard
+1 -1
View File
@@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle")) hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle")) hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle")) hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper")) hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview")) hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview")) hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle")) hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
+1 -2
View File
@@ -11,8 +11,7 @@ end)
hl.config({ hl.config({
input = { input = {
-- empty inherits XKB_DEFAULT_LAYOUT (libxkbcommon), falls back to "us" kb_layout = "us",
kb_layout = "",
numlock_by_default = true, numlock_by_default = true,
follow_mouse = 0, follow_mouse = 0,
touchpad = { touchpad = {
@@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle
# Notepad # Notepad
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
# Browse Wallpapers # Browse Wallpapers
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
# Power Menu # Power Menu
bind=SUPER,x,spawn,dms ipc call powermenu toggle bind=SUPER,x,spawn,dms ipc call powermenu toggle
# Cycle Display Profile # Cycle Display Profile
+3 -1
View File
@@ -7,7 +7,9 @@ env=XDG_SESSION_TYPE,wayland
# exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs # exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs
# every exec= on each config reload, and DMS reloads the config, which would # every exec= on each config reload, and DMS reloads the config, which would
# spawn a new shell on every reload. # spawn a new shell on every reload. The tray watcher starts first so it owns
# the SNI name before dms and the tray apps come up.
exec-once=dms tray-watcher
exec-once=dms run exec-once=dms run
source=./dms/colors.conf source=./dms/colors.conf
+19 -19
View File
@@ -24,7 +24,7 @@ binds {
spawn "dms" "ipc" "call" "settings" "focusOrToggle"; spawn "dms" "ipc" "call" "settings" "focusOrToggle";
} }
Mod+Y hotkey-overlay-title="Browse Wallpapers" { Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper"; spawn "dms" "ipc" "call" "dankdash" "wallpaper";
} }
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; } Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; } Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
@@ -51,24 +51,24 @@ binds {
XF86AudioMicMute allow-when-locked=true { XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute"; spawn "dms" "ipc" "call" "audio" "micmute";
} }
XF86AudioPause allow-when-locked=true { XF86AudioPause allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause"; spawn "dms" "ipc" "call" "mpris" "playPause";
} }
XF86AudioPlay allow-when-locked=true { XF86AudioPlay allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause"; spawn "dms" "ipc" "call" "mpris" "playPause";
} }
XF86AudioPrev allow-when-locked=true { XF86AudioPrev allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "previous"; spawn "dms" "ipc" "call" "mpris" "previous";
} }
XF86AudioNext allow-when-locked=true { XF86AudioNext allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "next"; spawn "dms" "ipc" "call" "mpris" "next";
} }
Ctrl+XF86AudioRaiseVolume allow-when-locked=true { Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "increment" "3"; spawn "dms" "ipc" "call" "mpris" "increment" "3";
} }
Ctrl+XF86AudioLowerVolume allow-when-locked=true { Ctrl+XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "decrement" "3"; spawn "dms" "ipc" "call" "mpris" "decrement" "3";
} }
// === Brightness Controls === // === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true { XF86MonBrightnessUp allow-when-locked=true {
@@ -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
}
}
@@ -20,10 +20,3 @@ window-rule {
tiled-state true tiled-state true
draw-border-with-background false draw-border-with-background false
} }
layer-rule {
exclude namespace="^dms:bar$"
background-effect {
xray false
}
}
+31 -8
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".
@@ -241,10 +265,9 @@ recent-windows {
} }
// Include dms files // Include dms files
include optional=true "dms/colors.kdl" include "dms/colors.kdl"
include optional=true "dms/layout.kdl" include "dms/layout.kdl"
include optional=true "dms/alttab.kdl" include "dms/alttab.kdl"
include optional=true "dms/binds.kdl" include "dms/binds.kdl"
include optional=true "dms/outputs.kdl" include "dms/outputs.kdl"
include optional=true "dms/cursor.kdl" include "dms/cursor.kdl"
include optional=true "dms/input.kdl"
+1
View File
@@ -116,6 +116,7 @@ func transformHyprlandLuaForNonSystemd(config, terminalCommand string) string {
`hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" + `hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" +
fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" + fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" +
`hl.on("hyprland.start", function()` + "\n" + `hl.on("hyprland.start", function()` + "\n" +
` hl.exec_cmd("dms tray-watcher")` + "\n" +
` hl.exec_cmd("dms run")` + "\n" + ` hl.exec_cmd("dms run")` + "\n" +
`end)` + "\n" + `end)` + "\n" +
hyprlandStartupEnd hyprlandStartupEnd
+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
+7 -22
View File
@@ -313,7 +313,6 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
fg := HexToRGB(hexColor) fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B} cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
Lf, af, bf := cf.Lab() Lf, af, bf := cf.Lab()
Lf *= 100.0
dir := 1.0 dir := 1.0
if isLightMode { if isLightMode {
@@ -342,7 +341,6 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
fg := HexToRGB(hexColor) fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B} cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
origL, af, bf := cf.Lab() origL, af, bf := cf.Lab()
origL *= 100.0
var darkerResult, lighterResult string var darkerResult, lighterResult string
darkerL, lighterL := origL, origL darkerL, lighterL := origL, origL
@@ -422,24 +420,6 @@ func blendHue(base, target, factor float64) float64 {
return result return result
} }
// color8 sits a fixed L* offset from the background so it keeps its dim role
// regardless of primary brightness (conventional palettes put ANSI bright
// black ~2-2.5:1 from the background, e.g. catppuccin-mocha #585b70 on #1e1e2e)
func DeriveDim(bgHex string, hue, sat float64, isLight bool) string {
offset := 22.0
if isLight {
offset = -offset
}
bgL := getLstar(bgHex)
targetL := math.Max(0, math.Min(100, bgL+offset))
tint := HSVToRGB(HSV{H: hue, S: sat, V: 0.5})
c := colorful.Color{R: tint.R, G: tint.G, B: tint.B}
_, af, bf := c.Lab()
return labToHex(targetL, af, bf)
}
func DeriveContainer(primary string, isLight bool) string { func DeriveContainer(primary string, isLight bool) string {
rgb := HexToRGB(primary) rgb := HexToRGB(primary)
hsv := RGBToHSV(rgb) hsv := RGBToHSV(rgb)
@@ -520,7 +500,10 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
gray7V := baseVal * 0.28 gray7V := baseVal * 0.28
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts)) palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.05, true)) gray8S := baseSat * 0.05
gray8V := baseVal * 0.85
dimTarget := secondaryTarget * 0.5
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
brightRedS := math.Min(baseSat*1.0, 1.0) brightRedS := math.Min(baseSat*1.0, 1.0)
brightRedV := math.Min(baseVal*1.2, 1.0) brightRedV := math.Min(baseVal*1.2, 1.0)
@@ -576,7 +559,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
gray7V := math.Min(baseVal*1.05, 1.0) gray7V := math.Min(baseVal*1.05, 1.0)
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts)) palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.15, false)) gray8S := baseSat * 0.15
gray8V := baseVal * 0.65
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
brightRedS := math.Min(baseSat*0.75, 1.0) brightRedS := math.Min(baseSat*0.75, 1.0)
brightRedV := math.Min(baseVal*1.35, 1.0) brightRedV := math.Min(baseVal*1.35, 1.0)
-70
View File
@@ -679,73 +679,3 @@ func TestContrastAlgorithmComparison(t *testing.T) {
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount) t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
} }
func TestEnsureContrastDPSLightModeStaysLight(t *testing.T) {
tests := []struct {
name string
result string
bg string
minLstar float64
}{
{
name: "bidirectional adjustment",
result: EnsureContrastDPSBidirectional("#d0ccc6", "#f8f8f8", 17.5, true),
bg: "#f8f8f8",
minLstar: 20.0,
},
{
name: "lstar adjustment",
result: EnsureContrastDPSLstar("#c0c0c0", "#f8f8f8", 30.0, true),
bg: "#f8f8f8",
minLstar: 20.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lstar := getLstar(tt.result)
if lstar < tt.minLstar {
t.Errorf("result %s has L* %.2f on light bg %s, expected >= %.2f (collapsed to near-black)",
tt.result, lstar, tt.bg, tt.minLstar)
}
})
}
}
func TestGeneratePaletteColor8Dim(t *testing.T) {
hues := []string{"#e91e63", "#f59e0b", "#22c55e", "#06b6d4", "#8b5cf6", "#ef4444"}
for _, base := range hues {
t.Run(base, func(t *testing.T) {
palette := GeneratePalette(base, PaletteOptions{IsLight: false, UseDPS: true})
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
if bgRatio < 1.5 || bgRatio > 3.0 {
t.Errorf("Color8 %s vs bg %s ratio %.2f, expected 1.5-3.0 (bright black stays near bg)",
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
sepRatio := ContrastRatio(palette.Color4.Hex, palette.Color8.Hex)
if sepRatio < 2.0 {
t.Errorf("Color4 %s vs Color8 %s ratio %.2f, expected >= 2.0 (blue must not collide with bright black)",
palette.Color4.Hex, palette.Color8.Hex, sepRatio)
}
})
}
}
func TestGeneratePaletteLightColor8StaysLight(t *testing.T) {
palette := GeneratePalette("#f59e0b", PaletteOptions{IsLight: true, UseDPS: true})
lstar := getLstar(palette.Color8.Hex)
if lstar < 60.0 {
t.Errorf("light mode Color8 %s has L* %.2f, expected >= 60 (dim grey, not near-black)",
palette.Color8.Hex, lstar)
}
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
if bgRatio > 3.0 {
t.Errorf("light mode Color8 %s vs bg %s ratio %.2f, expected <= 3.0",
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
}
-16
View File
@@ -302,22 +302,6 @@ func AppsForMime(mimeType string) []string {
return out return out
} }
// HandlersForMime lists launchable handler entries for a mime type,
// excluding Terminal=true and NoDisplay=true desktop entries so pickers
// only offer graphical applications.
func HandlersForMime(mimeType string) []*Entry {
ids := AppsForMime(mimeType)
out := make([]*Entry, 0, len(ids))
for _, id := range ids {
entry := EntryByID(id)
if entry == nil || entry.Hidden || entry.NoDisplay || entry.Terminal {
continue
}
out = append(out, entry)
}
return out
}
func QueryDefaults(mimeTypes []string) map[string]string { func QueryDefaults(mimeTypes []string) map[string]string {
out := make(map[string]string, len(mimeTypes)) out := make(map[string]string, len(mimeTypes))
for _, m := range mimeTypes { for _, m := range mimeTypes {
+27 -180
View File
@@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"maps"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
@@ -82,10 +81,7 @@ func readAssociations(path string) (*MimeAssociations, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return associationsFromData(data), nil
}
func associationsFromData(data []byte) *MimeAssociations {
groups := parseGroups(data) groups := parseGroups(data)
assoc := newAssociations() assoc := newAssociations()
@@ -110,7 +106,7 @@ func associationsFromData(data []byte) *MimeAssociations {
} }
} }
return assoc return assoc, nil
} }
func mergedAssociations() *MimeAssociations { func mergedAssociations() *MimeAssociations {
@@ -137,33 +133,38 @@ func mergedAssociations() *MimeAssociations {
return merged return merged
} }
// isSafeIniField rejects values that would corrupt a key=value line in mimeapps.list
func isSafeIniField(s string) bool {
return !strings.ContainsAny(s, "\n\r[]")
}
func writeUserMimeapps(update func(*MimeAssociations)) error { func writeUserMimeapps(update func(*MimeAssociations)) error {
mimeappsWriteMu.Lock() mimeappsWriteMu.Lock()
defer mimeappsWriteMu.Unlock() defer mimeappsWriteMu.Unlock()
path := mimeappsWritePath() path := mimeappsWritePath()
original, err := os.ReadFile(path) assoc, err := readAssociations(path)
if err != nil && !os.IsNotExist(err) { if err != nil {
return err if !os.IsNotExist(err) {
return err
}
assoc = newAssociations()
} }
assoc := associationsFromData(original)
update(assoc) update(assoc)
rendered, err := renderMimeapps(original, assoc) var buf bytes.Buffer
if err != nil { w := bufio.NewWriter(&buf)
return err
writeSection := func(name string, entries map[string]string) {
fmt.Fprintf(w, "[%s]\n", name)
keys := make([]string, 0, len(entries))
for k := range entries {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Fprintf(w, "%s=%s\n", k, entries[k])
}
fmt.Fprintln(w)
} }
return atomicWriteFile(path, rendered)
}
func managedGroups(assoc *MimeAssociations) (map[string]map[string]string, error) {
flatten := func(m map[string][]string) map[string]string { flatten := func(m map[string][]string) map[string]string {
out := make(map[string]string, len(m)) out := make(map[string]string, len(m))
for k, list := range m { for k, list := range m {
@@ -172,172 +173,18 @@ func managedGroups(assoc *MimeAssociations) (map[string]map[string]string, error
return out return out
} }
groups := map[string]map[string]string{ writeSection(groupDefaults, assoc.Defaults)
groupDefaults: maps.Clone(assoc.Defaults), writeSection(groupAdded, flatten(assoc.Added))
groupAdded: flatten(assoc.Added), writeSection(groupRemoved, flatten(assoc.Removed))
groupRemoved: flatten(assoc.Removed),
}
for name, entries := range groups {
for k, v := range entries {
if isSafeIniField(k) && isSafeIniField(v) {
continue
}
return nil, fmt.Errorf("invalid mimeapps.list field in [%s]: %q=%q", name, k, v)
}
}
return groups, nil
}
// renderMimeapps rewrites only the three association groups defined by the if err := w.Flush(); err != nil {
// mime-apps spec (https://specifications.freedesktop.org/mime-apps-spec/),
// keeping comments, blank lines, and unrelated groups verbatim.
func renderMimeapps(original []byte, assoc *MimeAssociations) ([]byte, error) {
managed, err := managedGroups(assoc)
if err != nil {
return nil, err
}
written := map[string]map[string]bool{
groupDefaults: {},
groupAdded: {},
groupRemoved: {},
}
var out []string
flushMissing := func(group string) {
entries, ok := managed[group]
if !ok {
return
}
keys := make([]string, 0, len(entries))
for k := range entries {
if !written[group][k] {
keys = append(keys, k)
}
}
if len(keys) == 0 {
return
}
sort.Strings(keys)
insertAt := len(out)
for insertAt > 0 && strings.TrimSpace(out[insertAt-1]) == "" {
insertAt--
}
lines := make([]string, 0, len(keys))
for _, k := range keys {
written[group][k] = true
lines = append(lines, k+"="+entries[k])
}
tail := append([]string(nil), out[insertAt:]...)
out = append(out[:insertAt], append(lines, tail...)...)
}
current := ""
scanner := bufio.NewScanner(bytes.NewReader(original))
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
flushMissing(current)
current = trimmed[1 : len(trimmed)-1]
out = append(out, line)
continue
}
entries, isManaged := managed[current]
if !isManaged {
out = append(out, line)
continue
}
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
out = append(out, line)
continue
}
eq := strings.IndexByte(trimmed, '=')
if eq <= 0 {
out = append(out, line)
continue
}
key := strings.TrimSpace(trimmed[:eq])
val, keep := entries[key]
if !keep || written[current][key] {
continue
}
written[current][key] = true
out = append(out, key+"="+val)
}
if err := scanner.Err(); err != nil {
return nil, err
}
flushMissing(current)
for _, group := range []string{groupDefaults, groupAdded, groupRemoved} {
entries := managed[group]
pending := make([]string, 0, len(entries))
for k := range entries {
if !written[group][k] {
pending = append(pending, k)
}
}
if len(pending) == 0 {
continue
}
sort.Strings(pending)
if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" {
out = append(out, "")
}
out = append(out, "["+group+"]")
for _, k := range pending {
written[group][k] = true
out = append(out, k+"="+entries[k])
}
}
if len(out) == 0 {
return nil, nil
}
return []byte(strings.Join(out, "\n") + "\n"), nil
}
func atomicWriteFile(path string, data []byte) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err return err
} }
tmp, err := os.CreateTemp(dir, ".mimeapps-*.tmp") if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
if err != nil {
return err return err
} }
tmpPath := tmp.Name() return os.WriteFile(path, buf.Bytes(), 0o644)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(tmpPath)
return err
}
if err := tmp.Chmod(0o644); err != nil {
tmp.Close()
os.Remove(tmpPath)
return err
}
if err := tmp.Close(); err != nil {
os.Remove(tmpPath)
return err
}
if err := os.Rename(tmpPath, path); err != nil {
os.Remove(tmpPath)
return err
}
return nil
} }
func setDefaultAssociation(mimeType, desktopID string) error { func setDefaultAssociation(mimeType, desktopID string) error {
-210
View File
@@ -1,210 +0,0 @@
package desktop
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestSetDefaultsPreservesUnrelatedContent(t *testing.T) {
configHome, _ := setupFakeXDG(t)
path := filepath.Join(configHome, "mimeapps.list")
writeFile(t, path, `# managed by hand
[Default Applications]
text/html=firefox.desktop
# pinned image viewer
image/png=eog.desktop
[Added Associations]
text/html=firefox.desktop;
[Custom Section]
foo=bar
`)
if err := SetDefaults([]string{"inode/directory", "x-scheme-handler/sftp"}, "org.kde.dolphin.desktop"); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
got := string(data)
preserved := []string{
"# managed by hand",
"# pinned image viewer",
"text/html=firefox.desktop",
"image/png=eog.desktop",
"[Custom Section]",
"foo=bar",
}
for _, want := range preserved {
if !strings.Contains(got, want) {
t.Errorf("rewrite lost %q:\n%s", want, got)
}
}
written := []string{
"inode/directory=org.kde.dolphin.desktop",
"x-scheme-handler/sftp=org.kde.dolphin.desktop",
}
for _, want := range written {
if !strings.Contains(got, want) {
t.Errorf("rewrite missing %q:\n%s", want, got)
}
}
if got := GetDefault("inode/directory"); got != "org.kde.dolphin.desktop" {
t.Errorf("GetDefault(inode/directory) = %q", got)
}
if got := GetDefault("x-scheme-handler/sftp"); got != "org.kde.dolphin.desktop" {
t.Errorf("GetDefault(x-scheme-handler/sftp) = %q", got)
}
}
func TestSetDefaultReplacesKeyInPlace(t *testing.T) {
configHome, _ := setupFakeXDG(t)
path := filepath.Join(configHome, "mimeapps.list")
writeFile(t, path, `[Default Applications]
inode/directory=foot.desktop
text/html=firefox.desktop
`)
if err := SetDefault("inode/directory", "org.gnome.Nautilus.desktop"); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
got := string(data)
if n := strings.Count(got, "inode/directory=org.gnome.Nautilus.desktop\n"); n != 1 {
t.Errorf("expected exactly one replaced default line, got %d:\n%s", n, got)
}
if strings.Contains(got, "foot.desktop") {
t.Errorf("old default still present:\n%s", got)
}
if !strings.Contains(got, "text/html=firefox.desktop") {
t.Errorf("unrelated default lost:\n%s", got)
}
}
func TestSetDefaultsCreatesFileWhenAbsent(t *testing.T) {
configHome, _ := setupFakeXDG(t)
path := filepath.Join(configHome, "mimeapps.list")
if err := SetDefaults([]string{"inode/directory", "x-scheme-handler/sftp"}, "org.kde.dolphin"); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
got := string(data)
expected := []string{
"[Default Applications]",
"inode/directory=org.kde.dolphin.desktop",
"x-scheme-handler/sftp=org.kde.dolphin.desktop",
"[Added Associations]",
}
for _, want := range expected {
if !strings.Contains(got, want) {
t.Errorf("new file missing %q:\n%s", want, got)
}
}
entries, err := os.ReadDir(configHome)
if err != nil {
t.Fatal(err)
}
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".tmp") {
t.Errorf("leftover temp file %q", e.Name())
}
}
}
func TestSetDefaultClearsRemovedAssociation(t *testing.T) {
configHome, _ := setupFakeXDG(t)
path := filepath.Join(configHome, "mimeapps.list")
writeFile(t, path, `[Removed Associations]
inode/directory=org.kde.dolphin.desktop;other.desktop;
`)
if err := SetDefault("inode/directory", "org.kde.dolphin.desktop"); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
got := string(data)
if !strings.Contains(got, "inode/directory=other.desktop;") {
t.Errorf("unrelated removed entry lost:\n%s", got)
}
if strings.Contains(got, "org.kde.dolphin.desktop;other.desktop") {
t.Errorf("chosen app still in removed list:\n%s", got)
}
if got := GetDefault("inode/directory"); got != "org.kde.dolphin.desktop" {
t.Errorf("GetDefault = %q", got)
}
}
func TestHandlersForMimeExcludesTerminalAndNoDisplay(t *testing.T) {
_, dataHome := setupFakeXDG(t)
apps := filepath.Join(dataHome, "applications")
writeFile(t, filepath.Join(apps, "org.kde.dolphin.desktop"), `[Desktop Entry]
Type=Application
Name=Dolphin
Icon=system-file-manager
MimeType=inode/directory;
`)
writeFile(t, filepath.Join(apps, "ghost-term.desktop"), `[Desktop Entry]
Type=Application
Name=Ghost Terminal
Terminal=true
MimeType=inode/directory;
`)
writeFile(t, filepath.Join(apps, "hidden-fm.desktop"), `[Desktop Entry]
Type=Application
Name=Hidden FM
NoDisplay=true
MimeType=inode/directory;
`)
writeFile(t, filepath.Join(apps, "mimeinfo.cache"), `[MIME Cache]
inode/directory=ghost-term.desktop;org.kde.dolphin.desktop;hidden-fm.desktop;stale.desktop;
`)
handlers := HandlersForMime("inode/directory")
byID := make(map[string]*Entry, len(handlers))
for _, h := range handlers {
byID[h.ID] = h
}
dolphin := byID["org.kde.dolphin.desktop"]
if dolphin == nil {
t.Fatal("org.kde.dolphin.desktop missing from handlers")
}
if dolphin.Name != "Dolphin" {
t.Errorf("Name = %q", dolphin.Name)
}
if dolphin.Icon != "system-file-manager" {
t.Errorf("Icon = %q", dolphin.Icon)
}
excluded := []string{"ghost-term.desktop", "hidden-fm.desktop", "stale.desktop"}
for _, id := range excluded {
if byID[id] != nil {
t.Errorf("%s should be excluded from handlers", id)
}
}
}
+5 -52
View File
@@ -118,30 +118,11 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
} }
dependencies = append(dependencies, a.detectMatugen()) dependencies = append(dependencies, a.detectMatugen())
dependencies = append(dependencies, a.detectDanksearch()) dependencies = append(dependencies, a.detectDgop())
dependencies = append(dependencies, a.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
func (a *ArchDistribution) detectDanksearch() deps.Dependency {
dep := a.BaseDistribution.detectDanksearch()
dep.CanToggle = true
if a.packageInstalled("dsearch-git") {
dep.Variant = deps.VariantGit
}
return dep
}
func (a *ArchDistribution) detectDankCalendar() deps.Dependency {
dep := a.BaseDistribution.detectDankCalendar()
dep.CanToggle = true
if a.packageInstalled("dankcalendar-git") {
dep.Variant = deps.VariantGit
}
return dep
}
func (a *ArchDistribution) detectXDGPortal() deps.Dependency { func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk")) return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
} }
@@ -151,13 +132,7 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
} }
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency { func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
installed := a.packageInstalled("greetd-dms-greeter-git") || a.packageInstalled("greetd-dms-greeter-bin") return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
dep := a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", installed)
dep.CanToggle = true
if a.packageInstalled("greetd-dms-greeter-git") {
dep.Variant = deps.VariantGit
}
return dep
} }
func (a *ArchDistribution) packageInstalled(pkg string) bool { func (a *ArchDistribution) packageInstalled(pkg string) bool {
@@ -216,15 +191,14 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
"quickshell": a.getQuickshellMapping(variants["quickshell"]), "quickshell": a.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": a.getDMSGreeterMapping(variants["dms-greeter"]), "dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
"matugen": a.getMatugenMapping(variants["matugen"]), "matugen": a.getMatugenMapping(variants["matugen"]),
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem}, "ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem}, "kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"danksearch": a.getDanksearchMapping(variants["danksearch"]),
"dankcalendar": a.getDankCalendarMapping(variants["dankcalendar"]),
} }
switch wm { switch wm {
@@ -279,27 +253,6 @@ func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) Packag
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem} return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
} }
func (a *ArchDistribution) getDanksearchMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dsearch-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dsearch-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDankCalendarMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dankcalendar-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dankcalendar-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDMSGreeterMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "greetd-dms-greeter-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping { func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
if forceDMSGit || variant == deps.VariantGit { if forceDMSGit || variant == deps.VariantGit {
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR} return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
@@ -657,7 +610,7 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
} }
func (a *ArchDistribution) reorderAURPackages(packages []string) []string { func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
dmsDepencies := []string{"quickshell", "quickshell-git"} dmsDepencies := []string{"quickshell", "quickshell-git", "dgop"}
var deps []string var deps []string
var others []string var others []string
+6 -8
View File
@@ -103,12 +103,8 @@ func (b *BaseDistribution) detectMatugen() deps.Dependency {
return b.detectCommand("matugen", "Material Design color generation tool") return b.detectCommand("matugen", "Material Design color generation tool")
} }
func (b *BaseDistribution) detectDanksearch() deps.Dependency { func (b *BaseDistribution) detectDgop() deps.Dependency {
return b.detectOptionalPackage("danksearch", "File indexing and search service", b.commandExists("dsearch") || b.commandExists("danksearch")) return b.detectCommand("dgop", "Desktop portal management tool")
}
func (b *BaseDistribution) detectDankCalendar() deps.Dependency {
return b.detectOptionalPackage("dankcalendar", "Calendar application", b.commandExists("dcal") || b.commandExists("dankcalendar"))
} }
func (b *BaseDistribution) detectDMS() deps.Dependency { func (b *BaseDistribution) detectDMS() deps.Dependency {
@@ -591,13 +587,15 @@ TERMINAL=%s
} }
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error { func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
// Pull in the tray watcher alongside dms; its Before=graphical-session.target
// ordering makes it claim the SNI name before autostart apps start.
switch wm { switch wm {
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil { if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms", "dms-tray-watcher").Run(); err != nil {
b.log("Warning: failed to add dms as a want for niri.service") b.log("Warning: failed to add dms as a want for niri.service")
} }
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil { if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms", "dms-tray-watcher").Run(); err != nil {
b.log("Warning: failed to add dms as a want for hyprland-session.target") b.log("Warning: failed to add dms as a want for hyprland-session.target")
} }
} }
+35 -66
View File
@@ -70,8 +70,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
dependencies = append(dependencies, d.detectMatugen()) dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDanksearch()) dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
@@ -134,9 +133,8 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"quickshell": d.getQuickshellMapping(variants["quickshell"]), "quickshell": d.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
} }
if wm == deps.WindowManagerNiri { if wm == deps.WindowManagerNiri {
@@ -159,60 +157,7 @@ func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) P
if forceQuickshellGit || variant == deps.VariantGit { if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
} }
// Debian 13 ships stable quickshell in trixie-backports only. return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
if d.debianNeedsQuickshellBackports() {
return PackageMapping{Name: "quickshell/trixie-backports", Repository: RepoTypeSystem}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
}
func (d *DebianDistribution) debianNeedsQuickshellBackports() bool {
osInfo, err := GetOSInfo()
if err != nil {
return false
}
return osInfo.VersionID == "13" || strings.EqualFold(osInfo.VersionCodename, "trixie")
}
func (d *DebianDistribution) ensureQuickshellBackports(ctx context.Context, systemPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
needsBackports := false
for _, pkg := range systemPkgs {
if strings.Contains(pkg, "trixie-backports") {
needsBackports = true
break
}
}
if !needsBackports {
return nil
}
policyOut, err := exec.CommandContext(ctx, "apt-cache", "policy").Output()
if err == nil && strings.Contains(string(policyOut), "trixie-backports") {
d.log("trixie-backports already configured")
return nil
}
listFile := "/etc/apt/sources.list.d/trixie-backports.list"
repoLine := "deb http://deb.debian.org/debian trixie-backports main contrib non-free non-free-firmware"
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.30,
Step: "Enabling trixie-backports for quickshell...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
LogOutput: "Debian 13 ships quickshell in trixie-backports",
}
addCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
if err := d.runWithProgress(addCmd, progressChan, PhaseSystemPackages, 0.30, 0.31); err != nil {
return err
}
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
return d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.31, 0.33)
} }
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
@@ -317,10 +262,6 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
} }
} }
if err := d.ensureQuickshellBackports(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable trixie-backports for quickshell: %w", err)
}
// System Packages // System Packages
if len(systemPkgs) > 0 { if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -472,8 +413,13 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
return fmt.Errorf("failed to get OS info: %w", err) return fmt.Errorf("failed to get OS info: %w", err)
} }
obsRepo := debianOBSRepoVersion(osInfo, readDebianAPTSuites()) // Determine Debian version for OBS repository URL
d.log(fmt.Sprintf("Using OBS Debian repository: %s", obsRepo)) debianVersion := "Debian_13"
if osInfo.VersionID == "testing" {
debianVersion = "Debian_Testing"
} else if osInfo.VersionCodename == "sid" || osInfo.VersionID == "sid" || strings.Contains(strings.ToLower(osInfo.PrettyName), "sid") || strings.Contains(strings.ToLower(osInfo.PrettyName), "unstable") {
debianVersion = "Debian_Unstable"
}
for _, pkg := range obsPkgs { for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] { if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
@@ -482,11 +428,12 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
// RepoURL format: "home:AvengeMedia:danklinux" // RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/") repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-") repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, obsRepo) baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, debianVersion)
// Check if repository already exists // Check if repository already exists
listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName) listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName)
if debianOBSListFileMatches(listFile, obsRepo) { checkCmd := exec.CommandContext(ctx, "test", "-f", listFile)
if checkCmd.Run() == nil {
d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL)) d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true enabledRepos[pkg.RepoURL] = true
continue continue
@@ -660,6 +607,10 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil { if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Rust: %w", err) return fmt.Errorf("failed to install Rust: %w", err)
} }
case "dgop":
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err)
}
} }
} }
@@ -718,6 +669,24 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
return nil return nil
} }
func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if d.commandExists("go") {
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.87,
Step: "Installing Go...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install golang-go",
}
installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
}
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
-172
View File
@@ -1,172 +0,0 @@
package distros
import (
"os"
"path/filepath"
"strings"
)
const (
obsRepoDebian13 = "Debian_13"
obsRepoDebianTesting = "Debian_Testing"
obsRepoDebianUnstable = "Debian_Unstable"
)
func debianOBSRepoVersion(info *OSInfo, aptSuites []string) string {
hasSuite := make(map[string]bool, len(aptSuites))
for _, suite := range aptSuites {
hasSuite[normalizeDebianSuite(suite)] = true
}
// APT suites distinguish testing from sid. os-release cannot: both currently
// report VERSION_CODENAME=forky and PRETTY_NAME="Debian GNU/Linux forky/sid"
if hasSuite["sid"] || hasSuite["unstable"] {
return obsRepoDebianUnstable
}
if hasSuite["testing"] || hasSuite["forky"] {
return obsRepoDebianTesting
}
if hasSuite["trixie"] || hasSuite["stable"] {
return obsRepoDebian13
}
if info == nil {
return obsRepoDebian13
}
id := strings.ToLower(strings.TrimSpace(info.VersionID))
code := strings.ToLower(strings.TrimSpace(info.VersionCodename))
if code == "sid" || id == "sid" {
return obsRepoDebianUnstable
}
if id == "13" || code == "trixie" {
return obsRepoDebian13
}
if id != "" && id != "testing" {
return obsRepoDebian13
}
// Prefer Testing when os-release is the shared forky/sid string. Unstable
// OBS packages can require a newer glibc than Testing ships.
return obsRepoDebianTesting
}
func debianOBSListFileMatches(path, debianVersion string) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
return debianOBSListPointsAt(string(data), debianVersion)
}
func debianOBSListPointsAt(contents, debianVersion string) bool {
return strings.Contains(contents, "/"+debianVersion+"/")
}
func readDebianAPTSuites() []string {
var chunks []string
if data, err := os.ReadFile("/etc/apt/sources.list"); err == nil {
chunks = append(chunks, string(data))
}
matches, _ := filepath.Glob("/etc/apt/sources.list.d/*")
for _, path := range matches {
if !strings.HasSuffix(path, ".list") && !strings.HasSuffix(path, ".sources") {
continue
}
data, err := os.ReadFile(path)
if err != nil {
continue
}
chunks = append(chunks, string(data))
}
return debianAPTSuites(strings.Join(chunks, "\n"))
}
func debianAPTSuites(sources string) []string {
seen := make(map[string]bool)
var suites []string
add := func(raw string) {
suite := normalizeDebianSuite(raw)
if suite == "" || seen[suite] {
return
}
seen[suite] = true
suites = append(suites, suite)
}
var stanza []string
flushStanza := func() {
if len(stanza) == 0 {
return
}
enabled := true
var stanzaSuites []string
for _, line := range stanza {
key, val, ok := strings.Cut(line, ":")
if !ok {
continue
}
switch strings.ToLower(strings.TrimSpace(key)) {
case "enabled":
switch strings.ToLower(strings.TrimSpace(val)) {
case "no", "false", "0":
enabled = false
}
case "suites":
stanzaSuites = strings.Fields(val)
}
}
if enabled {
for _, suite := range stanzaSuites {
add(suite)
}
}
stanza = nil
}
for _, raw := range strings.Split(sources, "\n") {
line := strings.TrimSpace(raw)
if line == "" {
flushStanza()
continue
}
if strings.HasPrefix(line, "#") {
continue
}
lower := strings.ToLower(line)
if strings.HasPrefix(lower, "deb ") || strings.HasPrefix(lower, "deb-src ") {
flushStanza()
if suite := suiteFromDebLine(line); suite != "" {
add(suite)
}
continue
}
stanza = append(stanza, line)
}
flushStanza()
return suites
}
func suiteFromDebLine(line string) string {
if i := strings.Index(line, "["); i != -1 {
if j := strings.Index(line, "]"); j > i {
line = strings.TrimSpace(line[:i] + " " + line[j+1:])
}
}
fields := strings.Fields(line)
if len(fields) < 3 {
return ""
}
return fields[2]
}
func normalizeDebianSuite(suite string) string {
suite = strings.ToLower(strings.TrimSpace(suite))
suite = strings.Trim(suite, "/")
for _, suffix := range []string{"-security", "-updates", "-backports", "-proposed-updates"} {
if strings.HasSuffix(suite, suffix) {
return strings.TrimSuffix(suite, suffix)
}
}
return suite
}
-34
View File
@@ -1,34 +0,0 @@
package distros
import (
"context"
"fmt"
"os/exec"
)
// SetupDsearchService enables the dsearch.service user unit. Enablement failures
// are returned for the caller to surface as a non-fatal warning.
func SetupDsearchService(ctx context.Context, logf func(string)) error {
if logf == nil {
logf = func(string) {}
}
if err := runSystemctlUser(ctx, "daemon-reload"); err != nil {
return err
}
if err := runSystemctlUser(ctx, "enable", "--now", "dsearch.service"); err != nil {
return err
}
logf("Enabled dsearch.service")
return nil
}
func runSystemctlUser(ctx context.Context, args ...string) error {
cmd := exec.CommandContext(ctx, "systemctl", append([]string{"--user"}, args...)...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("systemctl --user %v failed: %w: %s", args, err, string(output))
}
return nil
}
+2 -4
View File
@@ -103,8 +103,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
dependencies = append(dependencies, f.detectMatugen()) dependencies = append(dependencies, f.detectMatugen())
dependencies = append(dependencies, f.detectDanksearch()) dependencies = append(dependencies, f.detectDgop())
dependencies = append(dependencies, f.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
@@ -138,8 +137,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"danksearch": {Name: "danksearch", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
} }
switch wm { switch wm {
+9 -11
View File
@@ -53,11 +53,6 @@ func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDi
} }
} }
func emergeInstallArgs(packages []string) []string {
args := []string{"emerge", "--ask=n", "--quiet", "--autounmask-continue=y", "--autounmask-keep-keywords=y", "--autounmask-keep-masks=y"}
return append(args, packages...)
}
func (g *GentooDistribution) getArchKeyword() string { func (g *GentooDistribution) getArchKeyword() string {
arch := runtime.GOARCH arch := runtime.GOARCH
switch arch { switch arch {
@@ -117,7 +112,7 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
dependencies = append(dependencies, g.detectMatugen()) dependencies = append(dependencies, g.detectMatugen())
dependencies = append(dependencies, g.detectDanksearch()) dependencies = append(dependencies, g.detectDgop())
return dependencies, nil return dependencies, nil
} }
@@ -175,7 +170,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"quickshell": g.getQuickshellMapping(variants["quickshell"]), "quickshell": g.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}, "matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"dms (DankMaterialShell)": g.getDmsMapping(), "dms (DankMaterialShell)": g.getDmsMapping(),
"danksearch": {Name: "gui-apps/danksearch", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}, "dgop": {Name: "gui-apps/dgop", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
} }
switch wm { switch wm {
@@ -322,7 +317,6 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
g.log("Portage tree synced successfully") g.log("Portage tree synced successfully")
args := emergeInstallArgs(missingPkgs)
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", "))) g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites, Phase: PhasePrerequisites,
@@ -330,10 +324,12 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)), Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")),
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
} }
args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, missingPkgs...)
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -523,7 +519,8 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
} }
} }
args := emergeInstallArgs(packageNames) args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, packageNames...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -714,7 +711,8 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
guruPackages[i] = pkg + "::guru" guruPackages[i] = pkg + "::guru"
} }
args := emergeInstallArgs(guruPackages) args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, guruPackages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
-3
View File
@@ -17,7 +17,6 @@ const (
FamilyDebian DistroFamily = "debian" FamilyDebian DistroFamily = "debian"
FamilyNix DistroFamily = "nix" FamilyNix DistroFamily = "nix"
FamilyGentoo DistroFamily = "gentoo" FamilyGentoo DistroFamily = "gentoo"
FamilyVoid DistroFamily = "void"
) )
// PackageManagerType defines the package manager a distro uses // PackageManagerType defines the package manager a distro uses
@@ -30,7 +29,6 @@ const (
PackageManagerZypper PackageManagerType = "zypper" PackageManagerZypper PackageManagerType = "zypper"
PackageManagerNix PackageManagerType = "nix" PackageManagerNix PackageManagerType = "nix"
PackageManagerPortage PackageManagerType = "portage" PackageManagerPortage PackageManagerType = "portage"
PackageManagerXBPS PackageManagerType = "xbps"
) )
// RepositoryType defines the type of repository for a package // RepositoryType defines the type of repository for a package
@@ -44,7 +42,6 @@ const (
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE) RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
RepoTypeFlake RepositoryType = "flake" // Nix flake RepoTypeFlake RepositoryType = "flake" // Nix flake
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
RepoTypeManual RepositoryType = "manual" // Manual build from source RepoTypeManual RepositoryType = "manual" // Manual build from source
) )
+66 -15
View File
@@ -59,6 +59,10 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
if err := m.installDankMaterialShell(ctx, variant, sudoPassword, progressChan); err != nil { if err := m.installDankMaterialShell(ctx, variant, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install DankMaterialShell: %w", err) return fmt.Errorf("failed to install DankMaterialShell: %w", err)
} }
case "dgop":
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install dgop: %w", err)
}
case "niri": case "niri":
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil { if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install niri: %w", err) return fmt.Errorf("failed to install niri: %w", err)
@@ -91,6 +95,66 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
return nil return nil
} }
func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing dgop from source...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "dgop-build")
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Cloning dgop repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/AvengeMedia/dgop.git",
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/AvengeMedia/dgop.git", tmpDir)
if err := cloneCmd.Run(); err != nil {
m.logError("failed to clone dgop repository", err)
return fmt.Errorf("failed to clone dgop repository: %w", err)
}
buildCmd := exec.CommandContext(ctx, "make")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.7, "Building dgop..."); err != nil {
return fmt.Errorf("failed to build dgop: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.7,
Step: "Installing dgop...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo make install",
}
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
m.logError("failed to install dgop", err)
return fmt.Errorf("failed to install dgop: %w", err)
}
m.log("dgop installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing niri from source...") m.log("Installing niri from source...")
@@ -456,7 +520,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
Progress: 0.90, Progress: 0.90,
Step: "Cloning DankMaterialShell...", Step: "Cloning DankMaterialShell...",
IsComplete: false, IsComplete: false,
CommandInfo: "git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git", CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
} }
configDir := filepath.Dir(dmsPath) configDir := filepath.Dir(dmsPath)
@@ -464,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return fmt.Errorf("failed to create quickshell config directory: %w", err) return fmt.Errorf("failed to create quickshell config directory: %w", err)
} }
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recurse-submodules", cloneCmd := exec.CommandContext(ctx, "git", "clone",
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath) "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
if err := cloneCmd.Run(); err != nil { if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone DankMaterialShell: %w", err) return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
@@ -489,8 +553,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil return nil
} }
m.syncDMSSubmodules(ctx, dmsPath)
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag)) m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
m.log("DankMaterialShell cloned successfully") m.log("DankMaterialShell cloned successfully")
return nil return nil
@@ -529,8 +591,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil return nil
} }
m.syncDMSSubmodules(ctx, dmsPath)
m.log("DankMaterialShell updated successfully (git variant)") m.log("DankMaterialShell updated successfully (git variant)")
return nil return nil
} }
@@ -549,19 +609,10 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil return nil
} }
m.syncDMSSubmodules(ctx, dmsPath)
m.log(fmt.Sprintf("Updated to tag: %s", latestTag)) m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
return nil return nil
} }
func (m *ManualPackageInstaller) syncDMSSubmodules(ctx context.Context, dmsPath string) {
submoduleCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "submodule", "update", "--init", "--recursive")
if err := submoduleCmd.Run(); err != nil {
m.logError("Failed to update submodules", err)
}
}
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing xwayland-satellite from source...") m.log("Installing xwayland-satellite from source...")
+1 -1
View File
@@ -7,7 +7,7 @@ type minimalInstallGroup struct {
func shouldPreferMinimalInstall(pkg string) bool { func shouldPreferMinimalInstall(pkg string) bool {
switch pkg { switch pkg {
case "niri", "niri-git", "hyprland", "hyprland-git": case "niri", "niri-git":
return true return true
default: default:
return false return false
+2 -4
View File
@@ -90,8 +90,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
} }
dependencies = append(dependencies, o.detectMatugen()) dependencies = append(dependencies, o.detectMatugen())
dependencies = append(dependencies, o.detectDanksearch()) dependencies = append(dependencies, o.detectDgop())
dependencies = append(dependencies, o.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
@@ -129,8 +128,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
} }
switch wm { switch wm {
+1 -1
View File
@@ -104,7 +104,7 @@ func IsUnsupportedDistro(distroID, versionID string) bool {
if distroID == "debian" { if distroID == "debian" {
// unstable/sid support // unstable/sid support
if versionID == "sid" || versionID == "testing" { if versionID == "sid" {
return false return false
} }
if versionID == "" { if versionID == "" {
+53 -4
View File
@@ -79,8 +79,7 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
} }
dependencies = append(dependencies, u.detectMatugen()) dependencies = append(dependencies, u.detectMatugen())
dependencies = append(dependencies, u.detectDanksearch()) dependencies = append(dependencies, u.detectDgop())
dependencies = append(dependencies, u.detectDankCalendar())
return dependencies, nil return dependencies, nil
} }
@@ -123,9 +122,8 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"quickshell": u.getQuickshellMapping(variants["quickshell"]), "quickshell": u.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"danksearch": {Name: "danksearch", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dankcalendar": {Name: "dankcalendar-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
} }
switch wm { switch wm {
@@ -575,6 +573,10 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil { if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Rust: %w", err) return fmt.Errorf("failed to install Rust: %w", err)
} }
case "dgop":
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err)
}
} }
} }
@@ -634,6 +636,53 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
return nil return nil
} }
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if u.commandExists("go") {
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.87,
Step: "Adding Go PPA repository...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
}
addPPACmd := privesc.ExecCommand(ctx, sudoPassword,
"add-apt-repository -y ppa:longsleep/golang-backports")
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
return fmt.Errorf("failed to add Go PPA: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.88,
Step: "Updating package lists...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get update",
}
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.89,
Step: "Installing Go...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install golang-go",
}
installCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y golang-go")
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
}
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
-539
View File
@@ -1,539 +0,0 @@
package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
const (
VoidDMSRepo = "https://void.danklinux.com/dms/current"
VoidDankLinuxRepo = "https://void.danklinux.com/danklinux/current"
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
voidRunitSvDir = "/etc/sv"
voidRunitServiceDir = "/var/service"
)
func init() {
Register("void", "#478061", FamilyVoid, func(config DistroConfig, logChan chan<- string) Distribution {
return NewVoidDistribution(config, logChan)
})
}
type VoidDistribution struct {
*BaseDistribution
config DistroConfig
}
func NewVoidDistribution(config DistroConfig, logChan chan<- string) *VoidDistribution {
return &VoidDistribution{
BaseDistribution: NewBaseDistribution(logChan),
config: config,
}
}
func (v *VoidDistribution) GetID() string {
return v.config.ID
}
func (v *VoidDistribution) GetColorHex() string {
return v.config.ColorHex
}
func (v *VoidDistribution) GetFamily() DistroFamily {
return v.config.Family
}
func (v *VoidDistribution) GetPackageManager() PackageManagerType {
return PackageManagerXBPS
}
func (v *VoidDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return v.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
dependencies = append(dependencies, v.detectDMS())
dependencies = append(dependencies, v.detectSpecificTerminal(terminal))
dependencies = append(dependencies, v.detectGit())
dependencies = append(dependencies, v.detectWindowManager(wm))
dependencies = append(dependencies, v.detectQuickshell())
dependencies = append(dependencies, v.detectDMSGreeter())
dependencies = append(dependencies, v.detectXDGPortal())
dependencies = append(dependencies, v.detectAccountsService())
dependencies = append(dependencies, v.detectDBus())
dependencies = append(dependencies, v.detectElogind())
dependencies = append(dependencies, v.detectMesaDri())
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, v.detectHyprlandTools()...)
}
if wm == deps.WindowManagerNiri || wm == deps.WindowManagerMango {
dependencies = append(dependencies, v.detectXwaylandSatellite())
}
dependencies = append(dependencies, v.detectMatugen())
dependencies = append(dependencies, v.detectDanksearch())
dependencies = append(dependencies, v.detectDankCalendar())
return dependencies, nil
}
func (v *VoidDistribution) detectDMS() deps.Dependency {
status := deps.StatusMissing
version := ""
variant := deps.VariantStable
if v.packageInstalled("dms-git") {
status = deps.StatusInstalled
version = v.packageVersion("dms-git")
variant = deps.VariantGit
} else if v.packageInstalled("dms") {
status = deps.StatusInstalled
version = v.packageVersion("dms")
} else if v.commandExists("dms") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Version: version,
Description: "Desktop Management System package",
Required: true,
Variant: variant,
CanToggle: true,
}
}
func (v *VoidDistribution) detectQuickshell() deps.Dependency {
dep := v.BaseDistribution.detectQuickshell()
dep.CanToggle = false
return dep
}
func (v *VoidDistribution) detectXDGPortal() deps.Dependency {
return v.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", v.packageInstalled("xdg-desktop-portal-gtk"))
}
func (v *VoidDistribution) detectDMSGreeter() deps.Dependency {
return v.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", v.packageInstalled("dms-greeter"))
}
func (v *VoidDistribution) detectAccountsService() deps.Dependency {
return v.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", v.packageInstalled("accountsservice"))
}
func (v *VoidDistribution) detectDBus() deps.Dependency {
return v.detectPackage("dbus", "D-Bus system and session message bus", v.packageInstalled("dbus"))
}
func (v *VoidDistribution) detectElogind() deps.Dependency {
return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl"))
}
func (v *VoidDistribution) detectMesaDri() deps.Dependency {
return v.detectPackage("mesa-dri", "Mesa DRI/EGL drivers (GPU rendering; compositors find no outputs without it)", v.packageInstalled("mesa-dri"))
}
func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency {
return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite"))
}
func (v *VoidDistribution) packageInstalled(pkg string) bool {
return exec.Command("xbps-query", pkg).Run() == nil
}
func (v *VoidDistribution) packageVersion(pkg string) string {
output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(output))
}
func (v *VoidDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return v.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"dbus": {Name: "dbus", Repository: RepoTypeSystem},
"elogind": {Name: "elogind", Repository: RepoTypeSystem},
"mesa-dri": {Name: "mesa-dri", Repository: RepoTypeSystem},
"quickshell": {Name: "quickshell", Repository: RepoTypeSystem},
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
"dms (DankMaterialShell)": v.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo},
"danksearch": {Name: "danksearch", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
"dankcalendar": {Name: "dankcalendar", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
case deps.WindowManagerMango:
packages["mango"] = PackageMapping{Name: "mangowc", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
return packages
}
func (v *VoidDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantStable {
return PackageMapping{Name: "dms", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
return PackageMapping{Name: "dms-git", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
func (v *VoidDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking XBPS...",
IsComplete: false,
LogOutput: "Checking for xbps-install",
}
if _, err := exec.LookPath("xbps-install"); err != nil {
return fmt.Errorf("xbps-install not found; Void Linux package tools are required: %w", err)
}
return nil
}
func (v *VoidDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if wm == deps.WindowManagerHyprland {
arch, err := v.xbpsArch(ctx)
if err != nil {
return fmt.Errorf("failed to detect XBPS architecture for Hyprland repository selection: %w", err)
}
if arch != "x86_64" {
return fmt.Errorf("hyprland on Void Linux is installed from %s, which currently provides x86_64 packages only (detected %s)", VoidHyprlandRepo, arch)
}
}
if err := v.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, xbpsPkgs := v.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
if len(xbpsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling DMS XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
LogOutput: "Setting up custom XBPS repositories for DMS packages",
}
if err := v.enableXBPSRepos(ctx, xbpsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable XBPS repositories: %w", err)
}
}
allPkgs := v.uniquePackageNames(systemPkgs, v.extractPackageNames(xbpsPkgs))
if len(allPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d XBPS packages...", len(allPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing XBPS packages: %s", strings.Join(allPkgs, ", ")),
}
if err := v.installXBPSPackages(ctx, allPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install XBPS packages: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
v.log("Void Linux detected; DMS environment and autostart will be configured in the compositor config instead of systemd")
if err := v.ensureSessionServices(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable Void session services: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if !v.isRunitSystem() {
v.log("Void runit service directory not detected; skipping dbus/elogind service enablement")
return nil
}
// D-Bus activation alone starts elogind without its wrapper mounts; the runit service is required.
for _, service := range []string{"dbus", "elogind"} {
if !v.runitServiceInstalled(service) {
v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service))
continue
}
if v.runitServiceEnabled(service) {
v.log(fmt.Sprintf("Void runit service %s already enabled", service))
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.92,
Step: fmt.Sprintf("Enabling %s runit service...", service),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)),
LogOutput: fmt.Sprintf("Enabling Void runit service: %s", service),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)))
if err := v.runWithProgress(cmd, progressChan, PhaseConfiguration, 0.92, 0.95); err != nil {
return fmt.Errorf("failed to enable %s runit service: %w", service, err)
}
v.log(fmt.Sprintf("✓ Enabled %s runit service", service))
}
return nil
}
func (v *VoidDistribution) isRunitSystem() 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(voidRunitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func (v *VoidDistribution) runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(voidRunitSvDir, name))
return err == nil && fi.IsDir()
}
func (v *VoidDistribution) runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(voidRunitServiceDir, name))
return err == nil
}
func (v *VoidDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping) {
systemPkgs := []string{}
xbpsPkgs := []PackageMapping{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := v.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
v.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeXBPS:
xbpsPkgs = append(xbpsPkgs, pkgInfo)
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
}
}
return systemPkgs, xbpsPkgs
}
func (v *VoidDistribution) enableXBPSRepos(ctx context.Context, xbpsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
enabledRepoURLs := []string{}
for _, pkg := range xbpsPkgs {
if pkg.RepoURL == "" || enabledRepos[pkg.RepoURL] {
continue
}
repoName := v.xbpsRepoName(pkg.RepoURL)
confPath := filepath.Join("/etc/xbps.d", repoName+".conf")
repoLine := fmt.Sprintf("repository=%s", pkg.RepoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
v.log(fmt.Sprintf("XBPS repo %s already configured, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding XBPS repo %s...", repoName),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo 'repository=%s' | sudo tee %s", pkg.RepoURL, confPath),
LogOutput: fmt.Sprintf("Adding XBPS repository: %s", pkg.RepoURL),
}
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/xbps.d")
if err := v.runWithProgress(mkdirCmd, progressChan, PhaseSystemPackages, 0.18, 0.19); err != nil {
return fmt.Errorf("failed to create /etc/xbps.d: %w", err)
}
writeCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c 'printf \"%%s\\n\" %q > %s'", repoLine, confPath))
if err := v.runWithProgress(writeCmd, progressChan, PhaseSystemPackages, 0.19, 0.22); err != nil {
return fmt.Errorf("failed to add XBPS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
}
if len(enabledRepos) > 0 {
syncArgs := []string{"xbps-install", "-Sy", "-i"}
for _, repoURL := range enabledRepoURLs {
syncArgs = append(syncArgs, "--repository", repoURL)
}
syncCommand := strings.Join(syncArgs, " ")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Synchronizing XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo sh -c 'yes y | " + syncCommand + "'",
LogOutput: "Synchronizing XBPS repository indexes",
}
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | "+syncCommand+"'")
if err := v.runWithProgress(syncCmd, progressChan, PhaseSystemPackages, 0.25, 0.30); err != nil {
return fmt.Errorf("failed to synchronize XBPS repositories: %w", err)
}
}
return nil
}
func (v *VoidDistribution) xbpsRepoName(repoURL string) string {
switch repoURL {
case VoidDMSRepo:
return "dms"
case VoidDankLinuxRepo:
return "danklinux"
case VoidHyprlandRepo:
return "hyprland"
default:
name := strings.TrimPrefix(repoURL, "https://")
name = strings.TrimPrefix(name, "http://")
name = strings.NewReplacer("/", "-", ".", "-").Replace(name)
return name
}
}
func (v *VoidDistribution) xbpsArch(ctx context.Context) (string, error) {
output, err := exec.CommandContext(ctx, "xbps-uhelper", "arch").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func (v *VoidDistribution) installXBPSPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
args := append([]string{"xbps-install", "-Sy"}, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing XBPS packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return v.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.85)
}
func (v *VoidDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (v *VoidDistribution) uniquePackageNames(groups ...[]string) []string {
seen := make(map[string]bool)
var unique []string
for _, group := range groups {
for _, pkg := range group {
if pkg == "" || seen[pkg] {
continue
}
seen[pkg] = true
unique = append(unique, pkg)
}
}
return unique
}
+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 (
+31 -1
View File
@@ -6,7 +6,37 @@ func NewClient() Client {
geoclueClient, err := newGeoClueClient() geoclueClient, err := newGeoClueClient()
if err != nil { if err != nil {
log.Warnf("GeoClue2 unavailable: %v", err) log.Warnf("GeoClue2 unavailable: %v", err)
return newIpClient() return newSeededIpClient()
} }
loc, _ := geoclueClient.GetLocation()
if loc.Latitude != 0 || loc.Longitude != 0 {
log.Info("Using GeoClue2 location")
return geoclueClient
}
log.Info("GeoClue2 has no fix yet, seeding with IP location")
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("IP location seed failed: %v", err)
return geoclueClient
}
log.Info("Seeded GeoClue2 with IP location")
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
return geoclueClient return geoclueClient
} }
func newSeededIpClient() *IpClient {
client := newIpClient()
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("IP location also failed: %v", err)
return client
}
log.Info("Using IP location")
client.currLocation.Latitude = ipLoc.Latitude
client.currLocation.Longitude = ipLoc.Longitude
return client
}

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