1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-28 23:42:51 -05:00

Compare commits

..

6 Commits

Author SHA1 Message Date
bbedward
4a68ce35a3 hyprland: remove focus grab from dankpopout 2025-11-16 11:44:59 -05:00
bbedward
eb4655fcbc rebase woes 2025-11-16 11:37:19 -05:00
bbedward
6eb349c9d4 barmask: fix multi-screen handling 2025-11-16 11:33:47 -05:00
bbedward
0a8a7895b3 hyprland: use FocusGrab for ondemand windows 2025-11-16 11:33:47 -05:00
bbedward
73c82a4dd9 dankbar/mask: extra polish for Hyprland maybe 2025-11-16 11:33:47 -05:00
bbedward
ccf28fc4e7 dankbar: add a mask while popouts are open
- Retains ability to click items on the bar, while another is open
2025-11-16 11:32:47 -05:00
607 changed files with 43120 additions and 102630 deletions

View File

@@ -1,4 +1,8 @@
#!/usr/bin/env bash #!/bin/bash
# DISABLED for now
exit 0
set -euo pipefail set -euo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -6,64 +10,18 @@ REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
cd "$REPO_ROOT" cd "$REPO_ROOT"
# ============================================================================= if [[ -z "${POEDITOR_API_TOKEN:-}" ]] || [[ -z "${POEDITOR_PROJECT_ID:-}" ]]; then
# Go CI checks (when core/ files are staged) exit 0
# =============================================================================
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
if [[ -n "$STAGED_CORE_FILES" ]]; then
echo "Go files staged in core/, running CI checks..."
cd "$REPO_ROOT/core"
# Format check
echo " Checking gofmt..."
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
if [[ -n "$UNFORMATTED" ]]; then
echo "The following files are not formatted:"
echo "$UNFORMATTED"
echo ""
echo "Run: cd core && gofmt -s -w ."
exit 1
fi
# golangci-lint
if command -v golangci-lint &>/dev/null; then
echo " Running golangci-lint..."
golangci-lint run ./...
else
echo " Warning: golangci-lint not installed, skipping lint"
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
fi
# Tests
echo " Running tests..."
if ! go test ./... >/dev/null 2>&1; then
echo "Tests failed! Run 'go test ./...' for details."
exit 1
fi
# Build checks
echo " Building..."
mkdir -p bin
go build -buildvcs=false -o bin/dms ./cmd/dms
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
echo "All Go CI checks passed!"
cd "$REPO_ROOT"
fi fi
# ============================================================================= if ! command -v python3 &>/dev/null; then
# i18n sync check (DISABLED for now) exit 0
# ============================================================================= fi
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
# if command -v python3 &>/dev/null; then if ! python3 scripts/i18nsync.py check &>/dev/null; then
# if ! python3 scripts/i18nsync.py check &>/dev/null; then echo "Translations out of sync"
# echo "Translations out of sync" echo "run python3 scripts/i18nsync.py sync"
# echo "Run: python3 scripts/i18nsync.py sync" exit 1
# exit 1 fi
# fi
# fi
# fi
exit 0 exit 0

View File

@@ -1,4 +1,4 @@
name: DMS Copr Stable Release name: DMS Copr Stable Release (Manual)
on: on:
workflow_dispatch: workflow_dispatch:
@@ -62,7 +62,7 @@ jobs:
} }
echo "✅ Source downloaded" echo "✅ Source downloaded"
echo "Note: dms-cli binary will be downloaded during build based on target architecture" echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture"
ls -lh ls -lh
- name: Generate stable spec file - name: Generate stable spec file
@@ -94,7 +94,7 @@ jobs:
Requires: (quickshell or quickshell-git) Requires: (quickshell or quickshell-git)
Requires: accountsservice Requires: accountsservice
Requires: dms-cli = %{version}-%{release} Requires: dms-cli
Requires: dgop Requires: dgop
Recommends: cava Recommends: cava
@@ -125,6 +125,17 @@ jobs:
Command-line interface for DankMaterialShell configuration and management. Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities. Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep %prep
%setup -q -c -n dms-qml %setup -q -c -n dms-qml
@@ -151,10 +162,19 @@ jobs:
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli chmod +x %{_builddir}/dms-cli
# Download dgop for target architecture
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build %build
%install %install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
# Shell completions # Shell completions
install -d %{buildroot}%{_datadir}/bash-completion/completions install -d %{buildroot}%{_datadir}/bash-completion/completions
@@ -182,8 +202,11 @@ jobs:
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi fi
# Signal running DMS instances to reload (harmless if none running)
pkill -USR1 -x dms >/dev/null 2>&1 || : # Restart DMS for active users after upgrade
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files %files
%license LICENSE %license LICENSE
@@ -197,10 +220,14 @@ jobs:
%{_datadir}/zsh/site-functions/_dms %{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish %{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog %changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER - Stable release VERSION_PLACEHOLDER
- Built from GitHub release - Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec

View File

@@ -3,22 +3,13 @@ name: Go CI
on: on:
push: push:
branches: branches:
- "**" - '**'
paths: paths:
- "core/**" - 'core/**'
- ".github/workflows/go-ci.yml" - '.github/workflows/go-ci.yml'
pull_request:
branches: [master, main]
paths:
- "core/**"
- ".github/workflows/go-ci.yml"
concurrency:
group: go-ci-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
lint-and-test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
@@ -41,20 +32,11 @@ jobs:
exit 1 exit 1
fi fi
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.6
working-directory: core
- name: Test - name: Test
run: go test -v ./... run: go test -v ./...
- name: Build dms - name: Build dms
run: go build -v ./cmd/dms run: go build -v ./cmd/dms
- name: Build dms (distropkg)
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

View File

@@ -1,23 +0,0 @@
name: Check nix flake
on:
pull_request:
branches: [master, main]
paths:
- "flake.*"
- "distro/nix/**"
jobs:
check-flake:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Check the flake
run: nix flake check

View File

@@ -1,19 +1,16 @@
name: Release name: Release
on: on:
workflow_dispatch: push:
inputs: tags:
tag: - 'v*'
description: 'Tag to release (e.g., v1.0.1)'
required: true
type: string
permissions: permissions:
contents: write contents: write
actions: write actions: write
concurrency: concurrency:
group: release-${{ inputs.tag }} group: release-${{ github.ref_name }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@@ -27,14 +24,10 @@ jobs:
run: run:
working-directory: core working-directory: core
env:
TAG: ${{ inputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
@@ -61,7 +54,7 @@ jobs:
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=${GITHUB_REF#refs/tags/}" \
-o ../../dankinstall-${{ matrix.arch }} -o ../../dankinstall-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dankinstall-${{ matrix.arch }} gzip -9 -k dankinstall-${{ matrix.arch }}
@@ -75,7 +68,7 @@ jobs:
run: | run: |
set -eux set -eux
cd cmd/dms cd cmd/dms
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \ go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
-o ../../dms-${{ matrix.arch }} -o ../../dms-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dms-${{ matrix.arch }} gzip -9 -k dms-${{ matrix.arch }}
@@ -98,7 +91,7 @@ jobs:
run: | run: |
set -eux set -eux
cd cmd/dms cd cmd/dms
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \ go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
-o ../../dms-distropkg-${{ matrix.arch }} -o ../../dms-distropkg-${{ matrix.arch }}
cd ../.. cd ../..
gzip -9 -k dms-distropkg-${{ matrix.arch }} gzip -9 -k dms-distropkg-${{ matrix.arch }}
@@ -135,61 +128,58 @@ jobs:
core/completion.zsh core/completion.zsh
if-no-files-found: error if-no-files-found: error
# update-versions: update-versions:
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# needs: build-core needs: build-core
# steps: steps:
# - name: Create GitHub App token - name: Checkout
# id: app_token uses: actions/checkout@v4
# uses: actions/create-github-app-token@v1 with:
# with: token: ${{ secrets.GITHUB_TOKEN }}
# app-id: ${{ secrets.APP_ID }} fetch-depth: 0
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Update VERSION
# - name: Checkout run: |
# uses: actions/checkout@v4 set -euo pipefail
# with: git config user.name "github-actions[bot]"
# token: ${{ steps.app_token.outputs.token }} git config user.email "github-actions[bot]@users.noreply.github.com"
# fetch-depth: 0
version="${GITHUB_REF#refs/tags/}"
# - name: Update VERSION version_no_v="${version#v}"
# env: echo "Updating to version: $version"
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
# run: | # Update VERSION file in quickshell/
# set -euo pipefail echo "${version}" > quickshell/VERSION
# git config user.name "dms-ci[bot]"
# git config user.email "dms-ci[bot]@users.noreply.github.com" git add quickshell/VERSION
# version="${GITHUB_REF#refs/tags/}" if ! git diff --cached --quiet; then
# echo "Updating to version: $version" git commit -m "chore: bump version to $version"
# echo "${version}" > quickshell/VERSION git push origin HEAD:master || git push origin HEAD:main
# git add quickshell/VERSION echo "Pushed version updates to master"
else
# if ! git diff --cached --quiet; then echo "No version changes needed"
# git commit -m "chore: bump version to $version" fi
# git pull --rebase origin master
# git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master # Force-push the tag to point to the commit with updated VERSION
# fi git tag -f "${version}"
git push -f origin "${version}"
# git tag -f "${version}"
# git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}" release:
runs-on: ubuntu-24.04
release: needs: [build-core, update-versions]
runs-on: ubuntu-24.04 env:
needs: [build-core] #, update-versions] TAG: ${{ github.ref_name }}
env:
TAG: ${{ inputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
- 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 ${{ github.ref_name }}
git checkout ${TAG} git checkout ${{ github.ref_name }}
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -396,85 +386,15 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
trigger-obs-update:
runs-on: ubuntu-latest
needs: release
env:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Update OBS packages
run: |
cd distro
bash scripts/obs-upload.sh dms "Update to ${TAG}"
trigger-ppa-update:
runs-on: ubuntu-latest
needs: release
env:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Upload to PPA
run: |
cd distro/ubuntu/ppa
bash create-and-upload.sh ../dms dms questing
copr-build: copr-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: release needs: release
env: env:
TAG: ${{ inputs.tag }} TAG: ${{ github.ref_name }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Determine version - name: Determine version
id: version id: version
@@ -527,12 +447,13 @@ jobs:
Requires: (quickshell or quickshell-git) Requires: (quickshell or quickshell-git)
Requires: accountsservice Requires: accountsservice
Requires: dms-cli = %{version}-%{release} Requires: dms-cli
Requires: dgop Requires: dgop
Recommends: cava Recommends: cava
Recommends: cliphist Recommends: cliphist
Recommends: danksearch Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen Recommends: matugen
Recommends: wl-clipboard Recommends: wl-clipboard
Recommends: NetworkManager Recommends: NetworkManager
@@ -557,6 +478,17 @@ jobs:
Command-line interface for DankMaterialShell configuration and management. Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities. Provides native DBus bindings, NetworkManager integration, and system utilities.
%package -n dgop
Summary: Stateless CPU/GPU monitor for DankMaterialShell
License: MIT
URL: https://github.com/AvengeMedia/dgop
Provides: dgop
%description -n dgop
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
network statistics. Designed for integration with DankMaterialShell but can be
used standalone. This package always includes the latest stable dgop release.
%prep %prep
%setup -q -c -n dms-qml %setup -q -c -n dms-qml
@@ -581,10 +513,18 @@ jobs:
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli chmod +x %{_builddir}/dms-cli
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dgop for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
chmod +x %{_builddir}/dgop
%build %build
%install %install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
install -d %{buildroot}%{_datadir}/bash-completion/completions install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions install -d %{buildroot}%{_datadir}/zsh/site-functions
@@ -593,10 +533,7 @@ jobs:
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || : %{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : %{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
install -dm755 %{buildroot}%{_datadir}/quickshell/dms install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/ cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
@@ -606,24 +543,22 @@ jobs:
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans %posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi fi
# Signal running DMS instances to reload
pkill -USR1 -x dms >/dev/null 2>&1 || : if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files %files
%license LICENSE %license LICENSE
%doc README.md CONTRIBUTING.md %doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/ %{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service %{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%files -n dms-cli %files -n dms-cli
%{_bindir}/dms %{_bindir}/dms
@@ -631,10 +566,14 @@ jobs:
%{_datadir}/zsh/site-functions/_dms %{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish %{_datadir}/fish/vendor_completions.d/dms.fish
%files -n dgop
%{_bindir}/dgop
%changelog %changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1 * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
- Stable release VERSION_PLACEHOLDER - Stable release VERSION_PLACEHOLDER
- Built from GitHub release - Built from GitHub release
- Includes latest dms-cli and dgop binaries
SPECEOF SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec

View File

@@ -1,276 +0,0 @@
name: Update OBS Packages
on:
workflow_dispatch:
inputs:
package:
description: 'Package to update (dms, dms-git, or all)'
required: false
default: 'all'
rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
required: false
default: ''
push:
tags:
- 'v*'
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION (always update)"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match spec format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
if [[ -f "dms-git.spec" ]]; then
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract OBS commit, proceeding with update"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No spec file in OBS, proceeding with update"
fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
update-obs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine packages to update
id: packages
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Update dms-git spec version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
# Update version in spec
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
# Add changelog entry
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
# Debian version format: 0.6.2+git2256.9162e314
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
CHANGELOG_DATE=$(date -R)
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
# Get current version from changelog
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
echo "Current Debian version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Only update if version changed
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
# Create new changelog entry at top
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms-git ($NEW_VERSION) nightly; urgency=medium
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
# Prepend to existing changelog
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
else
echo "✓ Debian changelog already at version $NEW_VERSION"
fi
- name: Update dms stable version
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V"
# Update openSUSE dms spec (stable only)
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Update Debian _service files
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
fi
done
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
env:
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
fi
if [[ "$PACKAGES" == "all" ]]; then
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
else
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
fi
- name: Summary
run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,188 +0,0 @@
name: Update PPA Packages
on:
workflow_dispatch:
inputs:
package:
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
required: false
default: 'dms-git'
rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
required: false
default: ''
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match changelog format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Extract commit hash from changelog
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
if [[ -f "$CHANGELOG_FILE" ]]; then
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$CHANGELOG_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract commit from changelog, proceeding with upload"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
upload-ppa:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: false
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Determine packages to upload
id: packages
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Upload to PPA
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms to PPA..."
if [ -n "$REBUILD_RELEASE" ]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-git to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-greeter to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
else
PPA_NAME="$PACKAGES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PACKAGES to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
fi
- name: Summary
run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-git" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY

View File

@@ -1,7 +1,6 @@
name: Update Vendor Hash name: Update Vendor Hash
on: on:
workflow_dispatch:
push: push:
paths: paths:
- "core/go.mod" - "core/go.mod"
@@ -9,25 +8,14 @@ on:
branches: branches:
- master - master
permissions:
contents: write
jobs: jobs:
update-vendor-hash: update-vendor-hash:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31
@@ -35,32 +23,68 @@ jobs:
- name: Update vendorHash in flake.nix - name: Update vendorHash in flake.nix
run: | run: |
set -euo pipefail set -euo pipefail
# Try to build and capture the expected hash from error message
echo "Attempting nix build to get new vendorHash..." echo "Attempting nix build to get new vendorHash..."
if output=$(nix build .#dmsCli 2>&1); then if output=$(nix build .#dmsCli 2>&1); then
echo "Build succeeded, no hash update needed" echo "Build succeeded, no hash update needed"
exit 0 exit 0
fi fi
# Extract the expected hash from the error message
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1) new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
if [ -z "$new_hash" ]; then
echo "Could not extract new vendorHash from build output"
echo "Build output:"
echo "$output"
exit 1
fi
echo "New vendorHash: $new_hash"
# Get current hash from flake.nix
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix) current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; } echo "Current vendorHash: $current_hash"
if [ "$current_hash" = "$new_hash" ]; then
echo "vendorHash is already up to date"
exit 0
fi
# Update the hash in flake.nix
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
# Verify the build works with the new hash
echo "Verifying build with new vendorHash..." echo "Verifying build with new vendorHash..."
nix build .#dmsCli nix build .#dmsCli
echo "vendorHash updated successfully!" echo "vendorHash updated successfully!"
- name: Commit and push vendorHash update - name: Commit and push vendorHash update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: | run: |
set -euo pipefail set -euo pipefail
if ! git diff --quiet flake.nix; then if ! git diff --quiet flake.nix; then
git config user.name "dms-ci[bot]" git config user.name "github-actions[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
git add flake.nix git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0 git commit -m "nix: update vendorHash for go.mod changes"
git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master for attempt in 1 2 3; do
if git push; then
echo "Successfully pushed vendorHash update"
exit 0
fi
echo "Push attempt $attempt failed, pulling and retrying..."
git pull --rebase
sleep $((attempt*2))
done
echo "Failed to push after retries" >&2
exit 1
else else
echo "No changes to flake.nix" echo "No changes to flake.nix"
fi fi

43
.gitignore vendored
View File

@@ -102,14 +102,37 @@ go.work.sum
# .idea/ # .idea/
# .vscode/ # .vscode/
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# Editor/IDE
# .idea/
# .vscode/
bin/ bin/
# Extracted source trees in Ubuntu package directories
distro/ubuntu/*/dms-git-repo/
distro/ubuntu/*/DankMaterialShell-*/
distro/ubuntu/danklinux/*/dsearch-*/
distro/ubuntu/danklinux/*/dgop-*/
# direnv
.envrc
.direnv/

View File

@@ -2,65 +2,28 @@
Contributions are welcome and encouraged. Contributions are welcome and encouraged.
To contribute fork this repository, make your changes, and open a pull request. ## Formatting
## Setup The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported.
Enable pre-commit hooks to catch CI failures before pushing: We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
```bash You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
git config core.hooksPath .githooks
```
### Nix Development Shell
If you have Nix installed with flakes enabled, you can use the provided development shell which includes all necessary dependencies:
```bash
nix develop
```
This will provide:
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
- Quickshell and required QML packages
- Properly configured QML2_IMPORT_PATH
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
## 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.
### QML (`quickshell` directory)
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
```json ```json
{ "customLocalFormatters.formatters": [
"qt-qml.doNotAskForQmllsDownload": true, {
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls" "command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
} "languages": ["qml"]
}
],
"[qml]": {
"editor.defaultFormatter": "jkillian.custom-local-formatters",
"editor.formatOnSave": true
},
``` ```
3. Create empty `.qmlls.ini` file in `quickshell/` directory Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
```bash
cd quickshell
touch .qmlls.ini
```
4. Restart dms to generate the `.qmlls.ini` file
5. Make your changes, test, and open a pull request.
### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
2. Ensure code is formatted with `make fmt`
3. Add appropriate test coverage and ensure tests pass with `make test`
4. Run `go mod tidy`
5. Open pull request
## Pull request ## Pull request

156
Makefile
View File

@@ -1,156 +0,0 @@
# Root Makefile for DankMaterialShell (DMS)
# Orchestrates building, installation, and systemd management
# Build configuration
BINARY_NAME=dms
CORE_DIR=core
BUILD_DIR=$(CORE_DIR)/bin
PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin
DATA_DIR=$(PREFIX)/share
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user
SHELL_DIR=quickshell
SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
all: build
build:
@echo "Building $(BINARY_NAME)..."
@$(MAKE) -C $(CORE_DIR) build
@echo "Build complete"
clean:
@echo "Cleaning build artifacts..."
@$(MAKE) -C $(CORE_DIR) clean
@echo "Clean complete"
# Installation targets
install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Binary installed"
install-shell:
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
@mkdir -p $(SHELL_INSTALL_DIR)
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
@echo "Shell files installed"
install-completions:
@echo "Installing shell completions..."
@mkdir -p $(DATA_DIR)/bash-completion/completions
@mkdir -p $(DATA_DIR)/zsh/site-functions
@mkdir -p $(DATA_DIR)/fish/vendor_completions.d
@$(BUILD_DIR)/$(BINARY_NAME) completion bash > $(DATA_DIR)/bash-completion/completions/dms 2>/dev/null || true
@$(BUILD_DIR)/$(BINARY_NAME) completion zsh > $(DATA_DIR)/zsh/site-functions/_dms 2>/dev/null || true
@$(BUILD_DIR)/$(BINARY_NAME) completion fish > $(DATA_DIR)/fish/vendor_completions.d/dms.fish 2>/dev/null || true
@echo "Shell completions installed"
install-systemd:
@echo "Installing systemd user service..."
@mkdir -p $(SYSTEMD_USER_DIR)
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
@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
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
install-icon:
@echo "Installing icon..."
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
@echo "Icon installed"
install-desktop:
@echo "Installing desktop entry..."
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry installed"
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
@echo ""
@echo "Installation complete!"
@echo ""
@echo "=== Cheers, the DMS Team! ==="
# Uninstallation targets
uninstall-bin:
@echo "Removing $(BINARY_NAME) from $(INSTALL_DIR)..."
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Binary removed"
uninstall-shell:
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
@rm -rf $(SHELL_INSTALL_DIR)
@echo "Shell files removed"
uninstall-completions:
@echo "Removing shell completions..."
@rm -f $(DATA_DIR)/bash-completion/completions/dms
@rm -f $(DATA_DIR)/zsh/site-functions/_dms
@rm -f $(DATA_DIR)/fish/vendor_completions.d/dms.fish
@echo "Shell completions removed"
uninstall-systemd:
@echo "Removing systemd user service..."
@rm -f $(SYSTEMD_USER_DIR)/dms.service
@echo "Systemd service removed"
@echo "Note: Stop/disable service manually if running: systemctl --user stop dms"
uninstall-icon:
@echo "Removing icon..."
@rm -f $(ICON_DIR)/danklogo.svg
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
@echo "Icon removed"
uninstall-desktop:
@echo "Removing desktop entry..."
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry removed"
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
@echo ""
@echo "Uninstallation complete!"
# Target assist
help:
@echo "Available targets:"
@echo ""
@echo "Build:"
@echo " all (default) - Build the DMS binary"
@echo " build - Same as 'all'"
@echo " clean - Clean build artifacts"
@echo ""
@echo "Install:"
@echo " install - Build and install everything (requires sudo)"
@echo " install-bin - Install only the binary"
@echo " install-shell - Install only shell files"
@echo " install-completions - Install only shell completions"
@echo " install-systemd - Install only systemd service"
@echo " install-icon - Install only icon"
@echo " install-desktop - Install only desktop entry"
@echo ""
@echo "Uninstall:"
@echo " uninstall - Remove everything (requires sudo)"
@echo " uninstall-bin - Remove only the binary"
@echo " uninstall-shell - Remove only shell files"
@echo " uninstall-completions - Remove only shell completions"
@echo " uninstall-systemd - Remove only systemd service"
@echo " uninstall-icon - Remove only icon"
@echo " uninstall-desktop - Remove only desktop entry"
@echo ""
@echo "Usage:"
@echo " sudo make install - Build and install DMS"
@echo " sudo make uninstall - Remove DMS"
@echo " systemctl --user enable --now dms - Enable and start service"

View File

@@ -5,21 +5,21 @@
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200"> <img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
</a> </a>
### A modern desktop shell for Wayland ### A modern desktop shell for Wayland
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/) Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
[![Documentation](https://img.shields.io/badge/docs-danklinux.com-9ccbfb?style=for-the-badge&labelColor=101418)](https://danklinux.com/docs) [![Documentation](https://img.shields.io/badge/docs-danklinux.com-9ccbfb?style=for-the-badge&labelColor=101418)](https://danklinux.com/docs)
[![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers) [![GitHub stars](https://img.shields.io/github/stars/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=ffd700)](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
[![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE) [![GitHub License](https://img.shields.io/github/license/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=b9c8da)](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases) [![GitHub release](https://img.shields.io/github/v/release/AvengeMedia/DankMaterialShell?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://github.com/AvengeMedia/DankMaterialShell/releases)
[![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin) [![AUR version](https://img.shields.io/aur/version/dms-shell-bin?style=for-the-badge&labelColor=101418&color=9ccbfb)](https://aur.archlinux.org/packages/dms-shell-bin)
[![AUR version (git)](<https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git)>)](https://aur.archlinux.org/packages/dms-shell-git) [![AUR version (git)](https://img.shields.io/aur/version/dms-shell-git?style=for-the-badge&labelColor=101418&color=9ccbfb&label=AUR%20(git))](https://aur.archlinux.org/packages/dms-shell-git)
[![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Fdanklinux)](https://ko-fi.com/danklinux) [![Ko-Fi donate](https://img.shields.io/badge/donate-kofi?style=for-the-badge&logo=ko-fi&logoColor=ffffff&label=ko-fi&labelColor=101418&color=f16061&link=https%3A%2F%2Fko-fi.com%2Favengemediallc)](https://ko-fi.com/avengemediallc)
</div> </div>
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop. DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure ## Repository Structure
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors ## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features. Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), and [MangoWC](https://github.com/DreamMaoMao/mangowc) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors) [Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
@@ -127,7 +127,7 @@ dms plugins search # Browse plugin registry
## Documentation ## Documentation
- **Website:** [danklinux.com](https://danklinux.com) - **Website:** [danklinux.com](https://danklinux.com)
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs/) - **Docs:** [danklinux.com/docs](https://danklinux.com/docs)
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes) - **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
- **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)
@@ -143,7 +143,6 @@ See component-specific documentation:
### Building from Source ### Building from Source
**Core + Dankinstall:** **Core + Dankinstall:**
```bash ```bash
cd core cd core
make # Build dms CLI make # Build dms CLI
@@ -151,13 +150,11 @@ make dankinstall # Build installer
``` ```
**Shell:** **Shell:**
```bash ```bash
quickshell -p quickshell/ quickshell -p quickshell/
``` ```
**NixOS:** **NixOS:**
```nix ```nix
{ {
inputs.dms.url = "github:AvengeMedia/DankMaterialShell"; inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
@@ -186,10 +183,6 @@ For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeM
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration - [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration - [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=AvengeMedia/DankMaterialShell&type=date&legend=top-left)](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
## License ## License
MIT License - See [LICENSE](LICENSE) for details. MIT License - See [LICENSE](LICENSE) for details.

1
alejandra.toml Normal file
View File

@@ -0,0 +1 @@
indentation = "FourSpaces"

View File

@@ -1,10 +0,0 @@
[Desktop Entry]
Type=Application
Name=DMS Application Picker
Comment=Select an application to open links and files
Exec=dms open %u
Icon=danklogo
Terminal=false
NoDisplay=true
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
Categories=Utility;

View File

@@ -1,108 +0,0 @@
version: "2"
linters:
enable:
- revive
settings:
revive:
rules:
- name: use-any
severity: error
errcheck:
check-type-assertions: false
check-blank: false
exclude-functions:
# Cleanup/destroy operations
- (io.Closer).Close
- (*os.File).Close
- (net.Conn).Close
- (*net.Conn).Close
# Signal handling
- (*os.Process).Signal
- (*os.Process).Kill
- syscall.Kill
# Seek on memfd (reset position before passing fd)
- syscall.Seek
# DBus cleanup
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
# Encoding to network connections (if conn is bad, nothing we can do)
- (*encoding/json.Encoder).Encode
- (net.Conn).Write
# Command execution where failure is expected/ignored
- (*os/exec.Cmd).Run
- (*os/exec.Cmd).Start
# Flush operations
- (*bufio.Writer).Flush
# Scanning user input
- fmt.Scanln
- fmt.Scanf
# Parse operations where default value is acceptable
- fmt.Sscanf
# Flag operations
- (*github.com/spf13/pflag.FlagSet).MarkHidden
# Binary encoding to buffer (can't fail for basic types)
- binary.Write
# File operations in cleanup paths
- os.Rename
- os.Remove
- os.RemoveAll
- (*os.File).WriteString
# Stdout/stderr writes (can't meaningfully handle failure)
- fmt.Fprintln
- fmt.Fprintf
- fmt.Fprint
# Writing to pipes (if pipe is bad, nothing we can do)
- (*io.PipeWriter).Write
- (*os.File).Write
exclusions:
rules:
# Exclude generated mocks from all linters
- path: internal/mocks/
linters:
- errcheck
- govet
- unused
- ineffassign
- staticcheck
- gosimple
- revive
- path: _test\.go
linters:
- errcheck
- govet
- unused
- ineffassign
- staticcheck
- gosimple
# Exclude cleanup/teardown method calls from errcheck
- linters:
- errcheck
text: "Error return value of `.+\\.(Destroy|Release|Stop|Close|Roundtrip|Store)` is not checked"
# Exclude internal state update methods that are best-effort
- linters:
- errcheck
text: "Error return value of `[mb]\\.\\w*(update|initialize|recreate|acquire|enumerate|list|List|Ensure|refresh|Lock)\\w*` is not checked"
# Exclude SetMode on wayland power controls (best-effort)
- linters:
- errcheck
text: "Error return value of `.+\\.SetMode` is not checked"
# Exclude AddMatchSignal which is best-effort monitoring setup
- linters:
- errcheck
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)
- linters:
- ineffassign
path: internal/proto/
# binary.Write to bytes.Buffer can't fail
- linters:
- errcheck
text: "Error return value of `binary\\.Write` is not checked"

View File

@@ -28,31 +28,21 @@ packages:
outpkg: mocks_brightness outpkg: mocks_brightness
interfaces: interfaces:
DBusConn: DBusConn:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network: github.com/AvengeMedia/danklinux/internal/server/network:
config: config:
dir: "internal/mocks/network" dir: "internal/mocks/network"
outpkg: mocks_network outpkg: mocks_network
interfaces: interfaces:
Backend: Backend:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups: github.com/AvengeMedia/danklinux/internal/server/cups:
config: config:
dir: "internal/mocks/cups" dir: "internal/mocks/cups"
outpkg: mocks_cups outpkg: mocks_cups
interfaces: interfaces:
CUPSClientInterface: CUPSClientInterface:
PkHelper:
config:
dir: "internal/mocks/cups_pkhelper"
outpkg: mocks_cups_pkhelper
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev: github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
config: config:
dir: "internal/mocks/evdev" dir: "internal/mocks/evdev"
outpkg: mocks_evdev outpkg: mocks_evdev
interfaces: interfaces:
EvdevDevice: EvdevDevice:
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
config:
dir: "internal/mocks/version"
outpkg: mocks_version
interfaces:
VersionFetcher:

View File

@@ -10,19 +10,16 @@ GO=go
GOFLAGS=-ldflags="-s -w" GOFLAGS=-ldflags="-s -w"
# Version and build info # Version and build info
BASE_VERSION=$(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0") VERSION=$(shell git describe --tags --always 2>/dev/null || echo "dev")
COMMIT_COUNT=$(shell git rev-list --count HEAD 2>/dev/null || echo "0") BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
COMMIT_HASH=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "unknown") COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
VERSION?=$(BASE_VERSION)+git$(COMMIT_COUNT).$(COMMIT_HASH)
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
COMMIT?=$(COMMIT_HASH)
BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.commit=$(COMMIT)' BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.commit=$(COMMIT)'
# Architecture to build for dist target (amd64, arm64, or all) # Architecture to build for dist target (amd64, arm64, or all)
ARCH ?= all 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 .PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps help
# Default target # Default target
all: build all: build
@@ -135,9 +132,6 @@ version: check-go
@echo "Build Time: $(BUILD_TIME)" @echo "Build Time: $(BUILD_TIME)"
@echo "Commit: $(COMMIT)" @echo "Commit: $(COMMIT)"
print-version:
@echo "$(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)"

View File

@@ -16,9 +16,6 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
**Wayland Protocols** **Wayland Protocols**
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control - `wlr-gamma-control-unstable-v1` - Night mode and gamma control
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
- `wp-viewporter` - Fractional scaling support
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration - `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
- `ext-workspace-v1` - Workspace protocol support - `ext-workspace-v1` - Workspace protocol support
- `wlr-output-management-unstable-v1` - Display configuration - `wlr-output-management-unstable-v1` - Display configuration
@@ -47,24 +44,9 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
- `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]` - Plugin management - `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 update` - Update DMS and dependencies (disabled in distro packages) - `dms update` - Update DMS and dependencies (disabled in distro packages)
- `dms greeter install` - Install greetd greeter (disabled in distro packages) - `dms greeter install` - Install greetd greeter (disabled in distro packages)
### Color Picker
Native Wayland color picker with magnifier, no external dependencies. Supports HiDPI and fractional scaling.
```bash
dms color pick # Pick color, output hex
dms color pick --rgb # Output as RGB (255 128 64)
dms color pick --hsv # Output as HSV (24 75% 100%)
dms color pick --json # Output all formats as JSON
dms color pick -a # Auto-copy to clipboard
```
The on-screen preview displays the selected format. JSON output includes hex, RGB, HSL, HSV, and CMYK values.
## Building ## Building
Requires Go 1.24+ Requires Go 1.24+
@@ -90,13 +72,6 @@ sudo make install # Install to /usr/local/bin/dms
## Development ## Development
**Setup pre-commit hooks:**
```bash
git config core.hooksPath .githooks
```
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
**Regenerating Wayland Protocol Bindings:** **Regenerating Wayland Protocol Bindings:**
```bash ```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest

View File

@@ -12,11 +12,6 @@ import (
var Version = "dev" var Version = "dev"
func main() { func main() {
if os.Getuid() == 0 {
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
os.Exit(1)
}
fileLogger, err := log.NewFileLogger() fileLogger, err := log.NewFileLogger()
if err != nil { if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err) fmt.Printf("Warning: Failed to create log file: %v\n", err)

View File

@@ -28,14 +28,7 @@ var brightnessSetCmd = &cobra.Command{
Short: "Set brightness for a device", Short: "Set brightness for a device",
Long: "Set brightness percentage (0-100) for a specific device", Long: "Set brightness percentage (0-100) for a specific device",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { Run: runBrightnessSet,
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
includeDDC, _ := cmd.Flags().GetBool("ddc")
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
},
Run: runBrightnessSet,
} }
var brightnessGetCmd = &cobra.Command{ var brightnessGetCmd = &cobra.Command{
@@ -43,14 +36,7 @@ var brightnessGetCmd = &cobra.Command{
Short: "Get brightness for a device", Short: "Get brightness for a device",
Long: "Get current brightness percentage for a specific device", Long: "Get current brightness percentage for a specific device",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { Run: runBrightnessGet,
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
includeDDC, _ := cmd.Flags().GetBool("ddc")
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
},
Run: runBrightnessGet,
} }
func init() { func init() {
@@ -119,7 +105,9 @@ Global Flags:
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd) brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
} }
func getAllBrightnessDevices(includeDDC bool) []brightness.Device { func runBrightnessList(cmd *cobra.Command, args []string) {
includeDDC, _ := cmd.Flags().GetBool("ddc")
allDevices := []brightness.Device{} allDevices := []brightness.Device{}
sysfs, err := brightness.NewSysfsBackend() sysfs, err := brightness.NewSysfsBackend()
@@ -150,13 +138,6 @@ func getAllBrightnessDevices(includeDDC bool) []brightness.Device {
} }
} }
return allDevices
}
func runBrightnessList(cmd *cobra.Command, args []string) {
includeDDC, _ := cmd.Flags().GetBool("ddc")
allDevices := getAllBrightnessDevices(includeDDC)
if len(allDevices) == 0 { if len(allDevices) == 0 {
fmt.Println("No brightness devices found") fmt.Println("No brightness devices found")
return return
@@ -211,13 +192,45 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
exponential, _ := cmd.Flags().GetBool("exponential") exponential, _ := cmd.Flags().GetBool("exponential")
exponent, _ := cmd.Flags().GetFloat64("exponent") exponent, _ := cmd.Flags().GetFloat64("exponent")
// For backlight/leds devices, try logind backend first (requires D-Bus connection)
parts := strings.SplitN(deviceID, ":", 2) parts := strings.SplitN(deviceID, ":", 2)
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") { if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
if ok := tryLogindBrightness(parts[0], parts[1], deviceID, percent, exponential, exponent); ok { subsystem := parts[0]
return name := parts[1]
// Initialize backends needed for logind approach
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("NewSysfsBackend failed: %v", err)
} else {
logind, err := brightness.NewLogindBackend()
if err != nil {
log.Debugf("NewLogindBackend failed: %v", err)
} else {
defer logind.Close()
// Get device info to convert percent to value
dev, err := sysfs.GetDevice(deviceID)
if err == nil {
// Calculate hardware value using the same logic as Manager.setViaSysfsWithLogind
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
// Call logind with hardware value
if err := logind.SetBrightness(subsystem, name, uint32(value)); err == nil {
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return
} else {
log.Debugf("logind.SetBrightness failed: %v", err)
}
} else {
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
}
}
} }
} }
// Fallback to direct sysfs (requires write permissions)
sysfs, err := brightness.NewSysfsBackend() sysfs, err := brightness.NewSysfsBackend()
if err == nil { if err == nil {
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil { if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
@@ -248,51 +261,31 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
log.Fatalf("Failed to set brightness for device: %s", deviceID) log.Fatalf("Failed to set brightness for device: %s", deviceID)
} }
func tryLogindBrightness(subsystem, name, deviceID string, percent int, exponential bool, exponent float64) bool {
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("NewSysfsBackend failed: %v", err)
return false
}
logind, err := brightness.NewLogindBackend()
if err != nil {
log.Debugf("NewLogindBackend failed: %v", err)
return false
}
defer logind.Close()
dev, err := sysfs.GetDevice(deviceID)
if err != nil {
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
return false
}
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
if err := logind.SetBrightness(subsystem, name, uint32(value)); err != nil {
log.Debugf("logind.SetBrightness failed: %v", err)
return false
}
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return true
}
func getBrightnessDevices(includeDDC bool) []string {
allDevices := getAllBrightnessDevices(includeDDC)
var deviceIDs []string
for _, device := range allDevices {
deviceIDs = append(deviceIDs, device.ID)
}
return deviceIDs
}
func runBrightnessGet(cmd *cobra.Command, args []string) { func runBrightnessGet(cmd *cobra.Command, args []string) {
deviceID := args[0] deviceID := args[0]
includeDDC, _ := cmd.Flags().GetBool("ddc") includeDDC, _ := cmd.Flags().GetBool("ddc")
allDevices := getAllBrightnessDevices(includeDDC)
allDevices := []brightness.Device{}
sysfs, err := brightness.NewSysfsBackend()
if err == nil {
devices, err := sysfs.GetDevices()
if err == nil {
allDevices = append(allDevices, devices...)
}
}
if includeDDC {
ddc, err := brightness.NewDDCBackend()
if err == nil {
defer ddc.Close()
time.Sleep(100 * time.Millisecond)
devices, err := ddc.GetDevices()
if err == nil {
allDevices = append(allDevices, devices...)
}
}
}
for _, device := range allDevices { for _, device := range allDevices {
if device.ID == deviceID { if device.ID == deviceID {

View File

@@ -1,133 +0,0 @@
package main
import (
"fmt"
"os"
"os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
"github.com/spf13/cobra"
)
var (
colorOutputFmt string
colorAutocopy bool
colorNotify bool
colorLowercase bool
)
var colorCmd = &cobra.Command{
Use: "color",
Short: "Color utilities",
Long: "Color utilities including picking colors from the screen",
}
var colorPickCmd = &cobra.Command{
Use: "pick",
Short: "Pick a color from the screen",
Long: `Pick a color from anywhere on your screen using an interactive color picker.
Click on any pixel to capture its color, or press Escape to cancel.
Output format flags (mutually exclusive, default: --hex):
--hex - Hexadecimal (#RRGGBB)
--rgb - RGB values (R G B)
--hsl - HSL values (H S% L%)
--hsv - HSV values (H S% V%)
--cmyk - CMYK values (C% M% Y% K%)
--json - JSON with all formats
Examples:
dms color pick # Pick color, output as hex
dms color pick --rgb # Output as RGB
dms color pick --json # Output all formats as JSON
dms color pick --hex -l # Output hex in lowercase
dms color pick -a # Auto-copy result to clipboard`,
Run: runColorPick,
}
func init() {
colorPickCmd.Flags().Bool("hex", false, "Output as hexadecimal (#RRGGBB)")
colorPickCmd.Flags().Bool("rgb", false, "Output as RGB (R G B)")
colorPickCmd.Flags().Bool("hsl", false, "Output as HSL (H S% L%)")
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
colorCmd.AddCommand(colorPickCmd)
}
func runColorPick(cmd *cobra.Command, args []string) {
format := colorpicker.FormatHex // default
jsonOutput, _ := cmd.Flags().GetBool("json")
if rgb, _ := cmd.Flags().GetBool("rgb"); rgb {
format = colorpicker.FormatRGB
} else if hsl, _ := cmd.Flags().GetBool("hsl"); hsl {
format = colorpicker.FormatHSL
} else if hsv, _ := cmd.Flags().GetBool("hsv"); hsv {
format = colorpicker.FormatHSV
} else if cmyk, _ := cmd.Flags().GetBool("cmyk"); cmyk {
format = colorpicker.FormatCMYK
}
config := colorpicker.Config{
Format: format,
CustomFormat: colorOutputFmt,
Lowercase: colorLowercase,
Autocopy: colorAutocopy,
Notify: colorNotify,
}
picker := colorpicker.New(config)
color, err := picker.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if color == nil {
os.Exit(0)
}
var output string
if jsonOutput {
jsonStr, err := color.ToJSON()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
output = jsonStr
} else {
output = color.Format(config.Format, config.Lowercase, config.CustomFormat)
}
if colorAutocopy {
copyToClipboard(output)
}
if jsonOutput {
fmt.Println(output)
} else if color.IsDark() {
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
} else {
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
}
}
func copyToClipboard(text string) {
var cmd *exec.Cmd
if _, err := exec.LookPath("wl-copy"); err == nil {
cmd = exec.Command("wl-copy", text)
} else {
fmt.Fprintln(os.Stderr, "wl-copy not found, cannot copy to clipboard")
return
}
_ = cmd.Run()
}

View File

@@ -2,8 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"os"
"regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -68,10 +66,6 @@ var ipcCmd = &cobra.Command{
Short: "Send IPC commands to running DMS shell", Short: "Send IPC commands to running DMS shell",
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)", Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
PreRunE: findConfig, PreRunE: findConfig,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
runShellIPCCommand(args) runShellIPCCommand(args)
}, },
@@ -121,12 +115,6 @@ var pluginsInstallCmd = &cobra.Command{
Short: "Install a plugin by ID", Short: "Install a plugin by ID",
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.", Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getAvailablePluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := installPluginCLI(args[0]); err != nil { if err := installPluginCLI(args[0]); err != nil {
log.Fatalf("Error installing plugin: %v", err) log.Fatalf("Error installing plugin: %v", err)
@@ -139,12 +127,6 @@ var pluginsUninstallCmd = &cobra.Command{
Short: "Uninstall a plugin by ID", Short: "Uninstall a plugin by ID",
Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.", Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := uninstallPluginCLI(args[0]); err != nil { if err := uninstallPluginCLI(args[0]); err != nil {
log.Fatalf("Error uninstalling plugin: %v", err) log.Fatalf("Error uninstalling plugin: %v", err)
@@ -152,79 +134,12 @@ var pluginsUninstallCmd = &cobra.Command{
}, },
} }
var pluginsUpdateCmd = &cobra.Command{
Use: "update <plugin-id>",
Short: "Update a plugin by ID",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if err := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err)
}
},
}
func runVersion(cmd *cobra.Command, args []string) { func runVersion(cmd *cobra.Command, args []string) {
printASCII() printASCII()
fmt.Printf("%s\n", formatVersion(Version)) fmt.Printf("%s\n", Version)
}
// Git builds: dms (git) v0.6.2-XXXX
// Stable releases: dms v0.6.2
func formatVersion(version string) string {
// Arch/Debian/Ubuntu/OpenSUSE git format: 0.6.2+git2264.c5c5ce84
re := regexp.MustCompile(`^([\d.]+)\+git(\d+)\.`)
if matches := re.FindStringSubmatch(version); matches != nil {
return fmt.Sprintf("dms (git) v%s-%s", matches[1], matches[2])
}
// Fedora COPR git format: 0.0.git.2267.d430cae9
re = regexp.MustCompile(`^[\d.]+\.git\.(\d+)\.`)
if matches := re.FindStringSubmatch(version); matches != nil {
baseVersion := getBaseVersion()
return fmt.Sprintf("dms (git) v%s-%s", baseVersion, matches[1])
}
// Stable release format: 0.6.2
re = regexp.MustCompile(`^([\d.]+)$`)
if matches := re.FindStringSubmatch(version); matches != nil {
return fmt.Sprintf("dms v%s", matches[1])
}
return fmt.Sprintf("dms %s", version)
}
func getBaseVersion() string {
paths := []string{
"/usr/share/quickshell/dms/VERSION",
"/usr/local/share/quickshell/dms/VERSION",
"/etc/xdg/quickshell/dms/VERSION",
}
for _, path := range paths {
if content, err := os.ReadFile(path); err == nil {
ver := strings.TrimSpace(string(content))
ver = strings.TrimPrefix(ver, "v")
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
if matches := re.FindStringSubmatch(ver); matches != nil {
return matches[1]
}
}
}
}
// Fallback
return "0.6.2"
} }
func startDebugServer() error { func startDebugServer() error {
server.CLIVersion = Version
return server.Start(true) return server.Start(true)
} }
@@ -383,38 +298,6 @@ func installPluginCLI(idOrName string) error {
return nil return nil
} }
func getAvailablePluginIDs() []string {
registry, err := plugins.NewRegistry()
if err != nil {
return nil
}
pluginList, err := registry.List()
if err != nil {
return nil
}
var ids []string
for _, p := range pluginList {
ids = append(ids, p.ID)
}
return ids
}
func getInstalledPluginIDs() []string {
manager, err := plugins.NewManager()
if err != nil {
return nil
}
installed, err := manager.ListInstalled()
if err != nil {
return nil
}
return installed
}
func uninstallPluginCLI(idOrName string) error { func uninstallPluginCLI(idOrName string) error {
manager, err := plugins.NewManager() manager, err := plugins.NewManager()
if err != nil { if err != nil {
@@ -426,73 +309,53 @@ func uninstallPluginCLI(idOrName string) error {
return fmt.Errorf("failed to create registry: %w", err) return fmt.Errorf("failed to create registry: %w", err)
} }
pluginList, _ := registry.List() pluginList, err := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList) if err != nil {
return fmt.Errorf("failed to list plugins: %w", err)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Uninstall(*plugin); err != nil {
return fmt.Errorf("failed to uninstall plugin: %w", err)
}
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
return nil
} }
fmt.Printf("Uninstalling plugin: %s\n", idOrName) // First, try to find by ID (preferred method)
if err := manager.UninstallByIDOrName(idOrName); err != nil { var plugin *plugins.Plugin
return err for _, p := range pluginList {
if p.ID == idOrName {
plugin = &p
break
}
} }
fmt.Printf("Plugin uninstalled successfully: %s\n", idOrName)
return nil // Fallback to name for backward compatibility
} if plugin == nil {
for _, p := range pluginList {
func updatePluginCLI(idOrName string) error { if p.Name == idOrName {
manager, err := plugins.NewManager() plugin = &p
if err != nil { break
return fmt.Errorf("failed to create manager: %w", err) }
} }
}
registry, err := plugins.NewRegistry()
if err != nil { if plugin == nil {
return fmt.Errorf("failed to create registry: %w", err) return fmt.Errorf("plugin not found: %s", idOrName)
} }
pluginList, _ := registry.List() installed, err := manager.IsInstalled(*plugin)
plugin := plugins.FindByIDOrName(idOrName, pluginList) if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
if plugin != nil { }
installed, err := manager.IsInstalled(*plugin)
if err != nil { if !installed {
return fmt.Errorf("failed to check install status: %w", err) return fmt.Errorf("plugin not installed: %s", plugin.Name)
} }
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name) fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
} if err := manager.Uninstall(*plugin); err != nil {
return fmt.Errorf("failed to uninstall plugin: %w", err)
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID) }
if err := manager.Update(*plugin); err != nil {
return fmt.Errorf("failed to update plugin: %w", err) fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
}
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
return nil
}
fmt.Printf("Updating plugin: %s\n", idOrName)
if err := manager.UpdateByIDOrName(idOrName); err != nil {
return err
}
fmt.Printf("Plugin updated successfully: %s\n", idOrName)
return nil return nil
} }
// getCommonCommands returns the commands available in all builds
func getCommonCommands() []*cobra.Command { func getCommonCommands() []*cobra.Command {
return []*cobra.Command{ return []*cobra.Command{
versionCmd, versionCmd,
@@ -505,13 +368,8 @@ func getCommonCommands() []*cobra.Command {
pluginsCmd, pluginsCmd,
dank16Cmd, dank16Cmd,
brightnessCmd, brightnessCmd,
dpmsCmd,
keybindsCmd, keybindsCmd,
greeterCmd, greeterCmd,
setupCmd, setupCmd,
colorCmd,
screenshotCmd,
notifyActionCmd,
matugenCmd,
} }
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16" "github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
@@ -10,32 +11,32 @@ import (
) )
var dank16Cmd = &cobra.Command{ var dank16Cmd = &cobra.Command{
Use: "dank16 [hex_color]", Use: "dank16 <hex_color>",
Short: "Generate Base16 color palettes", Short: "Generate Base16 color palettes",
Long: "Generate Base16 color palettes from a color with support for various output formats", Long: "Generate Base16 color palettes from a color with support for various output formats",
Args: cobra.MaximumNArgs(1), Args: cobra.ExactArgs(1),
Run: runDank16, Run: runDank16,
} }
func init() { func init() {
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant (sets default to light)") dank16Cmd.Flags().Bool("light", false, "Generate light theme variant")
dank16Cmd.Flags().Bool("json", false, "Output in JSON format") dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format") dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format") dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format") dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format") dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format") dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
dank16Cmd.Flags().String("vscode-enrich", "", "Enrich existing VSCode theme file with terminal colors")
dank16Cmd.Flags().String("background", "", "Custom background color") dank16Cmd.Flags().String("background", "", "Custom background color")
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag") dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
dank16Cmd.Flags().Bool("variants", false, "Output all variants (dark/light/default) in JSON")
dank16Cmd.Flags().String("primary-dark", "", "Primary color for dark mode (use with --variants)")
dank16Cmd.Flags().String("primary-light", "", "Primary color for light mode (use with --variants)")
_ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp
})
} }
func runDank16(cmd *cobra.Command, args []string) { func runDank16(cmd *cobra.Command, args []string) {
primaryColor := args[0]
if !strings.HasPrefix(primaryColor, "#") {
primaryColor = "#" + primaryColor
}
isLight, _ := cmd.Flags().GetBool("light") isLight, _ := cmd.Flags().GetBool("light")
isJson, _ := cmd.Flags().GetBool("json") isJson, _ := cmd.Flags().GetBool("json")
isKitty, _ := cmd.Flags().GetBool("kitty") isKitty, _ := cmd.Flags().GetBool("kitty")
@@ -43,59 +44,19 @@ func runDank16(cmd *cobra.Command, args []string) {
isAlacritty, _ := cmd.Flags().GetBool("alacritty") isAlacritty, _ := cmd.Flags().GetBool("alacritty")
isGhostty, _ := cmd.Flags().GetBool("ghostty") isGhostty, _ := cmd.Flags().GetBool("ghostty")
isWezterm, _ := cmd.Flags().GetBool("wezterm") isWezterm, _ := cmd.Flags().GetBool("wezterm")
vscodeEnrich, _ := cmd.Flags().GetString("vscode-enrich")
background, _ := cmd.Flags().GetString("background") background, _ := cmd.Flags().GetString("background")
contrastAlgo, _ := cmd.Flags().GetString("contrast") contrastAlgo, _ := cmd.Flags().GetString("contrast")
useVariants, _ := cmd.Flags().GetBool("variants")
primaryDark, _ := cmd.Flags().GetString("primary-dark")
primaryLight, _ := cmd.Flags().GetString("primary-light")
if background != "" && !strings.HasPrefix(background, "#") { if background != "" && !strings.HasPrefix(background, "#") {
background = "#" + background background = "#" + background
} }
if primaryDark != "" && !strings.HasPrefix(primaryDark, "#") {
primaryDark = "#" + primaryDark
}
if primaryLight != "" && !strings.HasPrefix(primaryLight, "#") {
primaryLight = "#" + primaryLight
}
contrastAlgo = strings.ToLower(contrastAlgo) contrastAlgo = strings.ToLower(contrastAlgo)
if contrastAlgo != "dps" && contrastAlgo != "wcag" { if contrastAlgo != "dps" && contrastAlgo != "wcag" {
log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo) log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo)
} }
if useVariants {
if primaryDark == "" || primaryLight == "" {
if len(args) == 0 {
log.Fatalf("--variants requires either a positional color argument or both --primary-dark and --primary-light")
}
primaryColor := args[0]
if !strings.HasPrefix(primaryColor, "#") {
primaryColor = "#" + primaryColor
}
primaryDark = primaryColor
primaryLight = primaryColor
}
variantOpts := dank16.VariantOptions{
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
Background: background,
UseDPS: contrastAlgo == "dps",
IsLightMode: isLight,
}
variantColors := dank16.GenerateVariantPalette(variantOpts)
fmt.Print(dank16.GenerateVariantJSON(variantColors))
return
}
if len(args) == 0 {
log.Fatalf("A color argument is required (or use --variants with --primary-dark and --primary-light)")
}
primaryColor := args[0]
if !strings.HasPrefix(primaryColor, "#") {
primaryColor = "#" + primaryColor
}
opts := dank16.PaletteOptions{ opts := dank16.PaletteOptions{
IsLight: isLight, IsLight: isLight,
Background: background, Background: background,
@@ -104,7 +65,18 @@ func runDank16(cmd *cobra.Command, args []string) {
colors := dank16.GeneratePalette(primaryColor, opts) colors := dank16.GeneratePalette(primaryColor, opts)
if isJson { if vscodeEnrich != "" {
data, err := os.ReadFile(vscodeEnrich)
if err != nil {
log.Fatalf("Error reading file: %v", err)
}
enriched, err := dank16.EnrichVSCodeTheme(data, colors)
if err != nil {
log.Fatalf("Error enriching theme: %v", err)
}
fmt.Println(string(enriched))
} else if isJson {
fmt.Print(dank16.GenerateJSON(colors)) fmt.Print(dank16.GenerateJSON(colors))
} else if isKitty { } else if isKitty {
fmt.Print(dank16.GenerateKittyTheme(colors)) fmt.Print(dank16.GenerateKittyTheme(colors))

View File

@@ -1,105 +0,0 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var dpmsCmd = &cobra.Command{
Use: "dpms",
Short: "Control display power management",
}
var dpmsOnCmd = &cobra.Command{
Use: "on [output]",
Short: "Turn display(s) on",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp
},
Run: runDPMSOn,
}
var dpmsOffCmd = &cobra.Command{
Use: "off [output]",
Short: "Turn display(s) off",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp
},
Run: runDPMSOff,
}
var dpmsListCmd = &cobra.Command{
Use: "list",
Short: "List outputs",
Args: cobra.NoArgs,
Run: runDPMSList,
}
func init() {
dpmsCmd.AddCommand(dpmsOnCmd, dpmsOffCmd, dpmsListCmd)
}
func runDPMSOn(cmd *cobra.Command, args []string) {
outputName := ""
if len(args) > 0 {
outputName = args[0]
}
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
if err := client.SetDPMS(outputName, true); err != nil {
log.Fatalf("%v", err)
}
}
func runDPMSOff(cmd *cobra.Command, args []string) {
outputName := ""
if len(args) > 0 {
outputName = args[0]
}
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
if err := client.SetDPMS(outputName, false); err != nil {
log.Fatalf("%v", err)
}
}
func getDPMSOutputs() []string {
client, err := newDPMSClient()
if err != nil {
return nil
}
defer client.Close()
return client.ListOutputs()
}
func runDPMSList(cmd *cobra.Command, args []string) {
client, err := newDPMSClient()
if err != nil {
log.Fatalf("%v", err)
}
defer client.Close()
for _, output := range client.ListOutputs() {
fmt.Println(output)
}
}

View File

@@ -15,7 +15,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version" "github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -78,6 +77,8 @@ func runUpdate() {
switch config.Family { switch config.Family {
case distros.FamilyArch: case distros.FamilyArch:
updateErr = updateArchLinux() updateErr = updateArchLinux()
case distros.FamilyNix:
updateErr = updateNixOS()
case distros.FamilySUSE: case distros.FamilySUSE:
updateErr = updateOtherDistros() updateErr = updateOtherDistros()
default: default:
@@ -122,10 +123,10 @@ func updateArchLinux() error {
var helper string var helper string
var updateCmd *exec.Cmd var updateCmd *exec.Cmd
if utils.CommandExists("yay") { if commandExists("yay") {
helper = "yay" helper = "yay"
updateCmd = exec.Command("yay", "-S", packageName) updateCmd = exec.Command("yay", "-S", packageName)
} else if utils.CommandExists("paru") { } else if commandExists("paru") {
helper = "paru" helper = "paru"
updateCmd = exec.Command("paru", "-S", packageName) updateCmd = exec.Command("paru", "-S", packageName)
} else { } else {
@@ -151,6 +152,27 @@ func updateArchLinux() error {
return nil return nil
} }
func updateNixOS() error {
fmt.Println("This will update DankMaterialShell using nix profile.")
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Println("\nRunning: nix profile upgrade github:AvengeMedia/DankMaterialShell")
updateCmd := exec.Command("nix", "profile", "upgrade", "github:AvengeMedia/DankMaterialShell")
updateCmd.Stdout = os.Stdout
updateCmd.Stderr = os.Stderr
err := updateCmd.Run()
if err != nil {
fmt.Printf("Error: Failed to update using nix profile: %v\n", err)
fmt.Println("Falling back to git-based update method...")
return updateOtherDistros()
}
fmt.Println("dms successfully updated")
return nil
}
func updateOtherDistros() error { func updateOtherDistros() error {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {

View File

@@ -10,10 +10,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "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/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
var greeterCmd = &cobra.Command{ var greeterCmd = &cobra.Command{
@@ -220,191 +217,6 @@ func checkGroupExists(groupName string) bool {
return false return false
} }
func disableDisplayManager(dmName string) (bool, error) {
state, err := getSystemdServiceState(dmName)
if err != nil {
return false, fmt.Errorf("failed to check %s state: %w", dmName, err)
}
if !state.Exists {
return false, nil
}
fmt.Printf("\nChecking %s...\n", dmName)
fmt.Printf(" Current state: enabled=%s\n", state.EnabledState)
actionTaken := false
if state.NeedsDisable {
var disableCmd *exec.Cmd
var actionVerb string
if state.EnabledState == "static" {
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
actionVerb = "masked"
} else {
fmt.Printf(" Disabling %s...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
actionVerb = "disabled"
}
disableCmd.Stdout = os.Stdout
disableCmd.Stderr = os.Stderr
if err := disableCmd.Run(); err != nil {
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
}
enabledState, shouldDisable, verifyErr := checkSystemdServiceEnabled(dmName)
if verifyErr != nil {
fmt.Printf(" ⚠ Warning: Could not verify %s was %s: %v\n", dmName, actionVerb, verifyErr)
} else if shouldDisable {
return actionTaken, fmt.Errorf("%s is still in state '%s' after %s operation", dmName, enabledState, actionVerb)
} else {
fmt.Printf(" ✓ %s %s (now: %s)\n", cases.Title(language.English).String(actionVerb), dmName, enabledState)
}
actionTaken = true
} else {
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Printf(" ✓ %s is already masked\n", dmName)
} else {
fmt.Printf(" ✓ %s is already disabled\n", dmName)
}
}
return actionTaken, nil
}
func ensureGreetdEnabled() error {
fmt.Println("\nChecking greetd service status...")
state, err := getSystemdServiceState("greetd")
if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err)
}
if !state.Exists {
return fmt.Errorf("greetd service not found. Please install greetd first")
}
fmt.Printf(" Current state: %s\n", state.EnabledState)
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Println(" Unmasking greetd...")
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
unmaskCmd.Stdout = os.Stdout
unmaskCmd.Stderr = os.Stderr
if err := unmaskCmd.Run(); err != nil {
return fmt.Errorf("failed to unmask greetd: %w", err)
}
fmt.Println(" ✓ Unmasked greetd")
}
switch state.EnabledState {
case "disabled", "masked", "masked-runtime":
fmt.Println(" Enabling greetd service...")
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
enableCmd.Stdout = os.Stdout
enableCmd.Stderr = os.Stderr
if err := enableCmd.Run(); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
fmt.Println(" ✓ Enabled greetd service")
case "enabled", "enabled-runtime":
fmt.Println(" ✓ greetd is already enabled")
default:
fmt.Printf(" greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
}
return nil
}
func ensureGraphicalTarget() error {
getDefaultCmd := exec.Command("systemctl", "get-default")
currentTarget, err := getDefaultCmd.Output()
if err != nil {
fmt.Println("⚠ Warning: Could not detect current default systemd target")
return nil
}
currentTargetStr := strings.TrimSpace(string(currentTarget))
if currentTargetStr != "graphical.target" {
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
setDefaultCmd.Stdout = os.Stdout
setDefaultCmd.Stderr = os.Stderr
if err := setDefaultCmd.Run(); err != nil {
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
fmt.Println(" Greeter may not start on boot. Run manually:")
fmt.Println(" sudo systemctl set-default graphical.target")
return nil
}
fmt.Println("✓ Set graphical.target as default")
} else {
fmt.Println("✓ Default target already set to graphical.target")
}
return nil
}
func handleConflictingDisplayManagers() error {
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
disabledAny := false
var errors []string
for _, dm := range conflictingDMs {
actionTaken, err := disableDisplayManager(dm)
if err != nil {
errMsg := fmt.Sprintf("Failed to handle %s: %v", dm, err)
errors = append(errors, errMsg)
fmt.Printf(" ⚠⚠⚠ ERROR: %s\n", errMsg)
continue
}
if actionTaken {
disabledAny = true
}
}
if len(errors) > 0 {
fmt.Println("\n╔════════════════════════════════════════════════════════════╗")
fmt.Println("║ ⚠⚠⚠ ERRORS OCCURRED ⚠⚠⚠ ║")
fmt.Println("╚════════════════════════════════════════════════════════════╝")
fmt.Println("\nSome display managers could not be disabled:")
for _, err := range errors {
fmt.Printf(" ✗ %s\n", err)
}
fmt.Println("\nThis may prevent greetd from starting properly.")
fmt.Println("You may need to manually disable them before greetd will work.")
fmt.Println("\nManual commands to try:")
for _, dm := range conflictingDMs {
fmt.Printf(" sudo systemctl disable %s\n", dm)
fmt.Printf(" sudo systemctl mask %s\n", dm)
}
fmt.Print("\nContinue with greeter enablement anyway? (Y/n): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response == "n" || response == "no" {
return fmt.Errorf("aborted due to display manager conflicts")
}
fmt.Println("\nContinuing despite errors...")
}
if !disabledAny && len(errors) == 0 {
fmt.Println("\n✓ No conflicting display managers found")
} else if disabledAny && len(errors) == 0 {
fmt.Println("\n✓ Successfully handled all conflicting display managers")
}
return nil
}
func enableGreeter() error { func enableGreeter() error {
fmt.Println("=== DMS Greeter Enable ===") fmt.Println("=== DMS Greeter Enable ===")
fmt.Println() fmt.Println()
@@ -420,36 +232,15 @@ func enableGreeter() error {
} }
configContent := string(data) configContent := string(data)
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter") if strings.Contains(configContent, "dms-greeter") {
if configAlreadyCorrect {
fmt.Println("✓ Greeter is already configured with dms-greeter") fmt.Println("✓ Greeter is already configured with dms-greeter")
if err := ensureGraphicalTarget(); err != nil {
return err
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Enable Complete ===")
fmt.Println("\nGreeter configuration verified and system state corrected.")
fmt.Println("To start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.")
return nil return nil
} }
fmt.Println("Detecting installed compositors...") fmt.Println("Detecting installed compositors...")
compositors := greeter.DetectCompositors() compositors := greeter.DetectCompositors()
if utils.CommandExists("sway") { if commandExists("sway") {
compositors = append(compositors, "sway") compositors = append(compositors, "sway")
} }
@@ -531,23 +322,11 @@ func enableGreeter() error {
} }
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor) fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
if err := ensureGraphicalTarget(); err != nil {
return err
}
if err := handleConflictingDisplayManagers(); err != nil {
return err
}
if err := ensureGreetdEnabled(); err != nil {
return err
}
fmt.Println("\n=== Enable Complete ===") fmt.Println("\n=== Enable Complete ===")
fmt.Println("\nTo start the greeter now, run:") fmt.Println("\nTo start the greeter, run:")
fmt.Println(" sudo systemctl start greetd") fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.") fmt.Println("\nTo enable on boot, run:")
fmt.Println(" sudo systemctl enable --now greetd")
return nil return nil
} }

View File

@@ -30,44 +30,14 @@ var keybindsShowCmd = &cobra.Command{
Short: "Show keybinds for a provider", Short: "Show keybinds for a provider",
Long: "Display keybinds/cheatsheet for the specified provider", Long: "Display keybinds/cheatsheet for the specified provider",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { Run: runKeybindsShow,
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
registry := keybinds.GetDefaultRegistry()
return registry.List(), cobra.ShellCompDirectiveNoFileComp
},
Run: runKeybindsShow,
}
var keybindsSetCmd = &cobra.Command{
Use: "set <provider> <key> <action>",
Short: "Set a keybind override",
Long: "Create or update a keybind override for the specified provider",
Args: cobra.ExactArgs(3),
Run: runKeybindsSet,
}
var keybindsRemoveCmd = &cobra.Command{
Use: "remove <provider> <key>",
Short: "Remove a keybind override",
Long: "Remove a keybind override from the specified provider",
Args: cobra.ExactArgs(2),
Run: runKeybindsRemove,
} }
func init() { func init() {
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider") keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
keybindsCmd.AddCommand(keybindsListCmd) keybindsCmd.AddCommand(keybindsListCmd)
keybindsCmd.AddCommand(keybindsShowCmd) keybindsCmd.AddCommand(keybindsShowCmd)
keybindsCmd.AddCommand(keybindsSetCmd)
keybindsCmd.AddCommand(keybindsRemoveCmd)
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) { keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
return providers.NewJSONFileProvider(filePath) return providers.NewJSONFileProvider(filePath)
@@ -89,145 +59,78 @@ func initializeProviders() {
log.Warnf("Failed to register MangoWC provider: %v", err) log.Warnf("Failed to register MangoWC provider: %v", err)
} }
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
}
swayProvider := providers.NewSwayProvider("$HOME/.config/sway") swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
if err := registry.Register(swayProvider); err != nil { if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err) log.Warnf("Failed to register Sway provider: %v", err)
} }
niriProvider := providers.NewNiriProvider("")
if err := registry.Register(niriProvider); err != nil {
log.Warnf("Failed to register Niri provider: %v", err)
}
config := keybinds.DefaultDiscoveryConfig() config := keybinds.DefaultDiscoveryConfig()
if err := keybinds.AutoDiscoverProviders(registry, config); err != nil { if err := keybinds.AutoDiscoverProviders(registry, config); err != nil {
log.Warnf("Failed to auto-discover providers: %v", err) log.Warnf("Failed to auto-discover providers: %v", err)
} }
} }
func runKeybindsList(_ *cobra.Command, _ []string) { func runKeybindsList(cmd *cobra.Command, args []string) {
providerList := keybinds.GetDefaultRegistry().List() registry := keybinds.GetDefaultRegistry()
if len(providerList) == 0 { providers := registry.List()
if len(providers) == 0 {
fmt.Fprintln(os.Stdout, "No providers available") fmt.Fprintln(os.Stdout, "No providers available")
return return
} }
fmt.Fprintln(os.Stdout, "Available providers:") fmt.Fprintln(os.Stdout, "Available providers:")
for _, name := range providerList { for _, name := range providers {
fmt.Fprintf(os.Stdout, " - %s\n", name) fmt.Fprintf(os.Stdout, " - %s\n", name)
} }
} }
func makeProviderWithPath(name, path string) keybinds.Provider { func runKeybindsShow(cmd *cobra.Command, args []string) {
switch name { providerName := args[0]
case "hyprland": registry := keybinds.GetDefaultRegistry()
return providers.NewHyprlandProvider(path)
case "mangowc": customPath, _ := cmd.Flags().GetString("path")
return providers.NewMangoWCProvider(path) if customPath != "" {
case "sway": var provider keybinds.Provider
return providers.NewSwayProvider(path) switch providerName {
case "scroll": case "hyprland":
return providers.NewSwayProvider(path) provider = providers.NewHyprlandProvider(customPath)
case "niri": case "mangowc":
return providers.NewNiriProvider(path) provider = providers.NewMangoWCProvider(customPath)
default: case "sway":
return nil provider = providers.NewSwayProvider(customPath)
} default:
} log.Fatalf("Provider %s does not support custom path", providerName)
}
sheet, err := provider.GetCheatSheet()
if err != nil {
log.Fatalf("Error getting cheatsheet: %v", err)
}
output, err := json.MarshalIndent(sheet, "", " ")
if err != nil {
log.Fatalf("Error generating JSON: %v", err)
}
fmt.Fprintln(os.Stdout, string(output))
return
}
provider, err := registry.Get(providerName)
if err != nil {
log.Fatalf("Error: %v", err)
}
func printCheatSheet(provider keybinds.Provider) {
sheet, err := provider.GetCheatSheet() sheet, err := provider.GetCheatSheet()
if err != nil { if err != nil {
log.Fatalf("Error getting cheatsheet: %v", err) log.Fatalf("Error getting cheatsheet: %v", err)
} }
output, err := json.MarshalIndent(sheet, "", " ") output, err := json.MarshalIndent(sheet, "", " ")
if err != nil { if err != nil {
log.Fatalf("Error generating JSON: %v", err) log.Fatalf("Error generating JSON: %v", err)
} }
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsShow(cmd *cobra.Command, args []string) {
providerName := args[0]
customPath, _ := cmd.Flags().GetString("path")
if customPath != "" {
provider := makeProviderWithPath(providerName, customPath)
if provider == nil {
log.Fatalf("Provider %s does not support custom path", providerName)
}
printCheatSheet(provider)
return
}
provider, err := keybinds.GetDefaultRegistry().Get(providerName)
if err != nil {
log.Fatalf("Error: %v", err)
}
printCheatSheet(provider)
}
func getWritableProvider(name string) keybinds.WritableProvider {
provider, err := keybinds.GetDefaultRegistry().Get(name)
if err != nil {
log.Fatalf("Error: %v", err)
}
writable, ok := provider.(keybinds.WritableProvider)
if !ok {
log.Fatalf("Provider %s does not support writing keybinds", name)
}
return writable
}
func runKeybindsSet(cmd *cobra.Command, args []string) {
providerName, key, action := args[0], args[1], args[2]
writable := getWritableProvider(providerName)
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
_ = writable.RemoveBind(replaceKey)
}
options := make(map[string]any)
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
options["allow-when-locked"] = true
}
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
options["cooldown-ms"] = v
}
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false
}
desc, _ := cmd.Flags().GetString("desc")
if err := writable.SetBind(key, action, desc, options); err != nil {
log.Fatalf("Error setting keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"action": action,
"path": writable.GetOverridePath(),
}, "", " ")
fmt.Fprintln(os.Stdout, string(output))
}
func runKeybindsRemove(_ *cobra.Command, args []string) {
providerName, key := args[0], args[1]
writable := getWritableProvider(providerName)
if err := writable.RemoveBind(key); err != nil {
log.Fatalf("Error removing keybind: %v", err)
}
output, _ := json.MarshalIndent(map[string]any{
"success": true,
"key": key,
"removed": true,
}, "", " ")
fmt.Fprintln(os.Stdout, string(output)) fmt.Fprintln(os.Stdout, string(output))
} }

View File

@@ -1,182 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/spf13/cobra"
)
var matugenCmd = &cobra.Command{
Use: "matugen",
Short: "Generate Material Design themes",
Long: "Generate Material Design themes using matugen with dank16 color integration",
}
var matugenGenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate theme synchronously",
Run: runMatugenGenerate,
}
var matugenQueueCmd = &cobra.Command{
Use: "queue",
Short: "Queue theme generation (uses socket if available)",
Run: runMatugenQueue,
}
func init() {
matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files")
cmd.Flags().String("shell-dir", "", "DMS shell installation directory")
cmd.Flags().String("config-dir", "", "User config directory")
cmd.Flags().String("kind", "image", "Source type: image or hex")
cmd.Flags().String("value", "", "Wallpaper path or hex color")
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
}
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
}
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
stateDir, _ := cmd.Flags().GetString("state-dir")
shellDir, _ := cmd.Flags().GetString("shell-dir")
configDir, _ := cmd.Flags().GetString("config-dir")
kind, _ := cmd.Flags().GetString("kind")
value, _ := cmd.Flags().GetString("value")
mode, _ := cmd.Flags().GetString("mode")
iconTheme, _ := cmd.Flags().GetString("icon-theme")
matugenType, _ := cmd.Flags().GetString("matugen-type")
runUserTemplates, _ := cmd.Flags().GetBool("run-user-templates")
stockColors, _ := cmd.Flags().GetString("stock-colors")
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
return matugen.Options{
StateDir: stateDir,
ShellDir: shellDir,
ConfigDir: configDir,
Kind: kind,
Value: value,
Mode: mode,
IconTheme: iconTheme,
MatugenType: matugenType,
RunUserTemplates: runUserTemplates,
StockColors: stockColors,
SyncModeWithPortal: syncModeWithPortal,
TerminalsAlwaysDark: terminalsAlwaysDark,
}
}
func runMatugenGenerate(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd)
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
}
func runMatugenQueue(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd)
wait, _ := cmd.Flags().GetBool("wait")
timeout, _ := cmd.Flags().GetDuration("timeout")
socketPath := os.Getenv("DMS_SOCKET")
if socketPath == "" {
var err error
socketPath, err = server.FindSocket()
if err != nil {
log.Info("No socket available, running synchronously")
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
return
}
}
conn, err := net.Dial("unix", socketPath)
if err != nil {
log.Info("Socket connection failed, running synchronously")
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
return
}
defer conn.Close()
request := map[string]any{
"id": 1,
"method": "matugen.queue",
"params": map[string]any{
"stateDir": opts.StateDir,
"shellDir": opts.ShellDir,
"configDir": opts.ConfigDir,
"kind": opts.Kind,
"value": opts.Value,
"mode": opts.Mode,
"iconTheme": opts.IconTheme,
"matugenType": opts.MatugenType,
"runUserTemplates": opts.RunUserTemplates,
"stockColors": opts.StockColors,
"syncModeWithPortal": opts.SyncModeWithPortal,
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"wait": wait,
},
}
if err := json.NewEncoder(conn).Encode(request); err != nil {
log.Fatalf("Failed to send request: %v", err)
}
if !wait {
fmt.Println("Theme generation queued")
return
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resultCh := make(chan error, 1)
go func() {
var response struct {
ID int `json:"id"`
Result any `json:"result"`
Error string `json:"error"`
}
if err := json.NewDecoder(conn).Decode(&response); err != nil {
resultCh <- fmt.Errorf("failed to read response: %w", err)
return
}
if response.Error != "" {
resultCh <- fmt.Errorf("server error: %s", response.Error)
return
}
resultCh <- nil
}()
select {
case err := <-resultCh:
if err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
fmt.Println("Theme generation completed")
case <-ctx.Done():
log.Fatalf("Timeout waiting for theme generation")
}
}

View File

@@ -1,227 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"mime"
"net"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/spf13/cobra"
)
var (
openMimeType string
openCategories []string
openRequestType string
)
var openCmd = &cobra.Command{
Use: "open [target]",
Short: "Open a file, URL, or resource with an application picker",
Long: `Open a target (URL, file, or other resource) using the DMS application picker.
By default, this opens URLs with the browser picker. You can customize the behavior
with flags to handle different MIME types or application categories.
Examples:
dms open https://example.com # Open URL with browser picker
dms open file.pdf --mime application/pdf # Open PDF with compatible apps
dms open document.odt --category Office # Open with office applications
dms open --mime image/png image.png # Open image with image viewers`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runOpen(args[0])
},
}
func init() {
rootCmd.AddCommand(openCmd)
openCmd.Flags().StringVar(&openMimeType, "mime", "", "MIME type for filtering applications")
openCmd.Flags().StringSliceVar(&openCategories, "category", []string{}, "Application categories to filter (e.g., WebBrowser, Office, Graphics)")
openCmd.Flags().StringVar(&openRequestType, "type", "url", "Request type (url, file, or custom)")
_ = openCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"url", "file", "custom"}, cobra.ShellCompDirectiveNoFileComp
})
}
// mimeTypeToCategories maps MIME types to desktop file categories
func mimeTypeToCategories(mimeType string) []string {
// Split MIME type to get the main type
parts := strings.Split(mimeType, "/")
if len(parts) < 1 {
return nil
}
mainType := parts[0]
switch mainType {
case "image":
return []string{"Graphics", "Viewer"}
case "video":
return []string{"Video", "AudioVideo"}
case "audio":
return []string{"Audio", "AudioVideo"}
case "text":
if strings.Contains(mimeType, "html") {
return []string{"WebBrowser"}
}
return []string{"TextEditor", "Office"}
case "application":
if strings.Contains(mimeType, "pdf") {
return []string{"Office", "Viewer"}
}
if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "spreadsheet") ||
strings.Contains(mimeType, "presentation") || strings.Contains(mimeType, "msword") ||
strings.Contains(mimeType, "ms-excel") || strings.Contains(mimeType, "ms-powerpoint") ||
strings.Contains(mimeType, "opendocument") {
return []string{"Office"}
}
if strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") ||
strings.Contains(mimeType, "gzip") || strings.Contains(mimeType, "compress") {
return []string{"Archiving", "Utility"}
}
return []string{"Office", "Viewer"}
}
return nil
}
func runOpen(target string) {
socketPath, err := server.FindSocket()
if err != nil {
log.Warnf("DMS socket not found: %v", err)
fmt.Println("DMS is not running. Please start DMS first.")
os.Exit(1)
}
conn, err := net.Dial("unix", socketPath)
if err != nil {
log.Warnf("DMS socket connection failed: %v", err)
fmt.Println("DMS is not running. Please start DMS first.")
os.Exit(1)
}
defer conn.Close()
buf := make([]byte, 1)
for {
_, err := conn.Read(buf)
if err != nil {
return
}
if buf[0] == '\n' {
break
}
}
// Parse file:// URIs to extract the actual file path
actualTarget := target
detectedMimeType := openMimeType
detectedCategories := openCategories
detectedRequestType := openRequestType
log.Infof("Processing target: %s", target)
if parsedURL, err := url.Parse(target); err == nil && parsedURL.Scheme == "file" {
// Extract file path from file:// URI and convert to absolute path
actualTarget = parsedURL.Path
if absPath, err := filepath.Abs(actualTarget); err == nil {
actualTarget = absPath
}
if detectedRequestType == "url" || detectedRequestType == "" {
detectedRequestType = "file"
}
log.Infof("Detected file:// URI, extracted absolute path: %s", actualTarget)
// Auto-detect MIME type if not provided
if detectedMimeType == "" {
ext := filepath.Ext(actualTarget)
if ext != "" {
detectedMimeType = mime.TypeByExtension(ext)
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
}
}
// Auto-detect categories based on MIME type if not provided
if len(detectedCategories) == 0 && detectedMimeType != "" {
detectedCategories = mimeTypeToCategories(detectedMimeType)
log.Infof("Detected categories from MIME type: %v", detectedCategories)
}
} else if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
// Handle HTTP(S) URLs
if detectedRequestType == "" {
detectedRequestType = "url"
}
log.Infof("Detected HTTP(S) URL")
} else if _, err := os.Stat(target); err == nil {
// Handle local file paths directly (not file:// URIs)
// Convert to absolute path
if absPath, err := filepath.Abs(target); err == nil {
actualTarget = absPath
}
if detectedRequestType == "url" || detectedRequestType == "" {
detectedRequestType = "file"
}
log.Infof("Detected local file path, converted to absolute: %s", actualTarget)
// Auto-detect MIME type if not provided
if detectedMimeType == "" {
ext := filepath.Ext(actualTarget)
if ext != "" {
detectedMimeType = mime.TypeByExtension(ext)
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
}
}
// Auto-detect categories based on MIME type if not provided
if len(detectedCategories) == 0 && detectedMimeType != "" {
detectedCategories = mimeTypeToCategories(detectedMimeType)
log.Infof("Detected categories from MIME type: %v", detectedCategories)
}
}
params := map[string]any{
"target": actualTarget,
}
if detectedMimeType != "" {
params["mimeType"] = detectedMimeType
}
if len(detectedCategories) > 0 {
params["categories"] = detectedCategories
}
if detectedRequestType != "" {
params["requestType"] = detectedRequestType
}
method := "apppicker.open"
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
method = "browser.open"
params["url"] = target
}
req := models.Request{
ID: 1,
Method: method,
Params: params,
}
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
if err := json.NewEncoder(conn).Encode(req); err != nil {
log.Fatalf("Failed to send request: %v", err)
}
log.Infof("Request sent successfully")
}

View File

@@ -1,12 +1,14 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms" "github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -74,7 +76,14 @@ func findConfig(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func runInteractiveMode(cmd *cobra.Command, args []string) { func runInteractiveMode(cmd *cobra.Command, args []string) {
detector, _ := dms.NewDetector() detector, err := dms.NewDetector()
if err != nil && !errors.Is(err, &distros.UnsupportedDistributionError{}) {
log.Fatalf("Error initializing DMS detector: %v", err)
} else if errors.Is(err, &distros.UnsupportedDistributionError{}) {
log.Error("Interactive mode is not supported on this distribution.")
log.Info("Please run 'dms --help' for available commands.")
os.Exit(1)
}
if !detector.IsDMSInstalled() { if !detector.IsDMSInstalled() {
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.") log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")

View File

@@ -1,414 +0,0 @@
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
"github.com/spf13/cobra"
)
var (
ssOutputName string
ssIncludeCursor bool
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
)
var screenshotCmd = &cobra.Command{
Use: "screenshot",
Short: "Capture screenshots",
Long: `Capture screenshots from Wayland displays.
Modes:
region - Select a region interactively (default)
full - Capture the focused output
all - Capture all outputs combined
output - Capture a specific output by name
window - Capture the focused window (Hyprland/DWL)
last - Capture the last selected region
Output format (--format):
png - PNG format (default)
jpg/jpeg - JPEG format
ppm - PPM format
Examples:
dms screenshot # Region select, save file + clipboard
dms screenshot full # Full screen of focused output
dms screenshot all # All screens combined
dms screenshot output -o DP-1 # Specific output
dms screenshot window # Focused window (Hyprland)
dms screenshot last # Last region (pre-selected)
dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only
dms screenshot --cursor # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
}
var ssRegionCmd = &cobra.Command{
Use: "region",
Short: "Select a region interactively",
Run: runScreenshotRegion,
}
var ssFullCmd = &cobra.Command{
Use: "full",
Short: "Capture the focused output",
Run: runScreenshotFull,
}
var ssAllCmd = &cobra.Command{
Use: "all",
Short: "Capture all outputs combined",
Run: runScreenshotAll,
}
var ssOutputCmd = &cobra.Command{
Use: "output",
Short: "Capture a specific output",
Run: runScreenshotOutput,
}
var ssLastCmd = &cobra.Command{
Use: "last",
Short: "Capture the last selected region",
Long: `Capture the previously selected region without interactive selection.
If no previous region exists, falls back to interactive selection.`,
Run: runScreenshotLast,
}
var ssWindowCmd = &cobra.Command{
Use: "window",
Short: "Capture the focused window",
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
Run: runScreenshotWindow,
}
var ssListCmd = &cobra.Command{
Use: "list",
Short: "List available outputs",
Run: runScreenshotList,
}
var notifyActionCmd = &cobra.Command{
Use: "notify-action",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
screenshot.RunNotifyActionListener(args)
},
}
func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
screenshotCmd.PersistentFlags().StringVar(&ssFilename, "filename", "", "Output filename (auto-generated if empty)")
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
screenshotCmd.AddCommand(ssRegionCmd)
screenshotCmd.AddCommand(ssFullCmd)
screenshotCmd.AddCommand(ssAllCmd)
screenshotCmd.AddCommand(ssOutputCmd)
screenshotCmd.AddCommand(ssLastCmd)
screenshotCmd.AddCommand(ssWindowCmd)
screenshotCmd.AddCommand(ssListCmd)
screenshotCmd.Run = runScreenshotRegion
}
func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig()
config.Mode = mode
config.OutputName = ssOutputName
config.IncludeCursor = ssIncludeCursor
config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify
config.Stdout = ssStdout
if ssOutputDir != "" {
config.OutputDir = ssOutputDir
}
if ssFilename != "" {
config.Filename = ssFilename
}
switch strings.ToLower(ssFormat) {
case "jpg", "jpeg":
config.Format = screenshot.FormatJPEG
case "ppm":
config.Format = screenshot.FormatPPM
default:
config.Format = screenshot.FormatPNG
}
if ssQuality < 1 {
ssQuality = 1
}
if ssQuality > 100 {
ssQuality = 100
}
config.Quality = ssQuality
return config
}
func runScreenshot(config screenshot.Config) {
sc := screenshot.New(config)
result, err := sc.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if result == nil {
os.Exit(0)
}
defer result.Buffer.Close()
if result.YInverted {
result.Buffer.FlipVertical()
}
if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
os.Exit(1)
}
return
}
var filePath string
if config.SaveFile {
outputDir := config.OutputDir
if outputDir == "" {
outputDir = screenshot.GetOutputDir()
}
filename := config.Filename
if filename == "" {
filename = screenshot.GenerateFilename(config.Format)
}
filePath = filepath.Join(outputDir, filename)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
os.Exit(1)
}
fmt.Println(filePath)
}
if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
os.Exit(1)
}
if !config.SaveFile {
fmt.Println("Copied to clipboard")
}
}
if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
screenshot.SendNotification(screenshot.NotifyResult{
FilePath: filePath,
Clipboard: config.Clipboard,
ImageData: thumbData,
Width: thumbW,
Height: thumbH,
})
}
}
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
var mimeType string
var data bytes.Buffer
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
switch format {
case screenshot.FormatJPEG:
mimeType = "image/jpeg"
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
return err
}
default:
mimeType = "image/png"
if err := screenshot.EncodePNG(&data, img); err != nil {
return err
}
}
cmd := exec.Command("wl-copy", "--type", mimeType)
cmd.Stdin = &data
return cmd.Run()
}
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
switch format {
case screenshot.FormatJPEG:
return screenshot.EncodeJPEG(os.Stdout, img, quality)
default:
return screenshot.EncodePNG(os.Stdout, img)
}
}
func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) {
srcW, srcH := buf.Width, buf.Height
scale := 1.0
if srcW > maxSize || srcH > maxSize {
if srcW > srcH {
scale = float64(maxSize) / float64(srcW)
} else {
scale = float64(maxSize) / float64(srcH)
}
}
dstW := int(float64(srcW) * scale)
dstH := int(float64(srcH) * scale)
if dstW < 1 {
dstW = 1
}
if dstH < 1 {
dstH = 1
}
data := buf.Data()
rgb := make([]byte, dstW*dstH*3)
var swapRB bool
switch pixelFormat {
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
swapRB = false
default:
swapRB = true
}
for y := 0; y < dstH; y++ {
srcY := int(float64(y) / scale)
if srcY >= srcH {
srcY = srcH - 1
}
for x := 0; x < dstW; x++ {
srcX := int(float64(x) / scale)
if srcX >= srcW {
srcX = srcW - 1
}
si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3
if si+3 >= len(data) {
continue
}
if swapRB {
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
} else {
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
}
}
}
return rgb, dstW, dstH
}
func runScreenshotRegion(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeRegion)
runScreenshot(config)
}
func runScreenshotFull(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeFullScreen)
runScreenshot(config)
}
func runScreenshotAll(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeAllScreens)
runScreenshot(config)
}
func runScreenshotOutput(cmd *cobra.Command, args []string) {
if ssOutputName == "" && len(args) > 0 {
ssOutputName = args[0]
}
if ssOutputName == "" {
fmt.Fprintln(os.Stderr, "Error: output name required (use -o or provide as argument)")
os.Exit(1)
}
config := getScreenshotConfig(screenshot.ModeOutput)
runScreenshot(config)
}
func runScreenshotLast(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeLastRegion)
runScreenshot(config)
}
func runScreenshotWindow(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeWindow)
runScreenshot(config)
}
func runScreenshotList(cmd *cobra.Command, args []string) {
outputs, err := screenshot.ListOutputs()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
for _, o := range outputs {
scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
if o.FractionalScale == float64(int(o.FractionalScale)) {
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
}
transformStr := transformName(o.Transform)
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
}
}
func transformName(t int32) string {
switch t {
case 0:
return "normal"
case 1:
return "90"
case 2:
return "180"
case 3:
return "270"
case 4:
return "flipped"
case 5:
return "flipped-90"
case 6:
return "flipped-180"
case 7:
return "flipped-270"
default:
return fmt.Sprintf("%d", t)
}
}

View File

@@ -29,7 +29,6 @@ func runSetup() error {
wm, wmSelected := promptCompositor() wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
if !wmSelected && !terminalSelected { if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.") fmt.Println("No configurations selected. Exiting.")
@@ -68,14 +67,14 @@ func runSetup() error {
var err error var err error
if wmSelected && terminalSelected { if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd) results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal)
} else if wmSelected { } else if wmSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd) results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
if len(results) > 1 { if len(results) > 1 {
results = results[:1] results = results[:1]
} }
} else if terminalSelected { } else if terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd) results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal)
if len(results) > 0 && results[0].ConfigType == "Niri" { if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:] results = results[1:]
} }
@@ -145,19 +144,6 @@ func promptTerminal() (deps.Terminal, bool) {
} }
} }
func promptSystemd() bool {
fmt.Println("\nUse systemd for session management?")
fmt.Println("1) Yes (recommended for most distros)")
fmt.Println("2) No (standalone, no systemd integration)")
var response string
fmt.Print("\nChoice (1-2): ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
return response != "2"
}
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool { func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
homeDir := os.Getenv("HOME") homeDir := os.Getenv("HOME")
willBackup := false willBackup := false

View File

@@ -1,345 +0,0 @@
package main
import (
"fmt"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type cmd struct {
fn func()
done chan error
}
type dpmsClient struct {
display *wlclient.Display
ctx *wlclient.Context
powerMgr *wlr_output_power.ZwlrOutputPowerManagerV1
outputs map[string]*outputState
mu sync.Mutex
syncRound int
done bool
err error
cmdq chan cmd
stopChan chan struct{}
wg sync.WaitGroup
}
type outputState struct {
wlOutput *wlclient.Output
powerCtrl *wlr_output_power.ZwlrOutputPowerV1
name string
mode uint32
failed bool
waitCh chan struct{}
wantMode *uint32
}
func (c *dpmsClient) post(fn func()) {
done := make(chan error, 1)
select {
case c.cmdq <- cmd{fn: fn, done: done}:
<-done
case <-c.stopChan:
}
}
func (c *dpmsClient) waylandActor() {
defer c.wg.Done()
for {
select {
case <-c.stopChan:
return
case cmd := <-c.cmdq:
cmd.fn()
close(cmd.done)
}
}
}
func newDPMSClient() (*dpmsClient, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
}
c := &dpmsClient{
display: display,
ctx: display.Context(),
outputs: make(map[string]*outputState),
cmdq: make(chan cmd, 128),
stopChan: make(chan struct{}),
}
c.wg.Add(1)
go c.waylandActor()
registry, err := display.GetRegistry()
if err != nil {
display.Context().Close()
return nil, fmt.Errorf("failed to get registry: %w", err)
}
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
version := e.Version
if version > 1 {
version = 1
}
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
c.powerMgr = powerMgr
}
case "wl_output":
output := wlclient.NewOutput(c.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
outputID := fmt.Sprintf("output-%d", output.ID())
state := &outputState{
wlOutput: output,
name: outputID,
}
c.mu.Lock()
c.outputs[outputID] = state
c.mu.Unlock()
output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
c.mu.Lock()
delete(c.outputs, state.name)
state.name = ev.Name
c.outputs[ev.Name] = state
c.mu.Unlock()
})
}
}
})
syncCallback, err := display.Sync()
if err != nil {
c.Close()
return nil, fmt.Errorf("failed to sync display: %w", err)
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
c.handleSync()
})
for !c.done {
if err := c.ctx.Dispatch(); err != nil {
c.Close()
return nil, fmt.Errorf("dispatch error: %w", err)
}
}
if c.err != nil {
c.Close()
return nil, c.err
}
return c, nil
}
func (c *dpmsClient) handleSync() {
c.syncRound++
switch c.syncRound {
case 1:
if c.powerMgr == nil {
c.err = fmt.Errorf("wlr-output-power-management protocol not supported by compositor")
c.done = true
return
}
c.mu.Lock()
for _, state := range c.outputs {
powerCtrl, err := c.powerMgr.GetOutputPower(state.wlOutput)
if err != nil {
continue
}
state.powerCtrl = powerCtrl
powerCtrl.SetModeHandler(func(e wlr_output_power.ZwlrOutputPowerV1ModeEvent) {
c.mu.Lock()
defer c.mu.Unlock()
if state.powerCtrl == nil {
return
}
state.mode = e.Mode
if state.wantMode != nil && e.Mode == *state.wantMode && state.waitCh != nil {
close(state.waitCh)
state.wantMode = nil
}
})
powerCtrl.SetFailedHandler(func(e wlr_output_power.ZwlrOutputPowerV1FailedEvent) {
c.mu.Lock()
defer c.mu.Unlock()
if state.powerCtrl == nil {
return
}
state.failed = true
if state.waitCh != nil {
close(state.waitCh)
state.wantMode = nil
}
})
}
c.mu.Unlock()
syncCallback, err := c.display.Sync()
if err != nil {
c.err = fmt.Errorf("failed to sync display: %w", err)
c.done = true
return
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
c.handleSync()
})
default:
c.done = true
}
}
func (c *dpmsClient) ListOutputs() []string {
c.mu.Lock()
defer c.mu.Unlock()
names := make([]string, 0, len(c.outputs))
for name := range c.outputs {
names = append(names, name)
}
return names
}
func (c *dpmsClient) SetDPMS(outputName string, on bool) error {
var mode uint32
if on {
mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOn)
} else {
mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOff)
}
var setErr error
c.post(func() {
c.mu.Lock()
var waitStates []*outputState
if outputName == "" || outputName == "all" {
if len(c.outputs) == 0 {
c.mu.Unlock()
setErr = fmt.Errorf("no outputs found")
return
}
for _, state := range c.outputs {
if state.powerCtrl == nil {
continue
}
state.wantMode = &mode
state.waitCh = make(chan struct{})
state.failed = false
waitStates = append(waitStates, state)
state.powerCtrl.SetMode(mode)
}
} else {
state, ok := c.outputs[outputName]
if !ok {
c.mu.Unlock()
setErr = fmt.Errorf("output not found: %s", outputName)
return
}
if state.powerCtrl == nil {
c.mu.Unlock()
setErr = fmt.Errorf("output %s has nil powerCtrl", outputName)
return
}
state.wantMode = &mode
state.waitCh = make(chan struct{})
state.failed = false
waitStates = append(waitStates, state)
state.powerCtrl.SetMode(mode)
}
c.mu.Unlock()
deadline := time.Now().Add(10 * time.Second)
for _, state := range waitStates {
c.mu.Lock()
ch := state.waitCh
c.mu.Unlock()
done := false
for !done {
if err := c.ctx.Dispatch(); err != nil {
setErr = fmt.Errorf("dispatch error: %w", err)
return
}
select {
case <-ch:
c.mu.Lock()
if state.failed {
setErr = fmt.Errorf("compositor reported failed for %s", state.name)
c.mu.Unlock()
return
}
c.mu.Unlock()
done = true
default:
if time.Now().After(deadline) {
setErr = fmt.Errorf("timeout waiting for mode change on %s", state.name)
return
}
time.Sleep(10 * time.Millisecond)
}
}
}
c.mu.Lock()
for _, state := range waitStates {
if state.powerCtrl != nil {
state.powerCtrl.Destroy()
state.powerCtrl = nil
}
}
c.mu.Unlock()
c.display.Roundtrip()
})
return setErr
}
func (c *dpmsClient) Close() {
close(c.stopChan)
c.wg.Wait()
c.mu.Lock()
defer c.mu.Unlock()
for _, state := range c.outputs {
if state.powerCtrl != nil {
state.powerCtrl.Destroy()
}
}
c.outputs = nil
if c.powerMgr != nil {
c.powerMgr.Destroy()
c.powerMgr = nil
}
if c.display != nil {
c.ctx.Close()
c.display = nil
}
}

View File

@@ -23,7 +23,7 @@ func init() {
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins // Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
// Add common commands to root // Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)

View File

@@ -21,7 +21,7 @@ func init() {
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd) greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to plugins // Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
// Add common commands to root // Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)

View File

@@ -16,8 +16,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server" "github.com/AvengeMedia/DankMaterialShell/core/internal/server"
) )
type ipcTargets map[string]map[string][]string
var isSessionManaged bool var isSessionManaged bool
func execDetachedRestart(targetPID int) { func execDetachedRestart(targetPID int) {
@@ -59,18 +57,13 @@ func getRuntimeDir() string {
return os.TempDir() return os.TempDir()
} }
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string { func getPIDFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid())) return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
} }
func writePIDFile(childPID int) error { func writePIDFile(childPID int) error {
pidFile := getPIDFilePath() pidFile := getPIDFilePath()
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644) return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0644)
} }
func removePIDFile() { func removePIDFile() {
@@ -104,6 +97,7 @@ func getAllDMSPIDs() []int {
continue continue
} }
// Check if the child process is still alive
proc, err := os.FindProcess(childPID) proc, err := os.FindProcess(childPID)
if err != nil { if err != nil {
os.Remove(pidFile) os.Remove(pidFile)
@@ -111,15 +105,18 @@ func getAllDMSPIDs() []int {
} }
if err := proc.Signal(syscall.Signal(0)); err != nil { if err := proc.Signal(syscall.Signal(0)); err != nil {
// Process is dead, remove stale PID file
os.Remove(pidFile) os.Remove(pidFile)
continue continue
} }
pids = append(pids, childPID) pids = append(pids, childPID)
// Also get the parent PID from the filename
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-") parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid") parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil { if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
// Check if parent is still alive
if parentProc, err := os.FindProcess(parentPID); err == nil { if parentProc, err := os.FindProcess(parentPID); err == nil {
if err := parentProc.Signal(syscall.Signal(0)); err == nil { if err := parentProc.Signal(syscall.Signal(0)); err == nil {
pids = append(pids, parentPID) pids = append(pids, parentPID)
@@ -142,7 +139,7 @@ func runShellInteractive(session bool) {
socketPath := server.GetSocketPath() socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path") configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil { if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
log.Warnf("Failed to write config state file: %v", err) log.Warnf("Failed to write config state file: %v", err)
} }
defer os.Remove(configStateFile) defer os.Remove(configStateFile)
@@ -155,7 +152,6 @@ func runShellInteractive(session bool) {
errChan <- fmt.Errorf("server panic: %v", r) errChan <- fmt.Errorf("server panic: %v", r)
} }
}() }()
server.CLIVersion = Version
if err := server.Start(false); err != nil { if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err) errChan <- fmt.Errorf("server error: %w", err)
} }
@@ -169,10 +165,6 @@ func runShellInteractive(session bool) {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+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() homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" { if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) { if !strings.HasPrefix(configPath, homeDir) {
@@ -222,6 +214,7 @@ func runShellInteractive(session bool) {
return return
} }
// All other signals: clean shutdown
log.Infof("\nReceived signal %v, shutting down...", sig) log.Infof("\nReceived signal %v, shutting down...", sig)
cancel() cancel()
cmd.Process.Signal(syscall.SIGTERM) cmd.Process.Signal(syscall.SIGTERM)
@@ -278,6 +271,7 @@ func restartShell() {
} }
func killShell() { func killShell() {
// Get all tracked DMS PIDs from PID files
pids := getAllDMSPIDs() pids := getAllDMSPIDs()
if len(pids) == 0 { if len(pids) == 0 {
@@ -288,12 +282,14 @@ func killShell() {
currentPid := os.Getpid() currentPid := os.Getpid()
uniquePids := make(map[int]bool) uniquePids := make(map[int]bool)
// Deduplicate and filter out current process
for _, pid := range pids { for _, pid := range pids {
if pid != currentPid { if pid != currentPid {
uniquePids[pid] = true uniquePids[pid] = true
} }
} }
// Kill all tracked processes
for pid := range uniquePids { for pid := range uniquePids {
proc, err := os.FindProcess(pid) proc, err := os.FindProcess(pid)
if err != nil { if err != nil {
@@ -301,6 +297,7 @@ func killShell() {
continue continue
} }
// Check if process is still alive before killing
if err := proc.Signal(syscall.Signal(0)); err != nil { if err := proc.Signal(syscall.Signal(0)); err != nil {
continue continue
} }
@@ -312,6 +309,7 @@ func killShell() {
} }
} }
// Clean up any remaining PID files
dir := getRuntimeDir() dir := getRuntimeDir()
entries, err := os.ReadDir(dir) entries, err := os.ReadDir(dir)
if err != nil { if err != nil {
@@ -328,6 +326,7 @@ func killShell() {
func runShellDaemon(session bool) { func runShellDaemon(session bool) {
isSessionManaged = session isSessionManaged = session
// Check if this is the daemon child process by looking for the hidden flag
isDaemonChild := false isDaemonChild := false
for _, arg := range os.Args { for _, arg := range os.Args {
if arg == "--daemon-child" { if arg == "--daemon-child" {
@@ -362,7 +361,7 @@ func runShellDaemon(session bool) {
socketPath := server.GetSocketPath() socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path") configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil { if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
log.Warnf("Failed to write config state file: %v", err) log.Warnf("Failed to write config state file: %v", err)
} }
defer os.Remove(configStateFile) defer os.Remove(configStateFile)
@@ -375,7 +374,6 @@ func runShellDaemon(session bool) {
errChan <- fmt.Errorf("server panic: %v", r) errChan <- fmt.Errorf("server panic: %v", r)
} }
}() }()
server.CLIVersion = Version
if err := server.Start(false); err != nil { if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err) errChan <- fmt.Errorf("server error: %w", err)
} }
@@ -389,10 +387,6 @@ func runShellDaemon(session bool) {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+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() homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" { if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) { if !strings.HasPrefix(configPath, homeDir) {
@@ -465,79 +459,6 @@ func runShellDaemon(session bool) {
} }
} }
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
targets := make(ipcTargets)
var currentTarget string
for _, line := range strings.Split(output, "\n") {
if strings.HasPrefix(line, "target ") {
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
targets[currentTarget] = make(map[string][]string)
}
if strings.HasPrefix(line, " function") && currentTarget != "" {
argsList := []string{}
currentFunc := strings.TrimPrefix(line, " function ")
funcDef := strings.SplitN(currentFunc, "(", 2)
argList := strings.SplitN(funcDef[1], ")", 2)[0]
args := strings.Split(argList, ",")
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
argsList = append(argsList, funcDef[0])
for _, arg := range args {
argName := strings.SplitN(strings.TrimSpace(arg), ":", 2)[0]
argsList = append(argsList, argName)
}
targets[currentTarget][funcDef[0]] = argsList
} else {
targets[currentTarget][funcDef[0]] = make([]string, 0)
}
}
}
return targets
}
func getShellIPCCompletions(args []string, _ string) []string {
cmdArgs := []string{"-p", configPath, "ipc", "show"}
cmd := exec.Command("qs", cmdArgs...)
var targets ipcTargets
if output, err := cmd.Output(); err == nil {
targets = parseTargetsFromIPCShowOutput(string(output))
} else {
log.Debugf("Error getting IPC show output for completions: %v", err)
return nil
}
if len(args) > 0 && args[0] == "call" {
args = args[1:]
}
if len(args) == 0 {
targetNames := make([]string, 0)
targetNames = append(targetNames, "call")
for k := range targets {
targetNames = append(targetNames, k)
}
return targetNames
}
if len(args) == 1 {
if targetFuncs, ok := targets[args[0]]; ok {
funcNames := make([]string, 0)
for k := range targetFuncs {
funcNames = append(funcNames, k)
}
return funcNames
}
return nil
}
if len(args) <= len(targets[args[0]]) {
funcArgs := targets[args[0]][args[1]]
if len(funcArgs) >= len(args) {
return []string{fmt.Sprintf("[%s]", funcArgs[len(args)-1])}
}
}
return nil
}
func runShellIPCCommand(args []string) { func runShellIPCCommand(args []string) {
if len(args) == 0 { if len(args) == 0 {
log.Error("IPC command requires arguments") log.Error("IPC command requires arguments")

View File

@@ -3,9 +3,14 @@ package main
import ( import (
"fmt" "fmt"
"os/exec" "os/exec"
"strings"
) )
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// findCommandPath returns the absolute path to a command in PATH
func findCommandPath(cmd string) (string, error) { func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd) path, err := exec.LookPath(cmd)
if err != nil { if err != nil {
@@ -19,68 +24,3 @@ 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 := false
for _, known := range knownStates {
if stateStr == known {
isKnownState = true
break
}
}
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
}

View File

@@ -9,26 +9,24 @@ require (
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/log v0.4.2
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.2.0 github.com/godbus/dbus/v5 v5.1.0
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
github.com/pilebones/go-udev v0.9.1
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
) )
require ( 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.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect github.com/clipperhouse/displaywidth v0.5.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // 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-20251126203821-7f9c95185ee0 // indirect github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect
@@ -36,7 +34,7 @@ require (
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.47.0 // indirect
) )
@@ -45,12 +43,12 @@ require (
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.3.3 // indirect github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.2 // indirect github.com/charmbracelet/x/ansi v0.11.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
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.1 // indirect github.com/davecgh/go-spew v1.1.1 // 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-20251128074608-48f817f57805 github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@@ -65,6 +63,6 @@ require (
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.38.0 golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0 golang.org/x/text v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -24,14 +24,14 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs= github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg= github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
@@ -39,8 +39,8 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
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/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/cyphar/filepath-securejoin v0.6.0/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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=
@@ -56,17 +56,16 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c= github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 h1:EC9n6hr6yKDoVJ6g7Ko523LbbceJfR0ohbOp809Fyf4=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k= github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0/go.mod h1:E3VhlS+AKkrq6ZNn1axE2/nDRJ87l1FJk9r5HT2vPX0=
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w= github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU= github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY= github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9 h1:SOFrnF9LCssC6q6Rb0084Bzg2aBYbe8QXv9xKGXmt/w=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs= github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9/go.mod h1:0wtvm/JfPC9RFVEAP3ks0ec5h64/qmZkTTUE3pjz7Hc=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
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.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -84,9 +83,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
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=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -101,8 +99,6 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -112,8 +108,6 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/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/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -131,10 +125,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
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=

View File

@@ -1,306 +0,0 @@
package colorpicker
import (
"encoding/json"
"fmt"
"math"
"strings"
)
type Color struct {
R, G, B, A uint8
}
type OutputFormat int
const (
FormatHex OutputFormat = iota
FormatRGB
FormatHSL
FormatHSV
FormatCMYK
)
func ParseFormat(s string) OutputFormat {
switch strings.ToLower(s) {
case "rgb":
return FormatRGB
case "hsl":
return FormatHSL
case "hsv":
return FormatHSV
case "cmyk":
return FormatCMYK
default:
return FormatHex
}
}
func (c Color) ToHex(lowercase bool) string {
if lowercase {
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}
func (c Color) ToRGB() string {
return fmt.Sprintf("%d %d %d", c.R, c.G, c.B)
}
func (c Color) ToHSL() string {
h, s, l := rgbToHSL(c.R, c.G, c.B)
return fmt.Sprintf("%d %d%% %d%%", h, s, l)
}
func (c Color) ToHSV() string {
h, s, v := rgbToHSV(c.R, c.G, c.B)
return fmt.Sprintf("%d %d%% %d%%", h, s, v)
}
func (c Color) ToCMYK() string {
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
return fmt.Sprintf("%d%% %d%% %d%% %d%%", cy, m, y, k)
}
func (c Color) Format(format OutputFormat, lowercase bool, customFmt string) string {
if customFmt != "" {
return c.formatCustom(format, customFmt)
}
switch format {
case FormatRGB:
return c.ToRGB()
case FormatHSL:
return c.ToHSL()
case FormatHSV:
return c.ToHSV()
case FormatCMYK:
return c.ToCMYK()
default:
return c.ToHex(lowercase)
}
}
func (c Color) formatCustom(format OutputFormat, customFmt string) string {
switch format {
case FormatRGB:
return replaceArgs(customFmt, c.R, c.G, c.B)
case FormatHSL:
h, s, l := rgbToHSL(c.R, c.G, c.B)
return replaceArgs(customFmt, h, s, l)
case FormatHSV:
h, s, v := rgbToHSV(c.R, c.G, c.B)
return replaceArgs(customFmt, h, s, v)
case FormatCMYK:
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
return replaceArgs4(customFmt, cy, m, y, k)
default:
if strings.Contains(customFmt, "{0}") {
r := fmt.Sprintf("%02X", c.R)
g := fmt.Sprintf("%02X", c.G)
b := fmt.Sprintf("%02X", c.B)
return replaceArgsStr(customFmt, r, g, b)
}
return c.ToHex(false)
}
}
func replaceArgs[T any](format string, a, b, c T) string {
result := format
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
return result
}
func replaceArgs4[T any](format string, a, b, c, d T) string {
result := format
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
result = strings.ReplaceAll(result, "{3}", fmt.Sprintf("%v", d))
return result
}
func replaceArgsStr(format, a, b, c string) string {
result := format
result = strings.ReplaceAll(result, "{0}", a)
result = strings.ReplaceAll(result, "{1}", b)
result = strings.ReplaceAll(result, "{2}", c)
return result
}
func rgbToHSL(r, g, b uint8) (int, int, int) {
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
maxVal := math.Max(rf, math.Max(gf, bf))
minVal := math.Min(rf, math.Min(gf, bf))
l := (maxVal + minVal) / 2
if maxVal == minVal {
return 0, 0, int(math.Round(l * 100))
}
d := maxVal - minVal
var s float64
if l > 0.5 {
s = d / (2 - maxVal - minVal)
} else {
s = d / (maxVal + minVal)
}
var h float64
switch maxVal {
case rf:
h = (gf - bf) / d
if gf < bf {
h += 6
}
case gf:
h = (bf-rf)/d + 2
case bf:
h = (rf-gf)/d + 4
}
h /= 6
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(l * 100))
}
func rgbToHSV(r, g, b uint8) (int, int, int) {
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
maxVal := math.Max(rf, math.Max(gf, bf))
minVal := math.Min(rf, math.Min(gf, bf))
v := maxVal
d := maxVal - minVal
var s float64
if maxVal != 0 {
s = d / maxVal
}
if maxVal == minVal {
return 0, int(math.Round(s * 100)), int(math.Round(v * 100))
}
var h float64
switch maxVal {
case rf:
h = (gf - bf) / d
if gf < bf {
h += 6
}
case gf:
h = (bf-rf)/d + 2
case bf:
h = (rf-gf)/d + 4
}
h /= 6
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(v * 100))
}
func rgbToCMYK(r, g, b uint8) (int, int, int, int) {
if r == 0 && g == 0 && b == 0 {
return 0, 0, 0, 100
}
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
k := 1 - math.Max(rf, math.Max(gf, bf))
c := (1 - rf - k) / (1 - k)
m := (1 - gf - k) / (1 - k)
y := (1 - bf - k) / (1 - k)
return int(math.Round(c * 100)), int(math.Round(m * 100)), int(math.Round(y * 100)), int(math.Round(k * 100))
}
func (c Color) Luminance() float64 {
r := float64(c.R) / 255.0
g := float64(c.G) / 255.0
b := float64(c.B) / 255.0
if r <= 0.03928 {
r = r / 12.92
} else {
r = math.Pow((r+0.055)/1.055, 2.4)
}
if g <= 0.03928 {
g = g / 12.92
} else {
g = math.Pow((g+0.055)/1.055, 2.4)
}
if b <= 0.03928 {
b = b / 12.92
} else {
b = math.Pow((b+0.055)/1.055, 2.4)
}
return 0.2126*r + 0.7152*g + 0.0722*b
}
func (c Color) IsDark() bool {
return c.Luminance() < 0.179
}
type ColorJSON struct {
Hex string `json:"hex"`
RGB struct {
R int `json:"r"`
G int `json:"g"`
B int `json:"b"`
} `json:"rgb"`
HSL struct {
H int `json:"h"`
S int `json:"s"`
L int `json:"l"`
} `json:"hsl"`
HSV struct {
H int `json:"h"`
S int `json:"s"`
V int `json:"v"`
} `json:"hsv"`
CMYK struct {
C int `json:"c"`
M int `json:"m"`
Y int `json:"y"`
K int `json:"k"`
} `json:"cmyk"`
}
func (c Color) ToJSON() (string, error) {
h, s, l := rgbToHSL(c.R, c.G, c.B)
hv, sv, v := rgbToHSV(c.R, c.G, c.B)
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
data := ColorJSON{
Hex: c.ToHex(false),
}
data.RGB.R = int(c.R)
data.RGB.G = int(c.G)
data.RGB.B = int(c.B)
data.HSL.H = h
data.HSL.S = s
data.HSL.L = l
data.HSV.H = hv
data.HSV.S = sv
data.HSV.V = v
data.CMYK.C = cy
data.CMYK.M = m
data.CMYK.Y = y
data.CMYK.K = k
bytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return "", err
}
return string(bytes), nil
}

View File

@@ -1,750 +0,0 @@
package colorpicker
import (
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type Config struct {
Format OutputFormat
CustomFormat string
Lowercase bool
Autocopy bool
Notify bool
}
type Output struct {
wlOutput *client.Output
name string
globalName uint32
x, y int32
width int32
height int32
scale int32
fractionalScale float64
transform int32
}
type LayerSurface struct {
output *Output
state *SurfaceState
wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport
wlPool *client.ShmPool
wlBuffer *client.Buffer
bufferBusy bool
oldPool *client.ShmPool
oldBuffer *client.Buffer
scopyBuffer *client.Buffer
configured bool
hidden bool
}
type Picker struct {
config Config
display *client.Display
registry *client.Registry
ctx *client.Context
compositor *client.Compositor
shm *client.Shm
seat *client.Seat
pointer *client.Pointer
keyboard *client.Keyboard
layerShell *wlr_layer_shell.ZwlrLayerShellV1
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
viewporter *wp_viewporter.WpViewporter
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
outputs map[uint32]*Output
outputsMu sync.Mutex
surfaces []*LayerSurface
activeSurface *LayerSurface
running bool
pickedColor *Color
err error
}
func New(config Config) *Picker {
return &Picker{
config: config,
outputs: make(map[uint32]*Output),
}
}
func (p *Picker) Run() (*Color, error) {
if err := p.connect(); err != nil {
return nil, fmt.Errorf("wayland connect: %w", err)
}
defer p.cleanup()
if err := p.setupRegistry(); err != nil {
return nil, fmt.Errorf("registry setup: %w", err)
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
if p.screencopy == nil {
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
}
if p.layerShell == nil {
return nil, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
}
if p.seat == nil {
return nil, fmt.Errorf("no seat available")
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
// Extra roundtrip to ensure pointer/keyboard from seat capabilities are registered
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip after seat: %w", err)
}
if err := p.createSurfaces(); err != nil {
return nil, fmt.Errorf("create surfaces: %w", err)
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
p.running = true
for p.running {
if err := p.ctx.Dispatch(); err != nil {
p.err = err
break
}
p.checkDone()
}
if p.err != nil {
return nil, p.err
}
return p.pickedColor, nil
}
func (p *Picker) checkDone() {
for _, ls := range p.surfaces {
picked, cancelled := ls.state.IsDone()
switch {
case cancelled:
p.running = false
return
case picked:
color, ok := ls.state.PickColor()
if ok {
p.pickedColor = &color
}
p.running = false
return
}
}
}
func (p *Picker) connect() error {
display, err := client.Connect("")
if err != nil {
return err
}
p.display = display
p.ctx = display.Context()
return nil
}
func (p *Picker) roundtrip() error {
return wlhelpers.Roundtrip(p.display, p.ctx)
}
func (p *Picker) setupRegistry() error {
registry, err := p.display.GetRegistry()
if err != nil {
return err
}
p.registry = registry
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
p.handleGlobal(e)
})
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
p.outputsMu.Lock()
delete(p.outputs, e.Name)
p.outputsMu.Unlock()
})
return nil
}
func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.CompositorInterfaceName:
compositor := client.NewCompositor(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
p.compositor = compositor
}
case client.ShmInterfaceName:
shm := client.NewShm(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
p.shm = shm
}
case client.SeatInterfaceName:
seat := client.NewSeat(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
p.seat = seat
p.setupInput()
}
case client.OutputInterfaceName:
output := client.NewOutput(p.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
p.outputsMu.Lock()
p.outputs[e.Name] = &Output{
wlOutput: output,
globalName: e.Name,
scale: 1,
fractionalScale: 1.0,
}
p.outputsMu.Unlock()
p.setupOutputHandlers(e.Name, output)
}
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
p.layerShell = layerShell
}
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
version := e.Version
if version > 3 {
version = 3
}
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
p.screencopy = screencopy
}
case wp_viewporter.WpViewporterInterfaceName:
viewporter := wp_viewporter.NewWpViewporter(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, viewporter); err == nil {
p.viewporter = viewporter
}
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
p.shortcutsInhibitMgr = mgr
}
}
}
func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.x = e.X
o.y = e.Y
o.transform = int32(e.Transform)
}
p.outputsMu.Unlock()
})
output.SetModeHandler(func(e client.OutputModeEvent) {
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
return
}
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.width = e.Width
o.height = e.Height
}
p.outputsMu.Unlock()
})
output.SetScaleHandler(func(e client.OutputScaleEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.scale = e.Factor
o.fractionalScale = float64(e.Factor)
}
p.outputsMu.Unlock()
})
output.SetNameHandler(func(e client.OutputNameEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.name = e.Name
}
p.outputsMu.Unlock()
})
}
func (p *Picker) createSurfaces() error {
p.outputsMu.Lock()
outputs := make([]*Output, 0, len(p.outputs))
for _, o := range p.outputs {
outputs = append(outputs, o)
}
p.outputsMu.Unlock()
for _, output := range outputs {
ls, err := p.createLayerSurface(output)
if err != nil {
return fmt.Errorf("output %s: %w", output.name, err)
}
p.surfaces = append(p.surfaces, ls)
}
return nil
}
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
surface, err := p.compositor.CreateSurface()
if err != nil {
return nil, fmt.Errorf("create surface: %w", err)
}
layerSurf, err := p.layerShell.GetLayerSurface(
surface,
output.wlOutput,
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
"dms-colorpicker",
)
if err != nil {
return nil, fmt.Errorf("get layer surface: %w", err)
}
ls := &LayerSurface{
output: output,
state: NewSurfaceState(p.config.Format, p.config.Lowercase),
wlSurface: surface,
layerSurf: layerSurf,
hidden: true, // Start hidden, will show overlay when pointer enters
}
if p.viewporter != nil {
vp, err := p.viewporter.GetViewport(surface)
if err == nil {
ls.viewport = vp
}
}
if err := layerSurf.SetAnchor(
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
); err != nil {
log.Warn("failed to set layer anchor", "err", err)
}
if err := layerSurf.SetExclusiveZone(-1); err != nil {
log.Warn("failed to set exclusive zone", "err", err)
}
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
log.Warn("failed to set keyboard interactivity", "err", err)
}
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
if err := layerSurf.AckConfigure(e.Serial); err != nil {
log.Warn("failed to ack configure", "err", err)
}
if err := ls.state.OnLayerConfigure(int(e.Width), int(e.Height)); err != nil {
log.Warn("failed to handle layer configure", "err", err)
}
ls.configured = true
scale := p.computeSurfaceScale(ls)
ls.state.SetScale(scale)
if !ls.state.IsReady() {
p.captureForSurface(ls)
} else {
p.redrawSurface(ls)
}
// Request shortcut inhibition once surface is configured
p.ensureShortcutsInhibitor(ls)
})
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
p.running = false
})
if err := surface.Commit(); err != nil {
log.Warn("failed to commit surface", "err", err)
}
return ls, nil
}
func (p *Picker) computeSurfaceScale(ls *LayerSurface) int32 {
out := ls.output
if out == nil || out.scale <= 0 {
return 1
}
return out.scale
}
func (p *Picker) ensureShortcutsInhibitor(ls *LayerSurface) {
if p.shortcutsInhibitMgr == nil || p.seat == nil || p.shortcutsInhibitor != nil {
return
}
inhibitor, err := p.shortcutsInhibitMgr.InhibitShortcuts(ls.wlSurface, p.seat)
if err != nil {
log.Debug("failed to create shortcuts inhibitor", "err", err)
return
}
p.shortcutsInhibitor = inhibitor
inhibitor.SetActiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1ActiveEvent) {
log.Debug("shortcuts inhibitor active")
})
inhibitor.SetInactiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1InactiveEvent) {
log.Debug("shortcuts inhibitor deactivated by compositor")
})
}
func (p *Picker) captureForSurface(ls *LayerSurface) {
frame, err := p.screencopy.CaptureOutput(0, ls.output.wlOutput)
if err != nil {
return
}
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
if err := ls.state.OnScreencopyBuffer(PixelFormat(e.Format), int(e.Width), int(e.Height), int(e.Stride)); err != nil {
log.Error("failed to create screencopy buffer", "err", err)
}
})
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
screenBuf := ls.state.ScreenBuffer()
if screenBuf == nil {
return
}
pool, err := p.shm.CreatePool(screenBuf.Fd(), int32(screenBuf.Size()))
if err != nil {
return
}
wlBuffer, err := pool.CreateBuffer(0, int32(screenBuf.Width), int32(screenBuf.Height), int32(screenBuf.Stride), uint32(ls.state.screenFormat))
if err != nil {
pool.Destroy()
return
}
if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy()
}
ls.scopyBuffer = wlBuffer
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {})
if err := frame.Copy(wlBuffer); err != nil {
log.Error("failed to copy frame", "err", err)
}
pool.Destroy()
})
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
ls.state.OnScreencopyFlags(e.Flags)
})
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
ls.state.OnScreencopyReady()
screenBuf := ls.state.ScreenBuffer()
if screenBuf != nil && ls.output.transform != TransformNormal {
invTransform := InverseTransform(ls.output.transform)
transformed, err := screenBuf.ApplyTransform(invTransform)
if err != nil {
log.Error("apply transform failed", "err", err)
} else if transformed != screenBuf {
ls.state.ReplaceScreenBuffer(transformed)
}
}
logicalW, _ := ls.state.LogicalSize()
screenBuf = ls.state.ScreenBuffer()
if logicalW > 0 && screenBuf != nil {
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
}
scale := p.computeSurfaceScale(ls)
ls.state.SetScale(scale)
frame.Destroy()
p.redrawSurface(ls)
})
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
frame.Destroy()
})
}
func (p *Picker) redrawSurface(ls *LayerSurface) {
var renderBuf *ShmBuffer
if ls.hidden {
renderBuf = ls.state.RedrawScreenOnly()
} else {
renderBuf = ls.state.Redraw()
}
if renderBuf == nil {
return
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
ls.oldBuffer = nil
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
ls.oldPool = nil
}
ls.oldPool = ls.wlPool
ls.oldBuffer = ls.wlBuffer
ls.wlPool = nil
ls.wlBuffer = nil
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if err != nil {
return
}
ls.wlPool = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffer = wlBuffer
lsRef := ls
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
lsRef.bufferBusy = false
})
ls.bufferBusy = true
logicalW, logicalH := ls.state.LogicalSize()
if logicalW == 0 || logicalH == 0 {
logicalW = int(ls.output.width)
logicalH = int(ls.output.height)
}
if ls.viewport != nil {
_ = ls.wlSurface.SetBufferScale(1)
_ = ls.viewport.SetSource(0, 0, float64(renderBuf.Width), float64(renderBuf.Height))
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
} else {
bufferScale := ls.output.scale
if bufferScale <= 0 {
bufferScale = 1
}
_ = ls.wlSurface.SetBufferScale(bufferScale)
}
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
_ = ls.wlSurface.Commit()
ls.state.SwapBuffers()
}
func (p *Picker) hideSurface(ls *LayerSurface) {
if ls == nil || ls.wlSurface == nil || ls.hidden {
return
}
ls.hidden = true
// Redraw without the crosshair overlay
p.redrawSurface(ls)
}
func (p *Picker) setupInput() {
if p.seat == nil {
return
}
p.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && p.pointer == nil {
pointer, err := p.seat.GetPointer()
if err != nil {
return
}
p.pointer = pointer
p.setupPointerHandlers()
}
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
keyboard, err := p.seat.GetKeyboard()
if err != nil {
return
}
p.keyboard = keyboard
p.setupKeyboardHandlers()
}
})
}
func (p *Picker) setupPointerHandlers() {
p.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
if err := p.pointer.SetCursor(e.Serial, nil, 0, 0); err != nil {
log.Debug("failed to hide cursor", "err", err)
}
if e.Surface == nil {
return
}
p.activeSurface = nil
surfaceID := e.Surface.ID()
for _, ls := range p.surfaces {
if ls.wlSurface.ID() == surfaceID {
p.activeSurface = ls
break
}
}
if p.activeSurface == nil {
return
}
if p.activeSurface.hidden {
p.activeSurface.hidden = false
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
})
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
if e.Surface == nil {
return
}
surfaceID := e.Surface.ID()
for _, ls := range p.surfaces {
if ls.wlSurface.ID() == surfaceID {
p.hideSurface(ls)
break
}
}
})
p.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
if p.activeSurface == nil {
return
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
})
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
if p.activeSurface == nil {
return
}
p.activeSurface.state.OnPointerButton(e.Button, e.State)
})
}
func (p *Picker) setupKeyboardHandlers() {
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
for _, ls := range p.surfaces {
ls.state.OnKey(e.Key, e.State)
}
})
}
func (p *Picker) cleanup() {
for _, ls := range p.surfaces {
if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy()
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
}
if ls.wlBuffer != nil {
ls.wlBuffer.Destroy()
}
if ls.wlPool != nil {
ls.wlPool.Destroy()
}
if ls.viewport != nil {
ls.viewport.Destroy()
}
if ls.layerSurf != nil {
ls.layerSurf.Destroy()
}
if ls.wlSurface != nil {
ls.wlSurface.Destroy()
}
if ls.state != nil {
ls.state.Destroy()
}
}
if p.shortcutsInhibitor != nil {
if err := p.shortcutsInhibitor.Destroy(); err != nil {
log.Debug("failed to destroy shortcuts inhibitor", "err", err)
}
p.shortcutsInhibitor = nil
}
if p.shortcutsInhibitMgr != nil {
if err := p.shortcutsInhibitMgr.Destroy(); err != nil {
log.Debug("failed to destroy shortcuts inhibit manager", "err", err)
}
p.shortcutsInhibitMgr = nil
}
if p.viewporter != nil {
p.viewporter.Destroy()
}
if p.screencopy != nil {
p.screencopy.Destroy()
}
if p.pointer != nil {
p.pointer.Release()
}
if p.keyboard != nil {
p.keyboard.Release()
}
if p.display != nil {
p.ctx.Close()
}
}

View File

@@ -1,55 +0,0 @@
package colorpicker
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
type ShmBuffer = shm.Buffer
const (
TransformNormal = shm.TransformNormal
Transform90 = shm.Transform90
Transform180 = shm.Transform180
Transform270 = shm.Transform270
TransformFlipped = shm.TransformFlipped
TransformFlipped90 = shm.TransformFlipped90
TransformFlipped180 = shm.TransformFlipped180
TransformFlipped270 = shm.TransformFlipped270
)
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride)
}
func InverseTransform(transform int32) int32 {
return shm.InverseTransform(transform)
}
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
}
func GetPixelColorWithFormat(buf *ShmBuffer, x, y int, format PixelFormat) Color {
if x < 0 || x >= buf.Width || y < 0 || y >= buf.Height {
return Color{}
}
data := buf.Data()
offset := y*buf.Stride + x*4
if offset+3 >= len(data) {
return Color{}
}
if format == FormatABGR8888 || format == FormatXBGR8888 {
return Color{
R: data[offset],
G: data[offset+1],
B: data[offset+2],
A: data[offset+3],
}
}
return Color{
B: data[offset],
G: data[offset+1],
R: data[offset+2],
A: data[offset+3],
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -46,20 +46,11 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context,
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil) return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
} }
// DeployConfigurationsWithSystemd deploys configurations with systemd option
func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
}
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil) return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
} }
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
}
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
shouldReplaceConfig := func(configType string) bool { shouldReplaceConfig := func(configType string) bool {
@@ -73,7 +64,7 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
switch wm { switch wm {
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
if shouldReplaceConfig("Niri") { if shouldReplaceConfig("Niri") {
result, err := cd.deployNiriConfig(terminal, useSystemd) result, err := cd.deployNiriConfig(terminal)
results = append(results, result) results = append(results, result)
if err != nil { if err != nil {
return results, fmt.Errorf("failed to deploy Niri config: %w", err) return results, fmt.Errorf("failed to deploy Niri config: %w", err)
@@ -81,7 +72,7 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
} }
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
if shouldReplaceConfig("Hyprland") { if shouldReplaceConfig("Hyprland") {
result, err := cd.deployHyprlandConfig(terminal, useSystemd) result, err := cd.deployHyprlandConfig(terminal)
results = append(results, result) results = append(results, result)
if err != nil { if err != nil {
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err) return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
@@ -119,7 +110,8 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
return results, nil return results, nil
} }
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) { // deployNiriConfig handles Niri configuration deployment with backup and merging
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Niri", ConfigType: "Niri",
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"), Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
@@ -131,12 +123,6 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
return result, result.Error return result, result.Error
} }
dmsDir := filepath.Join(configDir, "dms")
if err := os.MkdirAll(dmsDir, 0755); err != nil {
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
return result, result.Error
}
var existingConfig string var existingConfig string
if _, err := os.Stat(result.Path); err == nil { if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Niri configuration") cd.log("Found existing Niri configuration")
@@ -157,6 +143,14 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
// Detect polkit agent path
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
}
// Determine terminal command based on choice
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -166,15 +160,13 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
case deps.TerminalAlacritty: case deps.TerminalAlacritty:
terminalCommand = "alacritty" terminalCommand = "alacritty"
default: default:
terminalCommand = "ghostty" terminalCommand = "ghostty" // fallback to ghostty
} }
newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand) newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
}
// If there was an existing config, merge the output sections
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig) mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
if err != nil { if err != nil {
@@ -190,38 +182,11 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
return result, result.Error return result, result.Error
} }
if err := cd.deployNiriDmsConfigs(dmsDir, terminalCommand); err != nil {
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
return result, result.Error
}
result.Deployed = true result.Deployed = true
cd.log("Successfully deployed Niri configuration") cd.log("Successfully deployed Niri configuration")
return result, nil return result, nil
} }
func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) error {
configs := []struct {
name string
content string
}{
{"colors.kdl", NiriColorsConfig},
{"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
}
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
}
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
}
return nil
}
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) { func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult
@@ -410,6 +375,41 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
return results, nil return results, nil
} }
// detectPolkitAgent tries to find the polkit authentication agent on the system
// Prioritizes mate-polkit paths since that's what we install
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
// Prioritize mate-polkit paths first
matePaths := []string{
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
}
for _, path := range matePaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
return path, nil
}
}
// Fallback to other polkit agents if mate-polkit is not found
fallbackPaths := []string{
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
"/usr/libexec/polkit-gnome-authentication-agent-1",
}
for _, path := range fallbackPaths {
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
return path, nil
}
}
return "", fmt.Errorf("no polkit agent found in common locations")
}
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config // mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) { func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones) // Regular expression to match output sections (including commented ones)
@@ -453,7 +453,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
} }
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging // deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) { func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Hyprland", ConfigType: "Hyprland",
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"), Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
@@ -485,6 +485,14 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
// Detect polkit agent path
polkitPath, err := cd.detectPolkitAgent()
if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
}
// Determine terminal command based on choice
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -494,15 +502,13 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
case deps.TerminalAlacritty: case deps.TerminalAlacritty:
terminalCommand = "alacritty" terminalCommand = "alacritty"
default: default:
terminalCommand = "ghostty" terminalCommand = "ghostty" // fallback to ghostty
} }
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand) newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
}
// If there was an existing config, merge the monitor sections
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig) mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
if err != nil { if err != nil {
@@ -525,16 +531,24 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config // mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) { func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match monitor lines (including commented ones)
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
// Also matches commented versions: # monitor = ...
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`) monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
// Find all monitor lines in the existing config
existingMonitors := monitorRegex.FindAllString(existingConfig, -1) existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
if len(existingMonitors) == 0 { if len(existingMonitors) == 0 {
// No monitor sections to merge
return newConfig, nil return newConfig, nil
} }
// Remove the example monitor line from the new config
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`) exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "") mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`) monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig) headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
@@ -542,7 +556,8 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return "", fmt.Errorf("could not find MONITOR CONFIG section") return "", fmt.Errorf("could not find MONITOR CONFIG section")
} }
insertPos := headerMatch[1] + 1 // Insert after the header
insertPos := headerMatch[1] + 1 // +1 for the newline
var builder strings.Builder var builder strings.Builder
builder.WriteString(mergedConfig[:insertPos]) builder.WriteString(mergedConfig[:insertPos])
@@ -557,69 +572,3 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return builder.String(), nil return builder.String(), nil
} }
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
lines := strings.Split(config, "\n")
var result []string
startupSectionFound := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
continue
}
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true
result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
continue
}
result = append(result, line)
}
if !startupSectionFound {
for i, line := range result {
if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{
"exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
}
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
break
}
}
}
return strings.Join(result, "\n")
}
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
TERMINAL "%s"
}`, terminalCommand)
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
spawnDms := `spawn-at-startup "dms" "run"`
if !strings.Contains(config, spawnDms) {
config = strings.Replace(config,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
1)
}
return config
}

View File

@@ -3,6 +3,7 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -10,6 +11,23 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDetectPolkitAgent(t *testing.T) {
cd := &ConfigDeployer{}
// This test depends on the system having a polkit agent installed
// We'll just test that the function doesn't crash and returns some path or error
path, err := cd.detectPolkitAgent()
if err != nil {
// If no polkit agent is found, that's okay for testing
assert.Contains(t, err.Error(), "no polkit agent found")
} else {
// If found, it should be a valid path
assert.NotEmpty(t, path)
assert.True(t, strings.Contains(path, "polkit"))
}
}
func TestMergeNiriOutputSections(t *testing.T) { func TestMergeNiriOutputSections(t *testing.T) {
cd := &ConfigDeployer{} cd := &ConfigDeployer{}
@@ -254,6 +272,17 @@ func getGhosttyPath() string {
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config") return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
} }
func TestPolkitPathInjection(t *testing.T) {
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
other content`
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
}
func TestMergeHyprlandMonitorSections(t *testing.T) { func TestMergeHyprlandMonitorSections(t *testing.T) {
cd := &ConfigDeployer{} cd := &ConfigDeployer{}
@@ -395,7 +424,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
cd := NewConfigDeployer(logChan) cd := NewConfigDeployer(logChan)
t.Run("deploy hyprland config to empty directory", func(t *testing.T) { t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true) result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType) assert.Equal(t, "Hyprland", result.ConfigType)
@@ -425,7 +454,7 @@ general {
err = os.WriteFile(hyprPath, []byte(existingContent), 0644) err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
require.NoError(t, err) require.NoError(t, err)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true) result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType) assert.Equal(t, "Hyprland", result.ConfigType)
@@ -450,17 +479,21 @@ general {
func TestNiriConfigStructure(t *testing.T) { func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {") assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {") assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriConfig, "binds {")
assert.Contains(t, NiriBindsConfig, "binds {") assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`) assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
} }
func TestHyprlandConfigStructure(t *testing.T) { func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG") assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
assert.Contains(t, HyprlandConfig, "# ENVIRONMENT VARS")
assert.Contains(t, HyprlandConfig, "# STARTUP APPS") assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG") assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS") assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}") assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, HyprlandConfig, "{{TERMINAL_COMMAND}}")
assert.Contains(t, HyprlandConfig, "exec-once = dms run")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec,")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle") assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$") assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
} }

View File

@@ -5,31 +5,25 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
func LocateDMSConfig() (string, error) { func LocateDMSConfig() (string, error) {
var primaryPaths []string var primaryPaths []string
configHome := utils.XDGConfigHome() configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(homeDir, ".config")
}
}
if configHome != "" { if configHome != "" {
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms")) primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
} }
// System data directories primaryPaths = append(primaryPaths, "/usr/share/quickshell/dms")
dataDirs := os.Getenv("XDG_DATA_DIRS")
if dataDirs == "" {
dataDirs = "/usr/local/share:/usr/share"
}
for _, dir := range strings.Split(dataDirs, ":") {
if dir != "" {
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
}
}
// System config directories (fallback)
configDirs := os.Getenv("XDG_CONFIG_DIRS") configDirs := os.Getenv("XDG_CONFIG_DIRS")
if configDirs == "" { if configDirs == "" {
configDirs = "/etc/xdg" configDirs = "/etc/xdg"

View File

@@ -7,12 +7,21 @@
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1 # monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
monitor = , preferred,auto,auto monitor = , preferred,auto,auto
# ==================
# ENVIRONMENT VARS
# ==================
env = QT_QPA_PLATFORM,wayland
env = ELECTRON_OZONE_PLATFORM_HINT,auto
env = QT_QPA_PLATFORMTHEME,gtk3
env = QT_QPA_PLATFORMTHEME_QT6,gtk3
env = TERMINAL,{{TERMINAL_COMMAND}}
# ================== # ==================
# STARTUP APPS # STARTUP APPS
# ================== # ==================
exec-once = dbus-update-activation-environment --systemd --all
exec-once = systemctl --user start hyprland-session.target
exec-once = bash -c "wl-paste --watch cliphist store &" exec-once = bash -c "wl-paste --watch cliphist store &"
exec-once = dms run
exec-once = {{POLKIT_AGENT_PATH}}
# ================== # ==================
# INPUT CONFIG # INPUT CONFIG
@@ -116,8 +125,6 @@ windowrulev2 = noborder, class:^(kitty)$
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$ windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
windowrulev2 = float, class:^(zoom)$ windowrulev2 = float, class:^(zoom)$
# DMS windows floating by default
windowrulev2 = float, class:^(org.quickshell)$
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0 windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
layerrule = noanim, ^(quickshell)$ layerrule = noanim, ^(quickshell)$
@@ -131,8 +138,8 @@ $mod = SUPER
bind = $mod, T, exec, {{TERMINAL_COMMAND}} bind = $mod, T, exec, {{TERMINAL_COMMAND}}
bind = $mod, space, exec, dms ipc call spotlight toggle bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist focusOrToggle bind = $mod, M, exec, dms ipc call processlist toggle
bind = $mod, comma, exec, dms ipc call settings focusOrToggle bind = $mod, comma, exec, dms ipc call settings toggle
bind = $mod, N, exec, dms ipc call notifications toggle bind = $mod, N, exec, dms ipc call notifications toggle
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
bind = $mod, Y, exec, dms ipc call dankdash wallpaper bind = $mod, Y, exec, dms ipc call dankdash wallpaper
@@ -144,7 +151,7 @@ bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
# === Security === # === Security ===
bind = $mod ALT, L, exec, dms ipc call lock lock bind = $mod ALT, L, exec, dms ipc call lock lock
bind = $mod SHIFT, E, exit bind = $mod SHIFT, E, exit
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle bind = CTRL ALT, Delete, exec, dms ipc call processlist toggle
# === Audio Controls === # === Audio Controls ===
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3 bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
@@ -272,9 +279,12 @@ binde = $mod SHIFT, minus, resizeactive, 0 -10%
binde = $mod SHIFT, equal, resizeactive, 0 10% binde = $mod SHIFT, equal, resizeactive, 0 10%
# === Screenshots === # === Screenshots ===
bind = , Print, exec, dms screenshot bind = , XF86Launch1, exec, grimblast copy area
bind = CTRL, Print, exec, dms screenshot full bind = CTRL, XF86Launch1, exec, grimblast copy screen
bind = ALT, Print, exec, dms screenshot window bind = ALT, XF86Launch1, exec, grimblast copy active
bind = , Print, exec, grimblast copy area
bind = CTRL, Print, exec, grimblast copy screen
bind = ALT, Print, exec, grimblast copy active
# === System Controls === # === System Controls ===
bind = $mod SHIFT, P, dpms, off bind = $mod SHIFT, P, dpms, off

View File

@@ -1,5 +0,0 @@
recent-windows {
highlight {
corner-radius 12
}
}

View File

@@ -1,195 +0,0 @@
binds {
// === System & Overview ===
Mod+D repeat=false { toggle-overview; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
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"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
}

View File

@@ -1,36 +0,0 @@
layout {
background-color "transparent"
focus-ring {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
}
border {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
}
shadow {
color "#00000070"
}
tab-indicator {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
}
insert-hint {
color "#9dcbfb80"
}
}
recent-windows {
highlight {
active-color "#124a73"
urgent-color "#ffb4ab"
}
}

View File

@@ -1,17 +0,0 @@
layout {
gaps 4
border {
width 2
}
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius 12
clip-to-geometry true
tiled-state true
draw-border-with-background false
}

View File

@@ -44,6 +44,7 @@ input {
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout // https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout { layout {
// Set gaps around windows in logical pixels. // Set gaps around windows in logical pixels.
gaps 5
background-color "transparent" background-color "transparent"
// When to center a column when changing focus, options are: // When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left // - "never", default behavior, focusing an off-screen column will keep at the left
@@ -86,6 +87,11 @@ layout {
inactive-color "#d0d0d0" // Light gray inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red urgent-color "#cc4444" // Softer red
} }
focus-ring {
width 2
active-color "#808080" // Medium gray
inactive-color "#505050" // Dark gray
}
shadow { shadow {
softness 30 softness 30
spread 5 spread 5
@@ -110,8 +116,15 @@ overview {
// See the binds section below for more spawn examples. // See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors. // This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &" spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
spawn-at-startup "dms" "run"
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
environment { environment {
XDG_CURRENT_DESKTOP "niri" XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
TERMINAL "{{TERMINAL_COMMAND}}"
} }
hotkey-overlay { hotkey-overlay {
skip-at-startup skip-at-startup
@@ -201,27 +214,205 @@ window-rule {
match app-id="zoom" match app-id="zoom"
open-floating true open-floating true
} }
// Open dms windows as floating by default
window-rule { window-rule {
match app-id=r#"org.quickshell$"# geometry-corner-radius 12
open-floating true clip-to-geometry true
}
binds {
// === System & Overview ===
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "toggle";
}
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "toggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
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"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "toggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
} }
debug { debug {
honor-xdg-activation-with-invalid-serial honor-xdg-activation-with-invalid-serial
} }
// Override to disable super+tab
recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
Alt+grave { next-window filter="app-id"; }
Alt+Shift+grave { previous-window filter="app-id"; }
}
}
// Include dms files
include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"

View File

@@ -4,15 +4,3 @@ import _ "embed"
//go:embed embedded/niri.kdl //go:embed embedded/niri.kdl
var NiriConfig string var NiriConfig string
//go:embed embedded/niri-colors.kdl
var NiriColorsConfig string
//go:embed embedded/niri-layout.kdl
var NiriLayoutConfig string
//go:embed embedded/niri-alttab.kdl
var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl
var NiriBindsConfig string

View File

@@ -1,6 +0,0 @@
package config
import _ "embed"
//go:embed embedded/testpage.pdf
var TestPage string

View File

@@ -15,78 +15,6 @@ type HSV struct {
H, S, V float64 H, S, V float64
} }
type ColorInfo struct {
Hex string `json:"hex"`
HexStripped string `json:"hex_stripped"`
R int `json:"r"`
G int `json:"g"`
B int `json:"b"`
}
type VariantColorValue struct {
Hex string `json:"hex"`
HexStripped string `json:"hex_stripped"`
}
type VariantColorInfo struct {
Dark VariantColorValue `json:"dark"`
Light VariantColorValue `json:"light"`
Default VariantColorValue `json:"default"`
}
type Palette struct {
Color0 ColorInfo `json:"color0"`
Color1 ColorInfo `json:"color1"`
Color2 ColorInfo `json:"color2"`
Color3 ColorInfo `json:"color3"`
Color4 ColorInfo `json:"color4"`
Color5 ColorInfo `json:"color5"`
Color6 ColorInfo `json:"color6"`
Color7 ColorInfo `json:"color7"`
Color8 ColorInfo `json:"color8"`
Color9 ColorInfo `json:"color9"`
Color10 ColorInfo `json:"color10"`
Color11 ColorInfo `json:"color11"`
Color12 ColorInfo `json:"color12"`
Color13 ColorInfo `json:"color13"`
Color14 ColorInfo `json:"color14"`
Color15 ColorInfo `json:"color15"`
}
type VariantPalette struct {
Color0 VariantColorInfo `json:"color0"`
Color1 VariantColorInfo `json:"color1"`
Color2 VariantColorInfo `json:"color2"`
Color3 VariantColorInfo `json:"color3"`
Color4 VariantColorInfo `json:"color4"`
Color5 VariantColorInfo `json:"color5"`
Color6 VariantColorInfo `json:"color6"`
Color7 VariantColorInfo `json:"color7"`
Color8 VariantColorInfo `json:"color8"`
Color9 VariantColorInfo `json:"color9"`
Color10 VariantColorInfo `json:"color10"`
Color11 VariantColorInfo `json:"color11"`
Color12 VariantColorInfo `json:"color12"`
Color13 VariantColorInfo `json:"color13"`
Color14 VariantColorInfo `json:"color14"`
Color15 VariantColorInfo `json:"color15"`
}
func NewColorInfo(hex string) ColorInfo {
rgb := HexToRGB(hex)
stripped := hex
if len(hex) > 0 && hex[0] == '#' {
stripped = hex[1:]
}
return ColorInfo{
Hex: hex,
HexStripped: stripped,
R: int(math.Round(rgb.R * 255)),
G: int(math.Round(rgb.G * 255)),
B: int(math.Round(rgb.B * 255)),
}
}
func HexToRGB(hex string) RGB { func HexToRGB(hex string) RGB {
if hex[0] == '#' { if hex[0] == '#' {
hex = hex[1:] hex = hex[1:]
@@ -113,14 +41,13 @@ func RGBToHSV(rgb RGB) HSV {
delta := max - min delta := max - min
var h float64 var h float64
switch { if delta == 0 {
case delta == 0:
h = 0 h = 0
case max == rgb.R: } else if max == rgb.R {
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0 h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
case max == rgb.G: } else if max == rgb.G {
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0 h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
default: } else {
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0 h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
} }
@@ -383,13 +310,13 @@ func DeriveContainer(primary string, isLight bool) string {
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV})) return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
} }
func GeneratePalette(primaryColor string, opts PaletteOptions) Palette { func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
baseColor := DeriveContainer(primaryColor, opts.IsLight) baseColor := DeriveContainer(primaryColor, opts.IsLight)
rgb := HexToRGB(baseColor) rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb) hsv := RGBToHSV(rgb)
var palette Palette palette := make([]string, 0, 16)
var normalTextTarget, secondaryTarget float64 var normalTextTarget, secondaryTarget float64
if opts.UseDPS { if opts.UseDPS {
@@ -408,7 +335,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
} else { } else {
bgColor = "#1a1a1a" bgColor = "#1a1a1a"
} }
palette.Color0 = NewColorInfo(bgColor) palette = append(palette, bgColor)
hueShift := (hsv.H - 0.6) * 0.12 hueShift := (hsv.H - 0.6) * 0.12
satBoost := 1.15 satBoost := 1.15
@@ -417,39 +344,39 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
var redColor string var redColor string
if opts.IsLight { if opts.IsLight {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55})) redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
} else { } else {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80})) redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
} }
greenH := math.Mod(0.33+hueShift+1.0, 1.0) greenH := math.Mod(0.33+hueShift+1.0, 1.0)
var greenColor string var greenColor string
if opts.IsLight { if opts.IsLight {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45})) greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} else { } else {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84})) greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} }
yellowH := math.Mod(0.15+hueShift+1.0, 1.0) yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
var yellowColor string var yellowColor string
if opts.IsLight { if opts.IsLight {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50})) yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
} else { } else {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86})) yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
} }
var blueColor string var blueColor string
if opts.IsLight { if opts.IsLight {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1})) blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} else { } else {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)})) blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} }
magH := hsv.H - 0.03 magH := hsv.H - 0.03
@@ -461,116 +388,66 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
hh := RGBToHSV(hr) hh := RGBToHSV(hr)
if opts.IsLight { if opts.IsLight {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85})) magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} else { } else {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75})) magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} }
cyanH := hsv.H + 0.08 cyanH := hsv.H + 0.08
if cyanH > 1.0 { if cyanH > 1.0 {
cyanH -= 1.0 cyanH -= 1.0
} }
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts)) palette = append(palette, ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
if opts.IsLight { if opts.IsLight {
palette.Color7 = NewColorInfo("#1a1a1a") palette = append(palette, "#1a1a1a")
palette.Color8 = NewColorInfo("#2e2e2e") palette = append(palette, "#2e2e2e")
} else { } else {
palette.Color7 = NewColorInfo("#abb2bf") palette = append(palette, "#abb2bf")
palette.Color8 = NewColorInfo("#5c6370") palette = append(palette, "#5c6370")
} }
if opts.IsLight { if opts.IsLight {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65})) brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55})) brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60})) brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
hr := HexToRGB(primaryColor) hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr) hh := RGBToHSV(hr)
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)})) brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)})) brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)})) brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} else { } else {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88})) brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88})) brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91})) brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
// Make it way brighter for type names in dark mode
brightBlue := retoneToL(primaryColor, 85.0) brightBlue := retoneToL(primaryColor, 85.0)
palette.Color12 = NewColorInfo(brightBlue) palette = append(palette, brightBlue)
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)})) brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyanH := hsv.H + 0.02 brightCyanH := hsv.H + 0.02
if brightCyanH > 1.0 { if brightCyanH > 1.0 {
brightCyanH -= 1.0 brightCyanH -= 1.0
} }
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)})) brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts)) palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} }
if opts.IsLight { if opts.IsLight {
palette.Color15 = NewColorInfo("#1a1a1a") palette = append(palette, "#1a1a1a")
} else { } else {
palette.Color15 = NewColorInfo("#ffffff") palette = append(palette, "#ffffff")
} }
return palette return palette
} }
type VariantOptions struct {
PrimaryDark string
PrimaryLight string
Background string
UseDPS bool
IsLightMode bool
}
func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
darkVal := VariantColorValue{Hex: dark.Hex, HexStripped: dark.HexStripped}
lightVal := VariantColorValue{Hex: light.Hex, HexStripped: light.HexStripped}
defaultVal := darkVal
if isLightMode {
defaultVal = lightVal
}
return VariantColorInfo{
Dark: darkVal,
Light: lightVal,
Default: defaultVal,
}
}
func GenerateVariantPalette(opts VariantOptions) VariantPalette {
darkOpts := PaletteOptions{IsLight: false, Background: opts.Background, UseDPS: opts.UseDPS}
lightOpts := PaletteOptions{IsLight: true, Background: opts.Background, UseDPS: opts.UseDPS}
dark := GeneratePalette(opts.PrimaryDark, darkOpts)
light := GeneratePalette(opts.PrimaryLight, lightOpts)
return VariantPalette{
Color0: mergeColorInfo(dark.Color0, light.Color0, opts.IsLightMode),
Color1: mergeColorInfo(dark.Color1, light.Color1, opts.IsLightMode),
Color2: mergeColorInfo(dark.Color2, light.Color2, opts.IsLightMode),
Color3: mergeColorInfo(dark.Color3, light.Color3, opts.IsLightMode),
Color4: mergeColorInfo(dark.Color4, light.Color4, opts.IsLightMode),
Color5: mergeColorInfo(dark.Color5, light.Color5, opts.IsLightMode),
Color6: mergeColorInfo(dark.Color6, light.Color6, opts.IsLightMode),
Color7: mergeColorInfo(dark.Color7, light.Color7, opts.IsLightMode),
Color8: mergeColorInfo(dark.Color8, light.Color8, opts.IsLightMode),
Color9: mergeColorInfo(dark.Color9, light.Color9, opts.IsLightMode),
Color10: mergeColorInfo(dark.Color10, light.Color10, opts.IsLightMode),
Color11: mergeColorInfo(dark.Color11, light.Color11, opts.IsLightMode),
Color12: mergeColorInfo(dark.Color12, light.Color12, opts.IsLightMode),
Color13: mergeColorInfo(dark.Color13, light.Color13, opts.IsLightMode),
Color14: mergeColorInfo(dark.Color14, light.Color14, opts.IsLightMode),
Color15: mergeColorInfo(dark.Color15, light.Color15, opts.IsLightMode),
}
}

View File

@@ -1,6 +1,7 @@
package dank16 package dank16
import ( import (
"encoding/json"
"math" "math"
"testing" "testing"
) )
@@ -345,36 +346,106 @@ func TestGeneratePalette(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts) result := GeneratePalette(tt.base, tt.opts)
colors := []ColorInfo{ if len(result) != 16 {
result.Color0, result.Color1, result.Color2, result.Color3, t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
result.Color4, result.Color5, result.Color6, result.Color7,
result.Color8, result.Color9, result.Color10, result.Color11,
result.Color12, result.Color13, result.Color14, result.Color15,
} }
for i, color := range colors { for i, color := range result {
if len(color.Hex) != 7 || color.Hex[0] != '#' { if len(color) != 7 || color[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex) t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
} }
} }
if tt.opts.Background != "" && result.Color0.Hex != tt.opts.Background { if tt.opts.Background != "" && result[0] != tt.opts.Background {
t.Errorf("Background color = %s, expected %s", result.Color0.Hex, tt.opts.Background) t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background)
} else if !tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#1a1a1a" { } else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result.Color0.Hex) t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0])
} else if tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#f8f8f8" { } else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex) t.Errorf("Light mode background = %s, expected #f8f8f8", result[0])
} }
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" { if tt.opts.IsLight && result[15] != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex) t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15])
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" { } else if !tt.opts.IsLight && result[15] != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex) t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15])
} }
}) })
} }
} }
func TestEnrichVSCodeTheme(t *testing.T) {
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
baseTheme := map[string]interface{}{
"name": "Test Theme",
"type": "dark",
"colors": map[string]interface{}{
"editor.background": "#000000",
},
}
themeJSON, err := json.Marshal(baseTheme)
if err != nil {
t.Fatalf("Failed to marshal base theme: %v", err)
}
result, err := EnrichVSCodeTheme(themeJSON, colors)
if err != nil {
t.Fatalf("EnrichVSCodeTheme failed: %v", err)
}
var enriched map[string]interface{}
if err := json.Unmarshal(result, &enriched); err != nil {
t.Fatalf("Failed to unmarshal result: %v", err)
}
colorsMap, ok := enriched["colors"].(map[string]interface{})
if !ok {
t.Fatal("colors is not a map")
}
terminalColors := []string{
"terminal.ansiBlack",
"terminal.ansiRed",
"terminal.ansiGreen",
"terminal.ansiYellow",
"terminal.ansiBlue",
"terminal.ansiMagenta",
"terminal.ansiCyan",
"terminal.ansiWhite",
"terminal.ansiBrightBlack",
"terminal.ansiBrightRed",
"terminal.ansiBrightGreen",
"terminal.ansiBrightYellow",
"terminal.ansiBrightBlue",
"terminal.ansiBrightMagenta",
"terminal.ansiBrightCyan",
"terminal.ansiBrightWhite",
}
for i, key := range terminalColors {
if val, ok := colorsMap[key]; !ok {
t.Errorf("Missing terminal color: %s", key)
} else if val != colors[i] {
t.Errorf("%s = %s, expected %s", key, val, colors[i])
}
}
if colorsMap["editor.background"] != "#000000" {
t.Error("Original theme colors should be preserved")
}
}
func TestEnrichVSCodeThemeInvalidJSON(t *testing.T) {
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
invalidJSON := []byte("{invalid json")
_, err := EnrichVSCodeTheme(invalidJSON, colors)
if err == nil {
t.Error("Expected error for invalid JSON, got nil")
}
}
func TestRoundTripConversion(t *testing.T) { func TestRoundTripConversion(t *testing.T) {
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"} testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
@@ -564,26 +635,23 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts) result := GeneratePalette(tt.base, tt.opts)
colors := []ColorInfo{ if len(result) != 16 {
result.Color0, result.Color1, result.Color2, result.Color3, t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
result.Color4, result.Color5, result.Color6, result.Color7,
result.Color8, result.Color9, result.Color10, result.Color11,
result.Color12, result.Color13, result.Color14, result.Color15,
} }
for i, color := range colors { for i, color := range result {
if len(color.Hex) != 7 || color.Hex[0] != '#' { if len(color) != 7 || color[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex) t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
} }
} }
bgColor := result.Color0.Hex bgColor := result[0]
for i := 1; i < 8; i++ { for i := 1; i < 8; i++ {
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight) lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight)
minLc := 30.0 minLc := 30.0
if lc < minLc && lc > 0 { if lc < minLc && lc > 0 {
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)", t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
i, colors[i].Hex, lc, bgColor, minLc) i, result[i], lc, bgColor, minLc)
} }
} }
}) })
@@ -640,26 +708,17 @@ func TestContrastAlgorithmComparison(t *testing.T) {
paletteWCAG := GeneratePalette(base, optsWCAG) paletteWCAG := GeneratePalette(base, optsWCAG)
paletteDPS := GeneratePalette(base, optsDPS) paletteDPS := GeneratePalette(base, optsDPS)
wcagColors := []ColorInfo{ if len(paletteWCAG) != 16 || len(paletteDPS) != 16 {
paletteWCAG.Color0, paletteWCAG.Color1, paletteWCAG.Color2, paletteWCAG.Color3, t.Fatal("Both palettes should have 16 colors")
paletteWCAG.Color4, paletteWCAG.Color5, paletteWCAG.Color6, paletteWCAG.Color7,
paletteWCAG.Color8, paletteWCAG.Color9, paletteWCAG.Color10, paletteWCAG.Color11,
paletteWCAG.Color12, paletteWCAG.Color13, paletteWCAG.Color14, paletteWCAG.Color15,
}
dpsColors := []ColorInfo{
paletteDPS.Color0, paletteDPS.Color1, paletteDPS.Color2, paletteDPS.Color3,
paletteDPS.Color4, paletteDPS.Color5, paletteDPS.Color6, paletteDPS.Color7,
paletteDPS.Color8, paletteDPS.Color9, paletteDPS.Color10, paletteDPS.Color11,
paletteDPS.Color12, paletteDPS.Color13, paletteDPS.Color14, paletteDPS.Color15,
} }
if paletteWCAG.Color0.Hex != paletteDPS.Color0.Hex { if paletteWCAG[0] != paletteDPS[0] {
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG.Color0.Hex, paletteDPS.Color0.Hex) t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0])
} }
differentCount := 0 differentCount := 0
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
if wcagColors[i].Hex != dpsColors[i].Hex { if paletteWCAG[i] != paletteDPS[i] {
differentCount++ differentCount++
} }
} }

View File

@@ -6,109 +6,135 @@ import (
"strings" "strings"
) )
func GenerateJSON(p Palette) string { func GenerateJSON(colors []string) string {
marshalled, _ := json.Marshal(p) colorMap := make(map[string]string)
for i, color := range colors {
colorMap[fmt.Sprintf("color%d", i)] = color
}
marshalled, _ := json.Marshal(colorMap)
return string(marshalled) return string(marshalled)
} }
func GenerateVariantJSON(p VariantPalette) string { func GenerateKittyTheme(colors []string) string {
marshalled, _ := json.Marshal(p) kittyColors := []struct {
return string(marshalled) name string
} index int
}{
{"color0", 0},
{"color1", 1},
{"color2", 2},
{"color3", 3},
{"color4", 4},
{"color5", 5},
{"color6", 6},
{"color7", 7},
{"color8", 8},
{"color9", 9},
{"color10", 10},
{"color11", 11},
{"color12", 12},
{"color13", 13},
{"color14", 14},
{"color15", 15},
}
func GenerateKittyTheme(p Palette) string {
var result strings.Builder var result strings.Builder
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex) for _, kc := range kittyColors {
fmt.Fprintf(&result, "color1 %s\n", p.Color1.Hex) fmt.Fprintf(&result, "%s %s\n", kc.name, colors[kc.index])
fmt.Fprintf(&result, "color2 %s\n", p.Color2.Hex) }
fmt.Fprintf(&result, "color3 %s\n", p.Color3.Hex)
fmt.Fprintf(&result, "color4 %s\n", p.Color4.Hex)
fmt.Fprintf(&result, "color5 %s\n", p.Color5.Hex)
fmt.Fprintf(&result, "color6 %s\n", p.Color6.Hex)
fmt.Fprintf(&result, "color7 %s\n", p.Color7.Hex)
fmt.Fprintf(&result, "color8 %s\n", p.Color8.Hex)
fmt.Fprintf(&result, "color9 %s\n", p.Color9.Hex)
fmt.Fprintf(&result, "color10 %s\n", p.Color10.Hex)
fmt.Fprintf(&result, "color11 %s\n", p.Color11.Hex)
fmt.Fprintf(&result, "color12 %s\n", p.Color12.Hex)
fmt.Fprintf(&result, "color13 %s\n", p.Color13.Hex)
fmt.Fprintf(&result, "color14 %s\n", p.Color14.Hex)
fmt.Fprintf(&result, "color15 %s\n", p.Color15.Hex)
return result.String() return result.String()
} }
func GenerateFootTheme(p Palette) string { func GenerateFootTheme(colors []string) string {
footColors := []struct {
name string
index int
}{
{"regular0", 0},
{"regular1", 1},
{"regular2", 2},
{"regular3", 3},
{"regular4", 4},
{"regular5", 5},
{"regular6", 6},
{"regular7", 7},
{"bright0", 8},
{"bright1", 9},
{"bright2", 10},
{"bright3", 11},
{"bright4", 12},
{"bright5", 13},
{"bright6", 14},
{"bright7", 15},
}
var result strings.Builder var result strings.Builder
fmt.Fprintf(&result, "regular0=%s\n", p.Color0.HexStripped) for _, fc := range footColors {
fmt.Fprintf(&result, "regular1=%s\n", p.Color1.HexStripped) fmt.Fprintf(&result, "%s=%s\n", fc.name, strings.TrimPrefix(colors[fc.index], "#"))
fmt.Fprintf(&result, "regular2=%s\n", p.Color2.HexStripped) }
fmt.Fprintf(&result, "regular3=%s\n", p.Color3.HexStripped)
fmt.Fprintf(&result, "regular4=%s\n", p.Color4.HexStripped)
fmt.Fprintf(&result, "regular5=%s\n", p.Color5.HexStripped)
fmt.Fprintf(&result, "regular6=%s\n", p.Color6.HexStripped)
fmt.Fprintf(&result, "regular7=%s\n", p.Color7.HexStripped)
fmt.Fprintf(&result, "bright0=%s\n", p.Color8.HexStripped)
fmt.Fprintf(&result, "bright1=%s\n", p.Color9.HexStripped)
fmt.Fprintf(&result, "bright2=%s\n", p.Color10.HexStripped)
fmt.Fprintf(&result, "bright3=%s\n", p.Color11.HexStripped)
fmt.Fprintf(&result, "bright4=%s\n", p.Color12.HexStripped)
fmt.Fprintf(&result, "bright5=%s\n", p.Color13.HexStripped)
fmt.Fprintf(&result, "bright6=%s\n", p.Color14.HexStripped)
fmt.Fprintf(&result, "bright7=%s\n", p.Color15.HexStripped)
return result.String() return result.String()
} }
func GenerateAlacrittyTheme(p Palette) string { func GenerateAlacrittyTheme(colors []string) string {
alacrittyColors := []struct {
section string
name string
index int
}{
{"normal", "black", 0},
{"normal", "red", 1},
{"normal", "green", 2},
{"normal", "yellow", 3},
{"normal", "blue", 4},
{"normal", "magenta", 5},
{"normal", "cyan", 6},
{"normal", "white", 7},
{"bright", "black", 8},
{"bright", "red", 9},
{"bright", "green", 10},
{"bright", "yellow", 11},
{"bright", "blue", 12},
{"bright", "magenta", 13},
{"bright", "cyan", 14},
{"bright", "white", 15},
}
var result strings.Builder var result strings.Builder
result.WriteString("[colors.normal]\n") currentSection := ""
fmt.Fprintf(&result, "black = '%s'\n", p.Color0.Hex) for _, ac := range alacrittyColors {
fmt.Fprintf(&result, "red = '%s'\n", p.Color1.Hex) if ac.section != currentSection {
fmt.Fprintf(&result, "green = '%s'\n", p.Color2.Hex) if currentSection != "" {
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color3.Hex) result.WriteString("\n")
fmt.Fprintf(&result, "blue = '%s'\n", p.Color4.Hex) }
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color5.Hex) fmt.Fprintf(&result, "[colors.%s]\n", ac.section)
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color6.Hex) currentSection = ac.section
fmt.Fprintf(&result, "white = '%s'\n", p.Color7.Hex) }
result.WriteString("\n[colors.bright]\n") fmt.Fprintf(&result, "%-7s = '%s'\n", ac.name, colors[ac.index])
fmt.Fprintf(&result, "black = '%s'\n", p.Color8.Hex) }
fmt.Fprintf(&result, "red = '%s'\n", p.Color9.Hex)
fmt.Fprintf(&result, "green = '%s'\n", p.Color10.Hex)
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color11.Hex)
fmt.Fprintf(&result, "blue = '%s'\n", p.Color12.Hex)
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color13.Hex)
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color14.Hex)
fmt.Fprintf(&result, "white = '%s'\n", p.Color15.Hex)
return result.String() return result.String()
} }
func GenerateGhosttyTheme(p Palette) string { func GenerateGhosttyTheme(colors []string) string {
var result strings.Builder var result strings.Builder
fmt.Fprintf(&result, "palette = 0=%s\n", p.Color0.Hex) for i, color := range colors {
fmt.Fprintf(&result, "palette = 1=%s\n", p.Color1.Hex) fmt.Fprintf(&result, "palette = %d=%s\n", i, color)
fmt.Fprintf(&result, "palette = 2=%s\n", p.Color2.Hex) }
fmt.Fprintf(&result, "palette = 3=%s\n", p.Color3.Hex)
fmt.Fprintf(&result, "palette = 4=%s\n", p.Color4.Hex)
fmt.Fprintf(&result, "palette = 5=%s\n", p.Color5.Hex)
fmt.Fprintf(&result, "palette = 6=%s\n", p.Color6.Hex)
fmt.Fprintf(&result, "palette = 7=%s\n", p.Color7.Hex)
fmt.Fprintf(&result, "palette = 8=%s\n", p.Color8.Hex)
fmt.Fprintf(&result, "palette = 9=%s\n", p.Color9.Hex)
fmt.Fprintf(&result, "palette = 10=%s\n", p.Color10.Hex)
fmt.Fprintf(&result, "palette = 11=%s\n", p.Color11.Hex)
fmt.Fprintf(&result, "palette = 12=%s\n", p.Color12.Hex)
fmt.Fprintf(&result, "palette = 13=%s\n", p.Color13.Hex)
fmt.Fprintf(&result, "palette = 14=%s\n", p.Color14.Hex)
fmt.Fprintf(&result, "palette = 15=%s\n", p.Color15.Hex)
return result.String() return result.String()
} }
func GenerateWeztermTheme(p Palette) string { func GenerateWeztermTheme(colors []string) string {
var result strings.Builder var result strings.Builder
fmt.Fprintf(&result, "ansi = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n", labels := []string{"ansi", "brights"}
p.Color0.Hex, p.Color1.Hex, p.Color2.Hex, p.Color3.Hex, for j, label := range labels {
p.Color4.Hex, p.Color5.Hex, p.Color6.Hex, p.Color7.Hex) start := j * 8
fmt.Fprintf(&result, "brights = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n", colorSlice := make([]string, 8)
p.Color8.Hex, p.Color9.Hex, p.Color10.Hex, p.Color11.Hex, for i, color := range colors[start : start+8] {
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex) colorSlice[i] = fmt.Sprintf("'%s'", color)
}
fmt.Fprintf(&result, "%s = [%s]\n", label, strings.Join(colorSlice, ", "))
}
return result.String() return result.String()
} }

View File

@@ -0,0 +1,250 @@
package dank16
import (
"encoding/json"
"fmt"
)
type VSCodeTheme struct {
Schema string `json:"$schema"`
Name string `json:"name"`
Type string `json:"type"`
Colors map[string]string `json:"colors"`
TokenColors []VSCodeTokenColor `json:"tokenColors"`
SemanticHighlighting bool `json:"semanticHighlighting"`
SemanticTokenColors map[string]VSCodeTokenSetting `json:"semanticTokenColors"`
}
type VSCodeTokenColor struct {
Scope interface{} `json:"scope"`
Settings VSCodeTokenSetting `json:"settings"`
}
type VSCodeTokenSetting struct {
Foreground string `json:"foreground,omitempty"`
FontStyle string `json:"fontStyle,omitempty"`
}
func updateTokenColor(tc interface{}, scopeToColor map[string]string) {
tcMap, ok := tc.(map[string]interface{})
if !ok {
return
}
scopes, ok := tcMap["scope"].([]interface{})
if !ok {
return
}
settings, ok := tcMap["settings"].(map[string]interface{})
if !ok {
return
}
isYaml := hasScopeContaining(scopes, "yaml")
for _, scope := range scopes {
scopeStr, ok := scope.(string)
if !ok {
continue
}
if scopeStr == "string" && isYaml {
continue
}
if applyColorToScope(settings, scope, scopeToColor) {
break
}
}
}
func applyColorToScope(settings map[string]interface{}, scope interface{}, scopeToColor map[string]string) bool {
scopeStr, ok := scope.(string)
if !ok {
return false
}
newColor, exists := scopeToColor[scopeStr]
if !exists {
return false
}
settings["foreground"] = newColor
return true
}
func hasScopeContaining(scopes []interface{}, substring string) bool {
for _, scope := range scopes {
scopeStr, ok := scope.(string)
if !ok {
continue
}
for i := 0; i <= len(scopeStr)-len(substring); i++ {
if scopeStr[i:i+len(substring)] == substring {
return true
}
}
}
return false
}
func EnrichVSCodeTheme(themeData []byte, colors []string) ([]byte, error) {
var theme map[string]interface{}
if err := json.Unmarshal(themeData, &theme); err != nil {
return nil, err
}
colorsMap, ok := theme["colors"].(map[string]interface{})
if !ok {
colorsMap = make(map[string]interface{})
theme["colors"] = colorsMap
}
bg := colors[0]
isLight := false
if len(bg) == 7 && bg[0] == '#' {
r, g, b := 0, 0, 0
fmt.Sscanf(bg[1:], "%02x%02x%02x", &r, &g, &b)
luminance := (0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 255.0
isLight = luminance > 0.5
}
if isLight {
theme["type"] = "light"
} else {
theme["type"] = "dark"
}
colorsMap["terminal.ansiBlack"] = colors[0]
colorsMap["terminal.ansiRed"] = colors[1]
colorsMap["terminal.ansiGreen"] = colors[2]
colorsMap["terminal.ansiYellow"] = colors[3]
colorsMap["terminal.ansiBlue"] = colors[4]
colorsMap["terminal.ansiMagenta"] = colors[5]
colorsMap["terminal.ansiCyan"] = colors[6]
colorsMap["terminal.ansiWhite"] = colors[7]
colorsMap["terminal.ansiBrightBlack"] = colors[8]
colorsMap["terminal.ansiBrightRed"] = colors[9]
colorsMap["terminal.ansiBrightGreen"] = colors[10]
colorsMap["terminal.ansiBrightYellow"] = colors[11]
colorsMap["terminal.ansiBrightBlue"] = colors[12]
colorsMap["terminal.ansiBrightMagenta"] = colors[13]
colorsMap["terminal.ansiBrightCyan"] = colors[14]
colorsMap["terminal.ansiBrightWhite"] = colors[15]
tokenColors, ok := theme["tokenColors"].([]interface{})
if ok {
scopeToColor := map[string]string{
"comment": colors[8],
"punctuation.definition.comment": colors[8],
"keyword": colors[5],
"storage.type": colors[13],
"storage.modifier": colors[5],
"variable": colors[15],
"variable.parameter": colors[7],
"meta.object-literal.key": colors[4],
"meta.property.object": colors[4],
"variable.other.property": colors[4],
"constant.other.symbol": colors[12],
"constant.numeric": colors[12],
"constant.language": colors[12],
"constant.character": colors[3],
"entity.name.type": colors[12],
"support.type": colors[13],
"entity.name.class": colors[12],
"entity.name.function": colors[2],
"support.function": colors[2],
"support.class": colors[15],
"support.variable": colors[15],
"variable.language": colors[12],
"entity.name.tag.yaml": colors[12],
"string.unquoted.plain.out.yaml": colors[15],
"string.unquoted.yaml": colors[15],
"string": colors[3],
}
for i, tc := range tokenColors {
updateTokenColor(tc, scopeToColor)
tokenColors[i] = tc
}
yamlRules := []VSCodeTokenColor{
{
Scope: "entity.name.tag.yaml",
Settings: VSCodeTokenSetting{Foreground: colors[12]},
},
{
Scope: []string{"string.unquoted.plain.out.yaml", "string.unquoted.yaml"},
Settings: VSCodeTokenSetting{Foreground: colors[15]},
},
}
for _, rule := range yamlRules {
tokenColors = append(tokenColors, rule)
}
theme["tokenColors"] = tokenColors
}
if semanticTokenColors, ok := theme["semanticTokenColors"].(map[string]interface{}); ok {
updates := map[string]string{
"variable": colors[15],
"variable.readonly": colors[12],
"property": colors[4],
"function": colors[2],
"method": colors[2],
"type": colors[12],
"class": colors[12],
"typeParameter": colors[13],
"enumMember": colors[12],
"string": colors[3],
"number": colors[12],
"comment": colors[8],
"keyword": colors[5],
"operator": colors[15],
"parameter": colors[7],
"namespace": colors[15],
}
for key, color := range updates {
if existing, ok := semanticTokenColors[key].(map[string]interface{}); ok {
existing["foreground"] = color
} else {
semanticTokenColors[key] = map[string]interface{}{
"foreground": color,
}
}
}
} else {
semanticTokenColors := make(map[string]interface{})
updates := map[string]string{
"variable": colors[7],
"variable.readonly": colors[12],
"property": colors[4],
"function": colors[2],
"method": colors[2],
"type": colors[12],
"class": colors[12],
"typeParameter": colors[13],
"enumMember": colors[12],
"string": colors[3],
"number": colors[12],
"comment": colors[8],
"keyword": colors[5],
"operator": colors[15],
"parameter": colors[7],
"namespace": colors[15],
}
for key, color := range updates {
semanticTokenColors[key] = map[string]interface{}{
"foreground": color,
}
}
theme["semanticTokenColors"] = semanticTokenColors
}
return json.MarshalIndent(theme, "", " ")
}

View File

@@ -37,9 +37,6 @@ func init() {
Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution { Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan) return NewArchDistribution(config, logChan)
}) })
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
} }
type ArchDistribution struct { type ArchDistribution struct {
@@ -91,6 +88,7 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectWindowManager(wm)) dependencies = append(dependencies, a.detectWindowManager(wm))
dependencies = append(dependencies, a.detectQuickshell()) dependencies = append(dependencies, a.detectQuickshell())
dependencies = append(dependencies, a.detectXDGPortal()) dependencies = append(dependencies, a.detectXDGPortal())
dependencies = append(dependencies, a.detectPolkitAgent())
dependencies = append(dependencies, a.detectAccountsService()) dependencies = append(dependencies, a.detectAccountsService())
// Hyprland-specific tools // Hyprland-specific tools
@@ -106,17 +104,52 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
// Base detections (common across distros) // Base detections (common across distros)
dependencies = append(dependencies, a.detectMatugen()) dependencies = append(dependencies, a.detectMatugen())
dependencies = append(dependencies, a.detectDgop()) dependencies = append(dependencies, a.detectDgop())
dependencies = append(dependencies, a.detectHyprpicker())
dependencies = append(dependencies, a.detectClipboardTools()...) dependencies = append(dependencies, a.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
} }
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")) status := deps.StatusMissing
if a.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (a *ArchDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if a.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
} }
func (a *ArchDistribution) detectAccountsService() deps.Dependency { func (a *ArchDistribution) detectAccountsService() deps.Dependency {
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice")) status := deps.StatusMissing
if a.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (a *ArchDistribution) packageInstalled(pkg string) bool { func (a *ArchDistribution) packageInstalled(pkg string) bool {
@@ -142,13 +175,18 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem}, "cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
} }
switch wm { switch wm {
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"]) packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"]) packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
packages["niri"] = a.getNiriMapping(variants["niri"]) packages["niri"] = a.getNiriMapping(variants["niri"])
@@ -162,11 +200,13 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
if forceQuickshellGit || variant == deps.VariantGit { if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR} return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
} }
// ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
} }
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
} }
@@ -335,19 +375,6 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
terminal := a.DetectTerminalFromDeps(dependencies)
if err := a.WriteEnvironmentConfig(terminal); err != nil {
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := a.WriteWindowManagerConfig(wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := a.EnableDMSService(ctx, wm); err != nil {
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
// Phase 7: Complete // Phase 7: Complete
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,

View File

@@ -17,10 +17,8 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/version" "github.com/AvengeMedia/DankMaterialShell/core/internal/version"
) )
const ( const forceQuickshellGit = false
forceQuickshellGit = false const forceDMSGit = false
forceDMSGit = false
)
// BaseDistribution provides common functionality for all distributions // BaseDistribution provides common functionality for all distributions
type BaseDistribution struct { type BaseDistribution struct {
@@ -76,42 +74,47 @@ func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *
return exec.CommandContext(ctx, "bash", "-c", cmdStr) return exec.CommandContext(ctx, "bash", "-c", cmdStr)
} }
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency { // Common dependency detection methods
status := deps.StatusMissing
if b.commandExists(name) {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: true,
}
}
func (b *BaseDistribution) detectPackage(name, description string, installed bool) deps.Dependency {
status := deps.StatusMissing
if installed {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: true,
}
}
func (b *BaseDistribution) detectGit() deps.Dependency { func (b *BaseDistribution) detectGit() deps.Dependency {
return b.detectCommand("git", "Version control system") status := deps.StatusMissing
if b.commandExists("git") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "git",
Status: status,
Description: "Version control system",
Required: true,
}
} }
func (b *BaseDistribution) detectMatugen() deps.Dependency { func (b *BaseDistribution) detectMatugen() deps.Dependency {
return b.detectCommand("matugen", "Material Design color generation tool") status := deps.StatusMissing
if b.commandExists("matugen") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "matugen",
Status: status,
Description: "Material Design color generation tool",
Required: true,
}
} }
func (b *BaseDistribution) detectDgop() deps.Dependency { func (b *BaseDistribution) detectDgop() deps.Dependency {
return b.detectCommand("dgop", "Desktop portal management tool") status := deps.StatusMissing
if b.commandExists("dgop") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dgop",
Status: status,
Description: "Desktop portal management tool",
Required: true,
}
} }
func (b *BaseDistribution) detectDMS() deps.Dependency { func (b *BaseDistribution) detectDMS() deps.Dependency {
@@ -216,6 +219,20 @@ func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
return dependencies return dependencies
} }
func (b *BaseDistribution) detectHyprpicker() deps.Dependency {
status := deps.StatusMissing
if b.commandExists("hyprpicker") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "hyprpicker",
Status: status,
Description: "Color picker for Wayland",
Required: true,
}
}
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency { func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
var dependencies []deps.Dependency var dependencies []deps.Dependency
@@ -223,7 +240,10 @@ func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
name string name string
description string description string
}{ }{
{"grim", "Screenshot utility for Wayland"},
{"slurp", "Region selection utility for Wayland"},
{"hyprctl", "Hyprland control utility"}, {"hyprctl", "Hyprland control utility"},
{"grimblast", "Screenshot script for Hyprland"},
{"jq", "JSON processor"}, {"jq", "JSON processor"},
} }
@@ -544,115 +564,6 @@ func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressCha
} }
} }
func (b *BaseDistribution) DetectTerminalFromDeps(dependencies []deps.Dependency) deps.Terminal {
for _, dep := range dependencies {
switch dep.Name {
case "ghostty":
return deps.TerminalGhostty
case "kitty":
return deps.TerminalKitty
case "alacritty":
return deps.TerminalAlacritty
}
}
return deps.TerminalGhostty
}
func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
envDir := filepath.Join(homeDir, ".config", "environment.d")
if err := os.MkdirAll(envDir, 0755); err != nil {
return fmt.Errorf("failed to create environment.d directory: %w", err)
}
var terminalCmd string
switch terminal {
case deps.TerminalGhostty:
terminalCmd = "ghostty"
case deps.TerminalKitty:
terminalCmd = "kitty"
case deps.TerminalAlacritty:
terminalCmd = "alacritty"
default:
terminalCmd = "ghostty"
}
content := fmt.Sprintf(`QT_QPA_PLATFORM=wayland
ELECTRON_OZONE_PLATFORM_HINT=auto
QT_QPA_PLATFORMTHEME=gtk3
QT_QPA_PLATFORMTHEME_QT6=gtk3
TERMINAL=%s
`, terminalCmd)
envFile := filepath.Join(envDir, "90-dms.conf")
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write environment config: %w", err)
}
b.log(fmt.Sprintf("Wrote environment config to %s", envFile))
return nil
}
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
cmd := exec.CommandContext(ctx, "systemctl", "--user", "enable", "--now", "dms")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to enable dms service: %w", err)
}
b.log("Enabled dms systemd user service")
switch wm {
case deps.WindowManagerNiri:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for niri.service")
}
case deps.WindowManagerHyprland:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil {
b.log("Warning: failed to add dms as a want for hyprland-session.target")
}
}
return nil
}
func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error {
if wm == deps.WindowManagerHyprland {
if err := b.WriteHyprlandSessionTarget(); err != nil {
return fmt.Errorf("failed to write hyprland session target: %w", err)
}
}
return nil
}
func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create systemd user directory: %w", err)
}
targetPath := filepath.Join(targetDir, "hyprland-session.target")
content := `[Unit]
Description=Hyprland Session Target
Requires=graphical-session.target
After=graphical-session.target
`
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
}
b.log(fmt.Sprintf("Wrote hyprland-session.target to %s", targetPath))
return nil
}
// installDMSBinary installs the DMS binary from GitHub releases // installDMSBinary installs the DMS binary from GitHub releases
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
b.log("Installing/updating DMS binary...") b.log("Installing/updating DMS binary...")
@@ -691,7 +602,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
return fmt.Errorf("failed to get user home directory: %w", err) return fmt.Errorf("failed to get user home directory: %w", err)
} }
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds") tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
if err := os.MkdirAll(tmpDir, 0o755); err != nil { if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err) return fmt.Errorf("failed to create temp directory: %w", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)

View File

@@ -7,7 +7,6 @@ import (
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) { func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
@@ -37,7 +36,7 @@ func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
} }
func TestBaseDistribution_detectDMS_Installed(t *testing.T) { func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
if !utils.CommandExists("git") { if !commandExists("git") {
t.Skip("git not available") t.Skip("git not available")
} }
@@ -81,7 +80,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
} }
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) { func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
if !utils.CommandExists("git") { if !commandExists("git") {
t.Skip("git not available") t.Skip("git not available")
} }
@@ -165,6 +164,11 @@ func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
} }
} }
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func TestBaseDistribution_versionCompare(t *testing.T) { func TestBaseDistribution_versionCompare(t *testing.T) {
logChan := make(chan string, 10) logChan := make(chan string, 10)
defer close(logChan) defer close(logChan)

View File

@@ -61,6 +61,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectWindowManager(wm)) dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell()) dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectXDGPortal()) dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectPolkitAgent())
dependencies = append(dependencies, d.detectAccountsService()) dependencies = append(dependencies, d.detectAccountsService())
if wm == deps.WindowManagerNiri { if wm == deps.WindowManagerNiri {
@@ -69,21 +70,66 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectMatugen()) dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDgop()) dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectHyprpicker())
dependencies = append(dependencies, d.detectClipboardTools()...) dependencies = append(dependencies, d.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
} }
func (d *DebianDistribution) detectXDGPortal() deps.Dependency { func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
return d.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", d.packageInstalled("xdg-desktop-portal-gtk")) status := deps.StatusMissing
if d.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if d.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
} }
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency { func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
return d.detectCommand("xwayland-satellite", "Xwayland support") status := deps.StatusMissing
if d.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (d *DebianDistribution) detectAccountsService() deps.Dependency { func (d *DebianDistribution) detectAccountsService() deps.Dependency {
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice")) status := deps.StatusMissing
if d.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {
@@ -93,65 +139,33 @@ func (d *DebianDistribution) packageInstalled(pkg string) bool {
} }
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{ packages := map[string]PackageMapping{
// Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem}, "kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
// DMS packages from OBS with variant support "dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]), "niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
"quickshell": d.getQuickshellMapping(variants["quickshell"]), "quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
} }
if wm == deps.WindowManagerNiri { if wm == deps.WindowManagerNiri {
niriVariant := variants["niri"] packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
packages["niri"] = d.getNiriMapping(niriVariant) packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
packages["xwayland-satellite"] = d.getXwaylandSatelliteMapping(niriVariant)
} }
return packages return packages
} }
func (d *DebianDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
}
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
}
func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "niri", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites, Phase: PhasePrerequisites,
@@ -178,7 +192,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential") checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil { if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential") cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil { if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err) return fmt.Errorf("failed to install build-essential: %w", err)
} }
@@ -195,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }
@@ -224,23 +238,8 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
return fmt.Errorf("failed to install prerequisites: %w", err) return fmt.Errorf("failed to install prerequisites: %w", err)
} }
systemPkgs, obsPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) systemPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Enable OBS repositories
if len(obsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling OBS repositories...",
IsComplete: false,
LogOutput: "Setting up OBS repositories for additional packages",
}
if err := d.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable OBS repositories: %w", err)
}
}
// System Packages
if len(systemPkgs) > 0 { if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -255,22 +254,6 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
} }
} }
// OBS Packages
obsPkgNames := d.extractPackageNames(obsPkgs)
if len(obsPkgNames) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
}
if err := d.installAPTPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err)
}
}
// Manual Builds
if len(manualPkgs) > 0 { if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -303,19 +286,6 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
terminal := d.DetectTerminalFromDeps(dependencies)
if err := d.WriteEnvironmentConfig(terminal); err != nil {
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := d.WriteWindowManagerConfig(wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := d.EnableDMSService(ctx, wm); err != nil {
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,
Progress: 1.0, Progress: 1.0,
@@ -327,9 +297,8 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
return nil return nil
} }
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) { func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
systemPkgs := []string{} systemPkgs := []string{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{} manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant) variantMap := make(map[string]deps.PackageVariant)
@@ -337,7 +306,7 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
variantMap[dep.Name] = dep.Variant variantMap[dep.Name] = dep.Variant
} }
packageMap := d.GetPackageMappingWithVariants(wm, variantMap) packageMap := d.GetPackageMapping(wm)
for _, dep := range dependencies { for _, dep := range dependencies {
if disabledFlags[dep.Name] { if disabledFlags[dep.Name] {
@@ -357,116 +326,12 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
switch pkgInfo.Repository { switch pkgInfo.Repository {
case RepoTypeSystem: case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name) systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual: case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name) manualPkgs = append(manualPkgs, dep.Name)
} }
} }
return systemPkgs, obsPkgs, manualPkgs, variantMap return systemPkgs, manualPkgs, variantMap
}
func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
osInfo, err := GetOSInfo()
if err != nil {
return fmt.Errorf("failed to get OS info: %w", err)
}
// Determine Debian version for OBS repository URL
debianVersion := "Debian_13"
if osInfo.VersionID == "testing" {
debianVersion = "Debian_Testing"
}
for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
d.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
// RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, debianVersion)
// Check if repository already exists
listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName)
checkCmd := exec.CommandContext(ctx, "test", "-f", listFile)
if checkCmd.Run() == nil {
d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
continue
}
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
// Create keyrings directory if it doesn't exist
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
if err := mkdirCmd.Run(); err != nil {
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
}
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
}
// Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Adding OBS repository %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
}
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
d.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
}
}
if len(enabledRepos) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Updating package lists...",
NeedsSudo: true,
CommandInfo: "sudo apt-get update",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
}
}
return nil
} }
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -476,7 +341,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} args := []string{"apt-get", "install", "-y"}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -586,7 +451,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup", CommandInfo: "sudo apt-get install rustup",
} }
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup") rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err) return fmt.Errorf("failed to install rustup: %w", err)
} }
@@ -625,10 +490,34 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go", CommandInfo: "sudo apt-get install golang-go",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go") installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90) return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
} }
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
d.log("Installing Ghostty using Debian installer script...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Running Ghostty Debian installer...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
LogOutput: "Installing Ghostty using pre-built Debian package",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
return fmt.Errorf("failed to install Ghostty: %w", err)
}
d.log("Ghostty installed successfully using Debian installer")
return nil
}
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
@@ -638,6 +527,10 @@ func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages
for _, pkg := range packages { for _, pkg := range packages {
switch pkg { switch pkg {
case "ghostty":
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
}
default: default:
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil { if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err) return fmt.Errorf("failed to install %s: %w", pkg, err)

View File

@@ -19,12 +19,10 @@ func init() {
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
} }
type FedoraDistribution struct { type FedoraDistribution struct {
@@ -76,6 +74,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectWindowManager(wm)) dependencies = append(dependencies, f.detectWindowManager(wm))
dependencies = append(dependencies, f.detectQuickshell()) dependencies = append(dependencies, f.detectQuickshell())
dependencies = append(dependencies, f.detectXDGPortal()) dependencies = append(dependencies, f.detectXDGPortal())
dependencies = append(dependencies, f.detectPolkitAgent())
dependencies = append(dependencies, f.detectAccountsService()) dependencies = append(dependencies, f.detectAccountsService())
// Hyprland-specific tools // Hyprland-specific tools
@@ -91,13 +90,38 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
// Base detections (common across distros) // Base detections (common across distros)
dependencies = append(dependencies, f.detectMatugen()) dependencies = append(dependencies, f.detectMatugen())
dependencies = append(dependencies, f.detectDgop()) dependencies = append(dependencies, f.detectDgop())
dependencies = append(dependencies, f.detectHyprpicker())
dependencies = append(dependencies, f.detectClipboardTools()...) dependencies = append(dependencies, f.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
} }
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency { func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
return f.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", f.packageInstalled("xdg-desktop-portal-gtk")) status := deps.StatusMissing
if f.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (f *FedoraDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if f.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
} }
func (f *FedoraDistribution) packageInstalled(pkg string) bool { func (f *FedoraDistribution) packageInstalled(pkg string) bool {
@@ -119,7 +143,9 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
// COPR packages // COPR packages
"quickshell": f.getQuickshellMapping(variants["quickshell"]), "quickshell": f.getQuickshellMapping(variants["quickshell"]),
@@ -132,7 +158,10 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
switch wm { switch wm {
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"]) packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"]) packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
packages["niri"] = f.getNiriMapping(variants["niri"]) packages["niri"] = f.getNiriMapping(variants["niri"])
@@ -156,15 +185,25 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"} return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
} }
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"} return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
} }
func (f *FedoraDistribution) getHyprpickerMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "hyprpicker-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
return PackageMapping{Name: "hyprpicker", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
}
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit { if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"} return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
} }
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"} return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
} }
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency { func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -344,19 +383,6 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
terminal := f.DetectTerminalFromDeps(dependencies)
if err := f.WriteEnvironmentConfig(terminal); err != nil {
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := f.WriteWindowManagerConfig(wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := f.EnableDMSService(ctx, wm); err != nil {
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
// Phase 7: Complete // Phase 7: Complete
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,
@@ -480,14 +506,6 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"} args := []string{"dnf", "install", "-y"}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{

View File

@@ -95,6 +95,7 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectWindowManager(wm)) dependencies = append(dependencies, g.detectWindowManager(wm))
dependencies = append(dependencies, g.detectQuickshell()) dependencies = append(dependencies, g.detectQuickshell())
dependencies = append(dependencies, g.detectXDGPortal()) dependencies = append(dependencies, g.detectXDGPortal())
dependencies = append(dependencies, g.detectPolkitAgent())
dependencies = append(dependencies, g.detectAccountsService()) dependencies = append(dependencies, g.detectAccountsService())
if wm == deps.WindowManagerHyprland { if wm == deps.WindowManagerHyprland {
@@ -107,21 +108,66 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectMatugen()) dependencies = append(dependencies, g.detectMatugen())
dependencies = append(dependencies, g.detectDgop()) dependencies = append(dependencies, g.detectDgop())
dependencies = append(dependencies, g.detectHyprpicker())
dependencies = append(dependencies, g.detectClipboardTools()...) dependencies = append(dependencies, g.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
} }
func (g *GentooDistribution) detectXDGPortal() deps.Dependency { func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
return g.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", g.packageInstalled("sys-apps/xdg-desktop-portal-gtk")) status := deps.StatusMissing
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if g.packageInstalled("mate-extra/mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
} }
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency { func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
return g.detectPackage("xwayland-satellite", "Xwayland support", g.packageInstalled("gui-apps/xwayland-satellite")) status := deps.StatusMissing
if g.packageInstalled("gui-apps/xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (g *GentooDistribution) detectAccountsService() deps.Dependency { func (g *GentooDistribution) detectAccountsService() deps.Dependency {
return g.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", g.packageInstalled("sys-apps/accountsservice")) status := deps.StatusMissing
if g.packageInstalled("sys-apps/accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (g *GentooDistribution) packageInstalled(pkg string) bool { func (g *GentooDistribution) packageInstalled(pkg string) bool {
@@ -142,7 +188,9 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"}, "alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"}, "xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"}, "qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"}, "qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
@@ -159,7 +207,10 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
switch wm { switch wm {
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"]) packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"]) packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
packages["grimblast"] = PackageMapping{Name: "gui-wm/hyprland-contrib", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
packages["niri"] = g.getNiriMapping(variants["niri"]) packages["niri"] = g.getNiriMapping(variants["niri"])
@@ -177,8 +228,16 @@ func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"} return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
} }
func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()} archKeyword := g.getArchKeyword()
if variant == deps.VariantGit {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
}
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
}
func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
} }
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping { func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
@@ -401,19 +460,6 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
terminal := g.DetectTerminalFromDeps(dependencies)
if err := g.WriteEnvironmentConfig(terminal); err != nil {
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := g.WriteWindowManagerConfig(wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := g.EnableDMSService(ctx, wm); err != nil {
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,
Progress: 1.0, Progress: 1.0,

View File

@@ -39,7 +39,6 @@ const (
RepoTypeAUR RepositoryType = "aur" // Arch User Repository RepoTypeAUR RepositoryType = "aur" // Arch User Repository
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
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
RepoTypeManual RepositoryType = "manual" // Manual build from source RepoTypeManual RepositoryType = "manual" // Manual build from source

View File

@@ -62,6 +62,10 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil { if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install dgop: %w", err) return fmt.Errorf("failed to install dgop: %w", err)
} }
case "grimblast":
if err := m.installGrimblast(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install grimblast: %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)
@@ -162,6 +166,62 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
return nil return nil
} }
func (m *ManualPackageInstaller) installGrimblast(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing grimblast script for Hyprland...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Downloading grimblast script...",
IsComplete: false,
CommandInfo: "curl grimblast script",
}
grimblastURL := "https://raw.githubusercontent.com/hyprwm/contrib/refs/heads/main/grimblast/grimblast"
tmpPath := filepath.Join(os.TempDir(), "grimblast")
downloadCmd := exec.CommandContext(ctx, "curl", "-L", "-o", tmpPath, grimblastURL)
if err := downloadCmd.Run(); err != nil {
m.logError("failed to download grimblast", err)
return fmt.Errorf("failed to download grimblast: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.5,
Step: "Making grimblast executable...",
IsComplete: false,
CommandInfo: "chmod +x grimblast",
}
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", tmpPath)
if err := chmodCmd.Run(); err != nil {
m.logError("failed to make grimblast executable", err)
return fmt.Errorf("failed to make grimblast executable: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing grimblast to /usr/local/bin...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cp grimblast /usr/local/bin/",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("cp %s /usr/local/bin/grimblast", tmpPath))
if err := installCmd.Run(); err != nil {
m.logError("failed to install grimblast", err)
return fmt.Errorf("failed to install grimblast: %w", err)
}
os.Remove(tmpPath)
m.log("grimblast installed successfully to /usr/local/bin")
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...")

View File

@@ -0,0 +1,466 @@
package distros
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func init() {
Register("nixos", "#7EBAE4", FamilyNix, func(config DistroConfig, logChan chan<- string) Distribution {
return NewNixOSDistribution(config, logChan)
})
}
type NixOSDistribution struct {
*BaseDistribution
config DistroConfig
}
func NewNixOSDistribution(config DistroConfig, logChan chan<- string) *NixOSDistribution {
base := NewBaseDistribution(logChan)
return &NixOSDistribution{
BaseDistribution: base,
config: config,
}
}
func (n *NixOSDistribution) GetID() string {
return n.config.ID
}
func (n *NixOSDistribution) GetColorHex() string {
return n.config.ColorHex
}
func (n *NixOSDistribution) GetFamily() DistroFamily {
return n.config.Family
}
func (n *NixOSDistribution) GetPackageManager() PackageManagerType {
return PackageManagerNix
}
func (n *NixOSDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return n.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (n *NixOSDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
// DMS at the top (shell is prominent)
dependencies = append(dependencies, n.detectDMS())
// Terminal with choice support
dependencies = append(dependencies, n.detectSpecificTerminal(terminal))
// Common detections using base methods
dependencies = append(dependencies, n.detectGit())
dependencies = append(dependencies, n.detectWindowManager(wm))
dependencies = append(dependencies, n.detectQuickshell())
dependencies = append(dependencies, n.detectXDGPortal())
dependencies = append(dependencies, n.detectPolkitAgent())
dependencies = append(dependencies, n.detectAccountsService())
// Hyprland-specific tools
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, n.detectHyprlandTools()...)
}
// Niri-specific tools
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, n.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, n.detectMatugen())
dependencies = append(dependencies, n.detectDgop())
dependencies = append(dependencies, n.detectHyprpicker())
dependencies = append(dependencies, n.detectClipboardTools()...)
return dependencies, nil
}
func (n *NixOSDistribution) detectDMS() deps.Dependency {
status := deps.StatusMissing
// For NixOS, check if quickshell can find the dms config
cmd := exec.Command("qs", "-c", "dms", "--list")
if err := cmd.Run(); err == nil {
status = deps.StatusInstalled
} else if n.packageInstalled("DankMaterialShell") {
// Fallback: check if flake is in profile
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Description: "Desktop Management System configuration (installed as flake)",
Required: true,
}
}
func (n *NixOSDistribution) detectXDGPortal() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (n *NixOSDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
switch wm {
case deps.WindowManagerHyprland:
status := deps.StatusMissing
description := "Dynamic tiling Wayland compositor"
if n.commandExists("hyprland") || n.commandExists("Hyprland") {
status = deps.StatusInstalled
} else {
description = "Install system-wide: programs.hyprland.enable = true; in configuration.nix"
}
return deps.Dependency{
Name: "hyprland",
Status: status,
Description: description,
Required: true,
}
case deps.WindowManagerNiri:
status := deps.StatusMissing
description := "Scrollable-tiling Wayland compositor"
if n.commandExists("niri") {
status = deps.StatusInstalled
} else {
description = "Install system-wide: environment.systemPackages = [ pkgs.niri ]; in configuration.nix"
}
return deps.Dependency{
Name: "niri",
Status: status,
Description: description,
Required: true,
}
default:
return deps.Dependency{
Name: "unknown-wm",
Status: deps.StatusMissing,
Description: "Unknown window manager",
Required: true,
}
}
}
func (n *NixOSDistribution) detectHyprlandTools() []deps.Dependency {
var dependencies []deps.Dependency
tools := []struct {
name string
description string
}{
{"grim", "Screenshot utility for Wayland"},
{"slurp", "Region selection utility for Wayland"},
{"hyprctl", "Hyprland control utility (comes with system Hyprland)"},
{"hyprpicker", "Color picker for Hyprland"},
{"grimblast", "Screenshot script for Hyprland"},
{"jq", "JSON processor"},
}
for _, tool := range tools {
status := deps.StatusMissing
// Special handling for hyprctl - it comes with system hyprland
if tool.name == "hyprctl" {
if n.commandExists("hyprctl") {
status = deps.StatusInstalled
}
} else {
if n.commandExists(tool.name) {
status = deps.StatusInstalled
}
}
dependencies = append(dependencies, deps.Dependency{
Name: tool.name,
Status: status,
Description: tool.description,
Required: true,
})
}
return dependencies
}
func (n *NixOSDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing
if n.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
}
func (n *NixOSDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
}
func (n *NixOSDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing
if n.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
}
func (n *NixOSDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("nix", "profile", "list")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(output), pkg)
}
func (n *NixOSDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
packages := map[string]PackageMapping{
"git": {Name: "nixpkgs#git", Repository: RepoTypeSystem},
"quickshell": {Name: "github:quickshell-mirror/quickshell", Repository: RepoTypeFlake},
"matugen": {Name: "github:InioX/matugen", Repository: RepoTypeFlake},
"dgop": {Name: "github:AvengeMedia/dgop", Repository: RepoTypeFlake},
"dms (DankMaterialShell)": {Name: "github:AvengeMedia/DankMaterialShell", Repository: RepoTypeFlake},
"ghostty": {Name: "nixpkgs#ghostty", Repository: RepoTypeSystem},
"alacritty": {Name: "nixpkgs#alacritty", Repository: RepoTypeSystem},
"cliphist": {Name: "nixpkgs#cliphist", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "nixpkgs#wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "nixpkgs#xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "nixpkgs#mate.mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "nixpkgs#accountsservice", Repository: RepoTypeSystem},
"hyprpicker": {Name: "nixpkgs#hyprpicker", Repository: RepoTypeSystem},
}
// Note: Window managers (hyprland/niri) should be installed system-wide on NixOS
// We only install the tools here
switch wm {
case deps.WindowManagerHyprland:
// Skip hyprland itself - should be installed system-wide
packages["grim"] = PackageMapping{Name: "nixpkgs#grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "nixpkgs#slurp", Repository: RepoTypeSystem}
packages["grimblast"] = PackageMapping{Name: "github:hyprwm/contrib#grimblast", Repository: RepoTypeFlake}
packages["jq"] = PackageMapping{Name: "nixpkgs#jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
// Skip niri itself - should be installed system-wide
packages["xwayland-satellite"] = PackageMapping{Name: "nixpkgs#xwayland-satellite", Repository: RepoTypeFlake}
}
return packages
}
func (n *NixOSDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.10,
Step: "NixOS prerequisites ready",
IsComplete: false,
LogOutput: "NixOS package manager is ready to use",
}
return nil
}
func (n *NixOSDistribution) 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 {
// Phase 1: Check Prerequisites
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if err := n.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
nixpkgsPkgs, flakePkgs, _ := n.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Phase 2: Nixpkgs Packages
if len(nixpkgsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d packages from nixpkgs...", len(nixpkgsPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(nixpkgsPkgs, ", ")),
}
if err := n.installNixpkgsPackages(ctx, nixpkgsPkgs, progressChan); err != nil {
return fmt.Errorf("failed to install nixpkgs packages: %w", err)
}
}
// Phase 3: Flake Packages
if len(flakePkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d packages from flakes...", len(flakePkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing flake packages: %s", strings.Join(flakePkgs, ", ")),
}
if err := n.installFlakePackages(ctx, flakePkgs, progressChan); err != nil {
return fmt.Errorf("failed to install flake packages: %w", err)
}
}
// Phase 4: Configuration
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
if err := n.postInstallConfig(progressChan); err != nil {
return fmt.Errorf("failed to configure system: %w", err)
}
// Phase 5: Complete
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (n *NixOSDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
nixpkgsPkgs := []string{}
flakePkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := n.GetPackageMapping(wm)
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 {
n.log(fmt.Sprintf("Warning: No package mapping found for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeSystem:
nixpkgsPkgs = append(nixpkgsPkgs, pkgInfo.Name)
case RepoTypeFlake:
flakePkgs = append(flakePkgs, pkgInfo.Name)
}
}
return nixpkgsPkgs, flakePkgs, variantMap
}
func (n *NixOSDistribution) installNixpkgsPackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
n.log(fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(packages, ", ")))
args := []string{"profile", "install"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing nixpkgs packages...",
IsComplete: false,
CommandInfo: fmt.Sprintf("nix %s", strings.Join(args, " ")),
}
cmd := exec.CommandContext(ctx, "nix", args...)
return n.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (n *NixOSDistribution) installFlakePackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
n.log(fmt.Sprintf("Installing flake packages: %s", strings.Join(packages, ", ")))
baseProgress := 0.65
progressStep := 0.20 / float64(len(packages))
for i, pkg := range packages {
currentProgress := baseProgress + (float64(i) * progressStep)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: currentProgress,
Step: fmt.Sprintf("Installing flake package %s (%d/%d)...", pkg, i+1, len(packages)),
IsComplete: false,
CommandInfo: fmt.Sprintf("nix profile install %s", pkg),
}
cmd := exec.CommandContext(ctx, "nix", "profile", "install", pkg)
if err := n.runWithProgress(cmd, progressChan, PhaseAURPackages, currentProgress, currentProgress+progressStep); err != nil {
return fmt.Errorf("failed to install flake package %s: %w", pkg, err)
}
}
return nil
}
func (n *NixOSDistribution) postInstallConfig(progressChan chan<- InstallProgressMsg) error {
// For NixOS, DMS is installed as a flake package, so we skip both the binary installation and git clone
// The flake installation handles both the binary and config files correctly
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.95,
Step: "NixOS configuration complete",
IsComplete: false,
LogOutput: "DMS installed via flake - binary and config handled by Nix",
}
return nil
}

View File

@@ -66,6 +66,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectWindowManager(wm)) dependencies = append(dependencies, o.detectWindowManager(wm))
dependencies = append(dependencies, o.detectQuickshell()) dependencies = append(dependencies, o.detectQuickshell())
dependencies = append(dependencies, o.detectXDGPortal()) dependencies = append(dependencies, o.detectXDGPortal())
dependencies = append(dependencies, o.detectPolkitAgent())
dependencies = append(dependencies, o.detectAccountsService()) dependencies = append(dependencies, o.detectAccountsService())
// Hyprland-specific tools // Hyprland-specific tools
@@ -81,13 +82,38 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
// Base detections (common across distros) // Base detections (common across distros)
dependencies = append(dependencies, o.detectMatugen()) dependencies = append(dependencies, o.detectMatugen())
dependencies = append(dependencies, o.detectDgop()) dependencies = append(dependencies, o.detectDgop())
dependencies = append(dependencies, o.detectHyprpicker())
dependencies = append(dependencies, o.detectClipboardTools()...) dependencies = append(dependencies, o.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
} }
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency { func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
return o.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", o.packageInstalled("xdg-desktop-portal-gtk")) status := deps.StatusMissing
if o.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if o.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
} }
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool { func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
@@ -109,59 +135,34 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem}, "cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
// DMS packages from OBS // Manual builds
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
"quickshell": o.getQuickshellMapping(variants["quickshell"]), "dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
} }
switch wm { switch wm {
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
// Niri stable has native package support on openSUSE packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
niriVariant := variants["niri"] packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
packages["niri"] = o.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = o.getXwaylandSatelliteMapping(niriVariant)
} }
return packages return packages
} }
func (o *OpenSUSEDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
}
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
}
func (o *OpenSUSEDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (o *OpenSUSEDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
}
func (o *OpenSUSEDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency { func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if o.commandExists("xwayland-satellite") { if o.commandExists("xwayland-satellite") {
@@ -293,23 +294,9 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
return fmt.Errorf("failed to install prerequisites: %w", err) return fmt.Errorf("failed to install prerequisites: %w", err)
} }
systemPkgs, obsPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) systemPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Enable OBS repositories // Phase 2: System Packages (Zypper)
if len(obsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling OBS repositories...",
IsComplete: false,
LogOutput: "Setting up OBS repositories for additional packages",
}
if err := o.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable OBS repositories: %w", err)
}
}
// Phase 3: System Packages (Zypper)
if len(systemPkgs) > 0 { if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -324,22 +311,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
} }
} }
// OBS Packages // Phase 3: Manual Builds
obsPkgNames := o.extractPackageNames(obsPkgs)
if len(obsPkgNames) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
}
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err)
}
}
// Manual Builds
if len(manualPkgs) > 0 { if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -353,7 +325,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
} }
} }
// Configuration // Phase 4: Configuration
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration, Phase: PhaseConfiguration,
Progress: 0.90, Progress: 0.90,
@@ -362,20 +334,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
terminal := o.DetectTerminalFromDeps(dependencies) // Phase 5: Complete
if err := o.WriteEnvironmentConfig(terminal); err != nil {
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := o.WriteWindowManagerConfig(wm); err != nil {
o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := o.EnableDMSService(ctx, wm); err != nil {
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
// Complete
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,
Progress: 1.0, Progress: 1.0,
@@ -387,9 +346,8 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
return nil return nil
} }
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) { func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
systemPkgs := []string{} systemPkgs := []string{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{} manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant) variantMap := make(map[string]deps.PackageVariant)
@@ -417,80 +375,12 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
switch pkgInfo.Repository { switch pkgInfo.Repository {
case RepoTypeSystem: case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name) systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual: case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name) manualPkgs = append(manualPkgs, dep.Name)
} }
} }
return systemPkgs, obsPkgs, manualPkgs, variantMap return systemPkgs, manualPkgs, variantMap
}
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
// RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
repoPath, pkg.RepoURL)
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
if checkCmd.Run() == nil {
o.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Enabling OBS repo %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
}
cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
}
enabledRepos[pkg.RepoURL] = true
o.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
}
}
// Refresh repositories with GPG auto-import
if len(enabledRepos) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Refreshing repositories...",
NeedsSudo: true,
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
}
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to refresh repositories: %w", err)
}
}
return nil
} }
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {

View File

@@ -3,7 +3,9 @@ package distros
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -64,6 +66,7 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, u.detectWindowManager(wm)) dependencies = append(dependencies, u.detectWindowManager(wm))
dependencies = append(dependencies, u.detectQuickshell()) dependencies = append(dependencies, u.detectQuickshell())
dependencies = append(dependencies, u.detectXDGPortal()) dependencies = append(dependencies, u.detectXDGPortal())
dependencies = append(dependencies, u.detectPolkitAgent())
dependencies = append(dependencies, u.detectAccountsService()) dependencies = append(dependencies, u.detectAccountsService())
// Hyprland-specific tools // Hyprland-specific tools
@@ -79,21 +82,66 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
// Base detections (common across distros) // Base detections (common across distros)
dependencies = append(dependencies, u.detectMatugen()) dependencies = append(dependencies, u.detectMatugen())
dependencies = append(dependencies, u.detectDgop()) dependencies = append(dependencies, u.detectDgop())
dependencies = append(dependencies, u.detectHyprpicker())
dependencies = append(dependencies, u.detectClipboardTools()...) dependencies = append(dependencies, u.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
} }
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency { func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
return u.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", u.packageInstalled("xdg-desktop-portal-gtk")) status := deps.StatusMissing
if u.packageInstalled("xdg-desktop-portal-gtk") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xdg-desktop-portal-gtk",
Status: status,
Description: "Desktop integration portal for GTK",
Required: true,
}
}
func (u *UbuntuDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if u.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
} }
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency { func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
return u.detectCommand("xwayland-satellite", "Xwayland support") status := deps.StatusMissing
if u.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
} }
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency { func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice")) status := deps.StatusMissing
if u.packageInstalled("accountsservice") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "accountsservice",
Status: status,
Description: "D-Bus interface for user account query and manipulation",
Required: true,
}
} }
func (u *UbuntuDistribution) packageInstalled(pkg string) bool { func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
@@ -103,10 +151,6 @@ func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
} }
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return u.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{ packages := map[string]PackageMapping{
// Standard APT packages // Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
@@ -114,60 +158,37 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"},
// DMS packages from PPAs // Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs)
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
"quickshell": u.getQuickshellMapping(variants["quickshell"]), "niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
} }
switch wm { switch wm {
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
// Use the cppiber PPA for Hyprland // Use the cppiber PPA for Hyprland
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"} packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"} packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
niriVariant := variants["niri"] packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
packages["niri"] = u.getNiriMapping(niriVariant) packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
packages["xwayland-satellite"] = u.getXwaylandSatelliteMapping(niriVariant)
} }
return packages return packages
} }
func (u *UbuntuDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dms-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms-git"}
}
return PackageMapping{Name: "dms", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms"}
}
func (u *UbuntuDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
func (u *UbuntuDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
return PackageMapping{Name: "niri", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
func (u *UbuntuDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites, Phase: PhasePrerequisites,
@@ -322,19 +343,6 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
terminal := u.DetectTerminalFromDeps(dependencies)
if err := u.WriteEnvironmentConfig(terminal); err != nil {
u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := u.WriteWindowManagerConfig(wm); err != nil {
u.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
}
if err := u.EnableDMSService(ctx, wm); err != nil {
u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
// Phase 7: Complete // Phase 7: Complete
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,
@@ -357,7 +365,7 @@ func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency,
variantMap[dep.Name] = dep.Variant variantMap[dep.Name] = dep.Variant
} }
packageMap := u.GetPackageMappingWithVariants(wm, variantMap) packageMap := u.GetPackageMapping(wm)
for _, dep := range dependencies { for _, dep := range dependencies {
if disabledFlags[dep.Name] { if disabledFlags[dep.Name] {
@@ -537,6 +545,10 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
buildDeps["libxcb1-dev"] = true buildDeps["libxcb1-dev"] = true
buildDeps["libpipewire-0.3-dev"] = true buildDeps["libpipewire-0.3-dev"] = true
buildDeps["libpam0g-dev"] = true buildDeps["libpam0g-dev"] = true
case "ghostty":
buildDeps["curl"] = true
buildDeps["libgtk-4-dev"] = true
buildDeps["libadwaita-1-dev"] = true
case "matugen": case "matugen":
buildDeps["curl"] = true buildDeps["curl"] = true
case "cliphist": case "cliphist":
@@ -550,6 +562,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 "ghostty":
if err := u.installZig(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Zig: %w", err)
}
case "cliphist", "dgop": case "cliphist", "dgop":
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil { if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err) return fmt.Errorf("failed to install Go: %w", err)
@@ -613,6 +629,40 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
return nil return nil
} }
func (u *UbuntuDistribution) installZig(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if u.commandExists("zig") {
return nil
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
zigUrl := "https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz"
zigTmp := filepath.Join(cacheDir, "zig.tar.xz")
downloadCmd := exec.CommandContext(ctx, "curl", "-L", zigUrl, "-o", zigTmp)
if err := u.runWithProgress(downloadCmd, progressChan, PhaseSystemPackages, 0.84, 0.85); err != nil {
return fmt.Errorf("failed to download Zig: %w", err)
}
extractCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("tar -xf %s -C /opt/", zigTmp))
if err := u.runWithProgress(extractCmd, progressChan, PhaseSystemPackages, 0.85, 0.86); err != nil {
return fmt.Errorf("failed to extract Zig: %w", err)
}
linkCmd := ExecSudoCommand(ctx, sudoPassword,
"ln -sf /opt/zig-linux-x86_64-0.11.0/zig /usr/local/bin/zig")
return u.runWithProgress(linkCmd, progressChan, PhaseSystemPackages, 0.86, 0.87)
}
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if u.commandExists("go") { if u.commandExists("go") {
return nil return nil
@@ -660,6 +710,30 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90) return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
} }
func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
u.log("Installing Ghostty using Ubuntu installer script...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Running Ghostty Ubuntu installer...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
LogOutput: "Installing Ghostty using pre-built Ubuntu package",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
if err := u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
return fmt.Errorf("failed to install Ghostty: %w", err)
}
u.log("Ghostty installed successfully using Ubuntu installer")
return nil
}
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
@@ -669,6 +743,10 @@ func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages
for _, pkg := range packages { for _, pkg := range packages {
switch pkg { switch pkg {
case "ghostty":
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
}
default: default:
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil { if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err) return fmt.Errorf("failed to install %s: %w", pkg, err)

View File

@@ -105,19 +105,14 @@ type MenuItem struct {
func NewModel(version string) Model { func NewModel(version string) Model {
detector, _ := NewDetector() detector, _ := NewDetector()
var dependencies []DependencyInfo dependencies := detector.GetInstalledComponents()
var hyprlandInstalled, niriInstalled bool
var err error
if detector != nil {
dependencies = detector.GetInstalledComponents()
// Use the proper detection method for both window managers // Use the proper detection method for both window managers
hyprlandInstalled, niriInstalled, err = detector.GetWindowManagerStatus() hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
if err != nil { if err != nil {
// Fallback to false if detection fails // Fallback to false if detection fails
hyprlandInstalled = false hyprlandInstalled = false
niriInstalled = false niriInstalled = false
}
} }
updateToggles := make(map[string]bool) updateToggles := make(map[string]bool)
@@ -286,13 +281,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, loadInstalledPlugins return m, loadInstalledPlugins
} }
return m, nil return m, nil
case pluginUpdatedMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsError = ""
}
return m, nil
case pluginInstalledMsg: case pluginInstalledMsg:
if msg.err != nil { if msg.err != nil {
m.pluginsError = msg.err.Error() m.pluginsError = msg.err.Error()

View File

@@ -75,13 +75,14 @@ type MenuItem struct {
func NewModel(version string) Model { func NewModel(version string) Model {
detector, _ := NewDetector() detector, _ := NewDetector()
dependencies := detector.GetInstalledComponents()
var dependencies []DependencyInfo // Use the proper detection method for both window managers
var hyprlandInstalled, niriInstalled bool hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
if err != nil {
if detector != nil { // Fallback to false if detection fails
dependencies = detector.GetInstalledComponents() hyprlandInstalled = false
hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus() niriInstalled = false
} }
m := Model{ m := Model{
@@ -200,13 +201,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, loadInstalledPlugins return m, loadInstalledPlugins
} }
return m, nil return m, nil
case pluginUpdatedMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsError = ""
}
return m, nil
case pluginInstalledMsg: case pluginInstalledMsg:
if msg.err != nil { if msg.err != nil {
m.pluginsError = msg.err.Error() m.pluginsError = msg.err.Error()

View File

@@ -122,8 +122,15 @@ func (d *Detector) GetInstalledComponents() []DependencyInfo {
return []DependencyInfo{} return []DependencyInfo{}
} }
isNixOS := d.isNixOS()
var components []DependencyInfo var components []DependencyInfo
for _, dep := range dependencies { for _, dep := range dependencies {
// On NixOS, filter out the window managers themselves but keep their components
if isNixOS && (dep.Name == "hyprland" || dep.Name == "niri") {
continue
}
components = append(components, DependencyInfo{ components = append(components, DependencyInfo{
Name: dep.Name, Name: dep.Name,
Status: dep.Status, Status: dep.Status,
@@ -135,6 +142,23 @@ func (d *Detector) GetInstalledComponents() []DependencyInfo {
return components return components
} }
func (d *Detector) isNixOS() bool {
_, err := os.Stat("/etc/nixos")
if err == nil {
return true
}
// Alternative check
if _, err := os.Stat("/nix/store"); err == nil {
// Also check for nixos-version command
if d.commandExists("nixos-version") {
return true
}
}
return false
}
type DependencyInfo struct { type DependencyInfo struct {
Name string Name string
Status deps.DependencyStatus Status deps.DependencyStatus

View File

@@ -227,11 +227,6 @@ func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd)
plugin := m.installedPluginsList[m.selectedInstalledIndex] plugin := m.installedPluginsList[m.selectedInstalledIndex]
return m, uninstallPlugin(plugin) return m, uninstallPlugin(plugin)
} }
case "p":
if m.selectedInstalledIndex < len(m.installedPluginsList) {
plugin := m.installedPluginsList[m.selectedInstalledIndex]
return m, updatePlugin(plugin)
}
} }
return m, nil return m, nil
} }
@@ -251,11 +246,6 @@ type pluginInstalledMsg struct {
err error err error
} }
type pluginUpdatedMsg struct {
pluginName string
err error
}
func loadInstalledPlugins() tea.Msg { func loadInstalledPlugins() tea.Msg {
manager, err := plugins.NewManager() manager, err := plugins.NewManager()
if err != nil { if err != nil {
@@ -347,31 +337,3 @@ func uninstallPlugin(plugin pluginInfo) tea.Cmd {
return pluginUninstalledMsg{pluginName: plugin.Name} return pluginUninstalledMsg{pluginName: plugin.Name}
} }
} }
func updatePlugin(plugin pluginInfo) tea.Cmd {
return func() tea.Msg {
manager, err := plugins.NewManager()
if err != nil {
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
}
p := plugins.Plugin{
ID: plugin.ID,
Name: plugin.Name,
Category: plugin.Category,
Author: plugin.Author,
Description: plugin.Description,
Repo: plugin.Repo,
Path: plugin.Path,
Capabilities: plugin.Capabilities,
Compositors: plugin.Compositors,
Dependencies: plugin.Dependencies,
}
if err := manager.Update(p); err != nil {
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
}
return pluginUpdatedMsg{pluginName: plugin.Name}
}
}

View File

@@ -109,9 +109,6 @@ func (m Model) renderAboutView() string {
b.WriteString(normalStyle.Render("Components:")) b.WriteString(normalStyle.Render("Components:"))
b.WriteString("\n") b.WriteString("\n")
if len(m.dependencies) == 0 {
b.WriteString(normalStyle.Render("\n Component detection not supported on this platform."))
}
for _, dep := range m.dependencies { for _, dep := range m.dependencies {
status := "✗" status := "✗"
if dep.Status == 1 { if dep.Status == 1 {

View File

@@ -514,7 +514,7 @@ func (m Model) categorizeDependencies() map[string][]DependencyInfo {
switch dep.Name { switch dep.Name {
case "dms (DankMaterialShell)", "quickshell": case "dms (DankMaterialShell)", "quickshell":
categories["Shell"] = append(categories["Shell"], dep) categories["Shell"] = append(categories["Shell"], dep)
case "hyprland", "hyprctl": case "hyprland", "grim", "slurp", "hyprctl", "grimblast":
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep) categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
case "niri": case "niri":
categories["Niri Components"] = append(categories["Niri Components"], dep) categories["Niri Components"] = append(categories["Niri Components"], dep)

View File

@@ -11,7 +11,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
// DetectDMSPath checks for DMS installation following XDG Base Directory specification // DetectDMSPath checks for DMS installation following XDG Base Directory specification
@@ -23,10 +22,10 @@ func DetectDMSPath() (string, error) {
func DetectCompositors() []string { func DetectCompositors() []string {
var compositors []string var compositors []string
if utils.CommandExists("niri") { if commandExists("niri") {
compositors = append(compositors, "niri") compositors = append(compositors, "niri")
} }
if utils.CommandExists("Hyprland") { if commandExists("Hyprland") {
compositors = append(compositors, "Hyprland") compositors = append(compositors, "Hyprland")
} }
@@ -63,7 +62,7 @@ func PromptCompositorChoice(compositors []string) (string, error) {
// EnsureGreetdInstalled checks if greetd is installed and installs it if not // EnsureGreetdInstalled checks if greetd is installed and installs it if not
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error { func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
if utils.CommandExists("greetd") { if commandExists("greetd") {
logFunc("✓ greetd is already installed") logFunc("✓ greetd is already installed")
return nil return nil
} }
@@ -145,7 +144,7 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
// Check if dms-greeter is already in PATH // Check if dms-greeter is already in PATH
if utils.CommandExists("dms-greeter") { if commandExists("dms-greeter") {
logFunc("✓ dms-greeter wrapper already installed") logFunc("✓ dms-greeter wrapper already installed")
} else { } else {
// Install the wrapper script // Install the wrapper script
@@ -205,7 +204,7 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal // SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error { func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
if !utils.CommandExists("setfacl") { if !commandExists("setfacl") {
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.") logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
logFunc(" If theme sync doesn't work, you may need to install acl package:") logFunc(" If theme sync doesn't work, you may need to install acl package:")
logFunc(" - Fedora/RHEL: sudo dnf install acl") logFunc(" - Fedora/RHEL: sudo dnf install acl")
@@ -368,7 +367,7 @@ func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) e
} }
} }
runSudoCmd(sudoPassword, "rm", "-f", link.target) //nolint:errcheck runSudoCmd(sudoPassword, "rm", "-f", link.target)
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil { if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err))
@@ -420,7 +419,7 @@ user = "greeter"
// Determine wrapper command path // Determine wrapper command path
wrapperCmd := "dms-greeter" wrapperCmd := "dms-greeter"
if !utils.CommandExists("dms-greeter") { if !commandExists("dms-greeter") {
wrapperCmd = "/usr/local/bin/dms-greeter" wrapperCmd = "/usr/local/bin/dms-greeter"
} }
@@ -487,3 +486,8 @@ func runSudoCmd(sudoPassword string, command string, args ...string) error {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() return cmd.Run()
} }
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}

View File

@@ -5,8 +5,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type DiscoveryConfig struct { type DiscoveryConfig struct {
@@ -16,7 +14,13 @@ type DiscoveryConfig struct {
func DefaultDiscoveryConfig() *DiscoveryConfig { func DefaultDiscoveryConfig() *DiscoveryConfig {
var searchPaths []string var searchPaths []string
configHome := utils.XDGConfigHome() configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(homeDir, ".config")
}
}
if configHome != "" { if configHome != "" {
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets")) searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
} }
@@ -39,7 +43,7 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
var files []string var files []string
for _, searchPath := range d.SearchPaths { for _, searchPath := range d.SearchPaths {
expandedPath, err := utils.ExpandPath(searchPath) expandedPath, err := expandPath(searchPath)
if err != nil { if err != nil {
continue continue
} }
@@ -70,6 +74,20 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
return files, nil return files, nil
} }
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if filepath.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
return filepath.Clean(expandedPath), nil
}
type JSONProviderFactory func(filePath string) (Provider, error) type JSONProviderFactory func(filePath string) (Provider, error)
var jsonProviderFactory JSONProviderFactory var jsonProviderFactory JSONProviderFactory

View File

@@ -4,8 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestDefaultDiscoveryConfig(t *testing.T) { func TestDefaultDiscoveryConfig(t *testing.T) {
@@ -274,13 +272,13 @@ func TestExpandPathInDiscovery(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := utils.ExpandPath(tt.input) result, err := expandPath(tt.input)
if err != nil { if err != nil {
t.Fatalf("expandPath failed: %v", err) t.Fatalf("expandPath failed: %v", err)
} }
if result != tt.expected { if result != tt.expected {
t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected) t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected)
} }
}) })
} }

View File

@@ -87,22 +87,20 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind { func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind {
key := h.formatKey(kb) key := h.formatKey(kb)
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
desc := kb.Comment desc := kb.Comment
if desc == "" { if desc == "" {
desc = rawAction desc = h.generateDescription(kb.Dispatcher, kb.Params)
} }
return keybinds.Keybind{ return keybinds.Keybind{
Key: key, Key: key,
Description: desc, Description: desc,
Action: rawAction,
Subcategory: subcategory, Subcategory: subcategory,
} }
} }
func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string { func (h *HyprlandProvider) generateDescription(dispatcher, params string) string {
if params != "" { if params != "" {
return dispatcher + " " + params return dispatcher + " " + params
} }

View File

@@ -5,8 +5,6 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -44,9 +42,14 @@ func NewHyprlandParser() *HyprlandParser {
} }
func (p *HyprlandParser) ReadContent(directory string) error { func (p *HyprlandParser) ReadContent(directory string) error {
expandedDir, err := utils.ExpandPath(directory) expandedDir := os.ExpandEnv(directory)
if err != nil { expandedDir = filepath.Clean(expandedDir)
return err if strings.HasPrefix(expandedDir, "~") {
home, err := os.UserHomeDir()
if err != nil {
return err
}
expandedDir = filepath.Join(home, expandedDir[1:])
} }
info, err := os.Stat(expandedDir) info, err := os.Stat(expandedDir)

View File

@@ -7,7 +7,6 @@ import (
"path/filepath" "path/filepath"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type JSONFileProvider struct { type JSONFileProvider struct {
@@ -20,7 +19,7 @@ func NewJSONFileProvider(filePath string) (*JSONFileProvider, error) {
return nil, fmt.Errorf("file path cannot be empty") return nil, fmt.Errorf("file path cannot be empty")
} }
expandedPath, err := utils.ExpandPath(filePath) expandedPath, err := expandPath(filePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to expand path: %w", err) return nil, fmt.Errorf("failed to expand path: %w", err)
} }
@@ -44,7 +43,7 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
return nil, fmt.Errorf("failed to read file: %w", err) return nil, fmt.Errorf("failed to read file: %w", err)
} }
var rawData map[string]any var rawData map[string]interface{}
if err := json.Unmarshal(data, &rawData); err != nil { if err := json.Unmarshal(data, &rawData); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err) return nil, fmt.Errorf("failed to parse JSON: %w", err)
} }
@@ -63,9 +62,9 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
} }
switch binds := bindsRaw.(type) { switch binds := bindsRaw.(type) {
case map[string]any: case map[string]interface{}:
for category, categoryBindsRaw := range binds { for category, categoryBindsRaw := range binds {
categoryBindsList, ok := categoryBindsRaw.([]any) categoryBindsList, ok := categoryBindsRaw.([]interface{})
if !ok { if !ok {
continue continue
} }
@@ -79,12 +78,11 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
categorizedBinds[category] = keybindsList categorizedBinds[category] = keybindsList
} }
case []any: case []interface{}:
flatBindsJSON, _ := json.Marshal(binds) flatBindsJSON, _ := json.Marshal(binds)
var flatBinds []struct { var flatBinds []struct {
Key string `json:"key"` Key string `json:"key"`
Description string `json:"desc"` Description string `json:"desc"`
Action string `json:"action,omitempty"`
Category string `json:"cat,omitempty"` Category string `json:"cat,omitempty"`
Subcategory string `json:"subcat,omitempty"` Subcategory string `json:"subcat,omitempty"`
} }
@@ -101,7 +99,6 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
kb := keybinds.Keybind{ kb := keybinds.Keybind{
Key: bind.Key, Key: bind.Key,
Description: bind.Description, Description: bind.Description,
Action: bind.Action,
Subcategory: bind.Subcategory, Subcategory: bind.Subcategory,
} }
categorizedBinds[category] = append(categorizedBinds[category], kb) categorizedBinds[category] = append(categorizedBinds[category], kb)
@@ -117,3 +114,17 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
Binds: categorizedBinds, Binds: categorizedBinds,
}, nil }, nil
} }
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if filepath.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
return filepath.Clean(expandedPath), nil
}

View File

@@ -4,8 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
func TestNewJSONFileProvider(t *testing.T) { func TestNewJSONFileProvider(t *testing.T) {
@@ -268,13 +266,13 @@ func TestExpandPath(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result, err := utils.ExpandPath(tt.input) result, err := expandPath(tt.input)
if err != nil { if err != nil {
t.Fatalf("expandPath failed: %v", err) t.Fatalf("expandPath failed: %v", err)
} }
if result != tt.expected { if result != tt.expected {
t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected) t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected)
} }
}) })
} }

View File

@@ -84,21 +84,19 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind { func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind {
key := m.formatKey(kb) key := m.formatKey(kb)
rawAction := m.formatRawAction(kb.Command, kb.Params)
desc := kb.Comment desc := kb.Comment
if desc == "" { if desc == "" {
desc = rawAction desc = m.generateDescription(kb.Command, kb.Params)
} }
return keybinds.Keybind{ return keybinds.Keybind{
Key: key, Key: key,
Description: desc, Description: desc,
Action: rawAction,
} }
} }
func (m *MangoWCProvider) formatRawAction(command, params string) string { func (m *MangoWCProvider) generateDescription(command, params string) string {
if params != "" { if params != "" {
return command + " " + params return command + " " + params
} }

View File

@@ -5,8 +5,6 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -36,9 +34,14 @@ func NewMangoWCParser() *MangoWCParser {
} }
func (p *MangoWCParser) ReadContent(path string) error { func (p *MangoWCParser) ReadContent(path string) error {
expandedPath, err := utils.ExpandPath(path) expandedPath := os.ExpandEnv(path)
if err != nil { expandedPath = filepath.Clean(expandedPath)
return err if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return err
}
expandedPath = filepath.Join(home, expandedPath[1:])
} }
info, err := os.Stat(expandedPath) info, err := os.Stat(expandedPath)

View File

@@ -1,587 +0,0 @@
package providers
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/sblinch/kdl-go"
"github.com/sblinch/kdl-go/document"
)
type NiriProvider struct {
configDir string
dmsBindsIncluded bool
parsed bool
}
func NewNiriProvider(configDir string) *NiriProvider {
if configDir == "" {
configDir = defaultNiriConfigDir()
}
return &NiriProvider{
configDir: configDir,
}
}
func defaultNiriConfigDir() string {
return filepath.Join(utils.XDGConfigHome(), "niri")
}
func (n *NiriProvider) Name() string {
return "niri"
}
func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
result, err := ParseNiriKeys(n.configDir)
if err != nil {
return nil, fmt.Errorf("failed to parse niri config: %w", err)
}
n.dmsBindsIncluded = result.DMSBindsIncluded
n.parsed = true
categorizedBinds := make(map[string][]keybinds.Keybind)
n.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
sheet := &keybinds.CheatSheet{
Title: "Niri Keybinds",
Provider: n.Name(),
Binds: categorizedBinds,
DMSBindsIncluded: result.DMSBindsIncluded,
}
if result.DMSStatus != nil {
sheet.DMSStatus = &keybinds.DMSBindsStatus{
Exists: result.DMSStatus.Exists,
Included: result.DMSStatus.Included,
IncludePosition: result.DMSStatus.IncludePosition,
TotalIncludes: result.DMSStatus.TotalIncludes,
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage,
}
}
return sheet, nil
}
func (n *NiriProvider) HasDMSBindsIncluded() bool {
if n.parsed {
return n.dmsBindsIncluded
}
result, err := ParseNiriKeys(n.configDir)
if err != nil {
return false
}
n.dmsBindsIncluded = result.DMSBindsIncluded
n.parsed = true
return n.dmsBindsIncluded
}
func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*NiriKeyBinding) {
currentSubcat := subcategory
if section.Name != "" {
currentSubcat = section.Name
}
for _, kb := range section.Keybinds {
category := n.categorizeByAction(kb.Action)
bind := n.convertKeybind(&kb, currentSubcat, conflicts)
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
for _, child := range section.Children {
n.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
}
}
func (n *NiriProvider) categorizeByAction(action string) string {
switch {
case action == "next-window" || action == "previous-window":
return "Alt-Tab"
case strings.Contains(action, "screenshot"):
return "Screenshot"
case action == "show-hotkey-overlay" || action == "toggle-overview":
return "Overview"
case action == "quit" ||
action == "power-off-monitors" ||
action == "toggle-keyboard-shortcuts-inhibit" ||
strings.Contains(action, "dpms"):
return "System"
case action == "spawn":
return "Execute"
case strings.Contains(action, "workspace"):
return "Workspace"
case strings.HasPrefix(action, "focus-monitor") ||
strings.HasPrefix(action, "move-column-to-monitor") ||
strings.HasPrefix(action, "move-window-to-monitor"):
return "Monitor"
case strings.Contains(action, "window") ||
strings.Contains(action, "focus") ||
strings.Contains(action, "move") ||
strings.Contains(action, "swap") ||
strings.Contains(action, "resize") ||
strings.Contains(action, "column"):
return "Window"
default:
return "Other"
}
}
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, conflicts map[string]*NiriKeyBinding) keybinds.Keybind {
rawAction := n.formatRawAction(kb.Action, kb.Args)
keyStr := n.formatKey(kb)
source := "config"
if strings.Contains(kb.Source, "dms/binds.kdl") {
source = "dms"
}
bind := keybinds.Keybind{
Key: keyStr,
Description: kb.Description,
Action: rawAction,
Subcategory: subcategory,
Source: source,
HideOnOverlay: kb.HideOnOverlay,
CooldownMs: kb.CooldownMs,
}
if source == "dms" && conflicts != nil {
if conflictKb, ok := conflicts[keyStr]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Description,
Action: n.formatRawAction(conflictKb.Action, conflictKb.Args),
Source: "config",
}
}
}
return bind
}
func (n *NiriProvider) formatRawAction(action string, args []string) string {
if len(args) == 0 {
return action
}
if action == "spawn" && len(args) >= 3 && args[1] == "-c" {
switch args[0] {
case "sh", "bash":
cmd := strings.Join(args[2:], " ")
return fmt.Sprintf("spawn %s -c \"%s\"", args[0], strings.ReplaceAll(cmd, "\"", "\\\""))
}
}
return action + " " + strings.Join(args, " ")
}
func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (n *NiriProvider) GetOverridePath() string {
return filepath.Join(n.configDir, "dms", "binds.kdl")
}
func (n *NiriProvider) validateAction(action string) error {
action = strings.TrimSpace(action)
switch {
case action == "":
return fmt.Errorf("action cannot be empty")
case action == "spawn" || action == "spawn ":
return fmt.Errorf("spawn command requires arguments")
case strings.HasPrefix(action, "spawn "):
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn "))
switch rest {
case "":
return fmt.Errorf("spawn command requires arguments")
case "sh -c \"\"", "sh -c ''", "bash -c \"\"", "bash -c ''":
return fmt.Errorf("shell command cannot be empty")
}
}
return nil
}
func (n *NiriProvider) SetBind(key, action, description string, options map[string]any) error {
if err := n.validateAction(action); err != nil {
return err
}
overridePath := n.GetOverridePath()
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
return fmt.Errorf("failed to create dms directory: %w", err)
}
existingBinds, err := n.loadOverrideBinds()
if err != nil {
existingBinds = make(map[string]*overrideBind)
}
existingBinds[key] = &overrideBind{
Key: key,
Action: action,
Description: description,
Options: options,
}
return n.writeOverrideBinds(existingBinds)
}
func (n *NiriProvider) RemoveBind(key string) error {
existingBinds, err := n.loadOverrideBinds()
if err != nil {
return nil
}
delete(existingBinds, key)
return n.writeOverrideBinds(existingBinds)
}
type overrideBind struct {
Key string
Action string
Description string
Options map[string]any
}
func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
overridePath := n.GetOverridePath()
binds := make(map[string]*overrideBind)
data, err := os.ReadFile(overridePath)
if os.IsNotExist(err) {
return binds, nil
}
if err != nil {
return nil, err
}
parser := NewNiriParser(filepath.Dir(overridePath))
parser.currentSource = overridePath
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return nil, err
}
for _, node := range doc.Nodes {
if node.Name.String() != "binds" || node.Children == nil {
continue
}
for _, child := range node.Children {
kb := parser.parseKeybindNode(child, "")
if kb == nil {
continue
}
keyStr := parser.formatBindKey(kb)
binds[keyStr] = &overrideBind{
Key: keyStr,
Action: n.formatRawAction(kb.Action, kb.Args),
Description: kb.Description,
Options: n.extractOptions(child),
}
}
}
return binds, nil
}
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
if node.Properties == nil {
return make(map[string]any)
}
opts := make(map[string]any)
if val, ok := node.Properties.Get("repeat"); ok {
opts["repeat"] = val.String() == "true"
}
if val, ok := node.Properties.Get("cooldown-ms"); ok {
if ms, err := strconv.Atoi(val.String()); err == nil {
opts["cooldown-ms"] = ms
}
}
if val, ok := node.Properties.Get("allow-when-locked"); ok {
opts["allow-when-locked"] = val.String() == "true"
}
return opts
}
func (n *NiriProvider) isRecentWindowsAction(action string) bool {
switch action {
case "next-window", "previous-window":
return true
default:
return false
}
}
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
node := document.NewNode()
node.SetName(bind.Key)
if bind.Options != nil {
if v, ok := bind.Options["repeat"]; ok && v == false {
node.AddProperty("repeat", false, "")
}
if v, ok := bind.Options["cooldown-ms"]; ok {
switch val := v.(type) {
case int:
node.AddProperty("cooldown-ms", val, "")
case string:
if ms, err := strconv.Atoi(val); err == nil {
node.AddProperty("cooldown-ms", ms, "")
}
}
}
if v, ok := bind.Options["allow-when-locked"]; ok && v == true {
node.AddProperty("allow-when-locked", true, "")
}
}
if bind.Description != "" {
node.AddProperty("hotkey-overlay-title", bind.Description, "")
}
actionNode := n.buildActionNode(bind.Action)
node.AddNode(actionNode)
return node
}
func (n *NiriProvider) buildActionNode(action string) *document.Node {
action = strings.TrimSpace(action)
node := document.NewNode()
parts := n.parseActionParts(action)
if len(parts) == 0 {
node.SetName(action)
return node
}
node.SetName(parts[0])
for _, arg := range parts[1:] {
if strings.Contains(arg, "=") {
kv := strings.SplitN(arg, "=", 2)
switch kv[1] {
case "true":
node.AddProperty(kv[0], true, "")
case "false":
node.AddProperty(kv[0], false, "")
default:
node.AddProperty(kv[0], kv[1], "")
}
continue
}
node.AddArgument(arg, "")
}
return node
}
func (n *NiriProvider) parseActionParts(action string) []string {
var parts []string
var current strings.Builder
var inQuote, escaped, wasQuoted bool
for _, r := range action {
switch {
case escaped:
current.WriteRune(r)
escaped = false
case r == '\\':
escaped = true
case r == '"':
wasQuoted = true
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 || wasQuoted {
parts = append(parts, current.String())
current.Reset()
wasQuoted = false
}
default:
current.WriteRune(r)
}
}
if current.Len() > 0 || wasQuoted {
parts = append(parts, current.String())
}
return parts
}
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
overridePath := n.GetOverridePath()
content := n.generateBindsContent(binds)
if err := n.validateBindsContent(content); err != nil {
return err
}
return os.WriteFile(overridePath, []byte(content), 0644)
}
func (n *NiriProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
return 0
case strings.Contains(action, "workspace"):
return 1
case strings.Contains(action, "window") || strings.Contains(action, "column") ||
strings.Contains(action, "focus") || strings.Contains(action, "move") ||
strings.Contains(action, "swap") || strings.Contains(action, "resize"):
return 2
case strings.HasPrefix(action, "focus-monitor") || strings.Contains(action, "monitor"):
return 3
case strings.Contains(action, "screenshot"):
return 4
case action == "quit" || action == "power-off-monitors" || strings.Contains(action, "dpms"):
return 5
case strings.HasPrefix(action, "spawn"):
return 6
default:
return 7
}
}
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
if len(binds) == 0 {
return "binds {}\n"
}
var regularBinds, recentWindowsBinds []*overrideBind
for _, bind := range binds {
switch {
case n.isRecentWindowsAction(bind.Action):
recentWindowsBinds = append(recentWindowsBinds, bind)
default:
regularBinds = append(regularBinds, bind)
}
}
sort.Slice(regularBinds, func(i, j int) bool {
pi, pj := n.getBindSortPriority(regularBinds[i].Action), n.getBindSortPriority(regularBinds[j].Action)
if pi != pj {
return pi < pj
}
return regularBinds[i].Key < regularBinds[j].Key
})
sort.Slice(recentWindowsBinds, func(i, j int) bool {
return recentWindowsBinds[i].Key < recentWindowsBinds[j].Key
})
var sb strings.Builder
sb.WriteString("binds {\n")
for _, bind := range regularBinds {
n.writeBindNode(&sb, bind, " ")
}
sb.WriteString("}\n")
if len(recentWindowsBinds) > 0 {
sb.WriteString("\nrecent-windows {\n")
sb.WriteString(" binds {\n")
for _, bind := range recentWindowsBinds {
n.writeBindNode(&sb, bind, " ")
}
sb.WriteString(" }\n")
sb.WriteString("}\n")
}
return sb.String()
}
func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, indent string) {
node := n.buildBindNode(bind)
sb.WriteString(indent)
sb.WriteString(node.Name.String())
if node.Properties.Exist() {
sb.WriteString(" ")
sb.WriteString(strings.TrimLeft(node.Properties.String(), " "))
}
sb.WriteString(" { ")
if len(node.Children) > 0 {
child := node.Children[0]
actionName := child.Name.String()
sb.WriteString(actionName)
forceQuote := actionName == "spawn"
for _, arg := range child.Arguments {
sb.WriteString(" ")
n.writeArg(sb, arg.ValueString(), forceQuote)
}
if child.Properties.Exist() {
sb.WriteString(" ")
sb.WriteString(strings.TrimLeft(child.Properties.String(), " "))
}
}
sb.WriteString("; }\n")
}
func (n *NiriProvider) writeArg(sb *strings.Builder, val string, forceQuote bool) {
if !forceQuote && n.isNumericArg(val) {
sb.WriteString(val)
return
}
sb.WriteString("\"")
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
sb.WriteString("\"")
}
func (n *NiriProvider) isNumericArg(val string) bool {
if val == "" {
return false
}
start := 0
if val[0] == '-' || val[0] == '+' {
if len(val) == 1 {
return false
}
start = 1
}
for i := start; i < len(val); i++ {
if val[i] < '0' || val[i] > '9' {
return false
}
}
return true
}
func (n *NiriProvider) validateBindsContent(content string) error {
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(content); err != nil {
tmpFile.Close()
return fmt.Errorf("failed to write temp file: %w", err)
}
tmpFile.Close()
cmd := exec.Command("niri", "validate", "-c", tmpFile.Name())
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("invalid config: %s", strings.TrimSpace(string(output)))
}
return nil
}

View File

@@ -1,378 +0,0 @@
package providers
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/sblinch/kdl-go"
"github.com/sblinch/kdl-go/document"
)
type NiriKeyBinding struct {
Mods []string
Key string
Action string
Args []string
Description string
HideOnOverlay bool
CooldownMs int
Source string
}
type NiriSection struct {
Name string
Keybinds []NiriKeyBinding
Children []NiriSection
}
type NiriParser struct {
configDir string
processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding
bindOrder []string
currentSource string
dmsBindsIncluded bool
dmsBindsExists bool
includeCount int
dmsIncludePos int
bindsBeforeDMS int
bindsAfterDMS int
dmsBindKeys map[string]bool
configBindKeys map[string]bool
dmsProcessed bool
dmsBindMap map[string]*NiriKeyBinding
conflictingConfigs map[string]*NiriKeyBinding
}
func NewNiriParser(configDir string) *NiriParser {
return &NiriParser{
configDir: configDir,
processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{},
currentSource: "",
dmsIncludePos: -1,
dmsBindKeys: make(map[string]bool),
configBindKeys: make(map[string]bool),
dmsBindMap: make(map[string]*NiriKeyBinding),
conflictingConfigs: make(map[string]*NiriKeyBinding),
}
}
func (p *NiriParser) Parse() (*NiriSection, error) {
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
if _, err := os.Stat(dmsBindsPath); err == nil {
p.dmsBindsExists = true
}
configPath := filepath.Join(p.configDir, "config.kdl")
section, err := p.parseFile(configPath, "")
if err != nil {
return nil, err
}
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath, section)
}
section.Keybinds = p.finalizeBinds()
return section, nil
}
func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSection) {
data, err := os.ReadFile(dmsBindsPath)
if err != nil {
return
}
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
baseDir := filepath.Dir(dmsBindsPath)
p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
p.dmsProcessed = true
}
func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
for _, key := range p.bindOrder {
if kb, ok := p.bindMap[key]; ok {
binds = append(binds, *kb)
}
}
return binds
}
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
key := p.formatBindKey(kb)
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
if isDMSBind {
p.dmsBindKeys[key] = true
p.dmsBindMap[key] = kb
} else if p.dmsBindKeys[key] {
p.bindsAfterDMS++
p.conflictingConfigs[key] = kb
p.configBindKeys[key] = true
return
} else {
p.configBindKeys[key] = true
}
if _, exists := p.bindMap[key]; !exists {
p.bindOrder = append(p.bindOrder, key)
}
p.bindMap[key] = kb
}
func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, error) {
absPath, err := filepath.Abs(filePath)
if err != nil {
return nil, fmt.Errorf("failed to resolve path %s: %w", filePath, err)
}
if p.processedFiles[absPath] {
return &NiriSection{Name: sectionName}, nil
}
p.processedFiles[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
}
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
}
section := &NiriSection{
Name: sectionName,
}
prevSource := p.currentSource
p.currentSource = absPath
baseDir := filepath.Dir(absPath)
p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
return section, nil
}
func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection, baseDir string) {
for _, node := range nodes {
name := node.Name.String()
switch name {
case "include":
p.handleInclude(node, section, baseDir)
case "binds":
p.extractBinds(node, section, "")
case "recent-windows":
p.handleRecentWindows(node, section)
}
}
}
func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) {
if len(node.Arguments) == 0 {
return
}
includePath := strings.Trim(node.Arguments[0].String(), "\"")
isDMSInclude := includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl")
p.includeCount++
if isDMSInclude {
p.dmsBindsIncluded = true
p.dmsIncludePos = p.includeCount
p.bindsBeforeDMS = len(p.bindMap)
}
fullPath := filepath.Join(baseDir, includePath)
if filepath.IsAbs(includePath) {
fullPath = includePath
}
if isDMSInclude {
p.dmsProcessed = true
}
includedSection, err := p.parseFile(fullPath, "")
if err != nil {
return
}
section.Children = append(section.Children, includedSection.Children...)
}
func (p *NiriParser) HasDMSBindsIncluded() bool {
return p.dmsBindsIncluded
}
func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) {
if node.Children == nil {
return
}
for _, child := range node.Children {
if child.Name.String() != "binds" {
continue
}
p.extractBinds(child, section, "Alt-Tab")
}
}
func (p *NiriParser) extractBinds(node *document.Node, section *NiriSection, subcategory string) {
if node.Children == nil {
return
}
for _, child := range node.Children {
kb := p.parseKeybindNode(child, subcategory)
if kb == nil {
continue
}
p.addBind(kb)
}
}
func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBinding {
keyCombo := node.Name.String()
if keyCombo == "" {
return nil
}
mods, key := p.parseKeyCombo(keyCombo)
var action string
var args []string
if len(node.Children) > 0 {
actionNode := node.Children[0]
action = actionNode.Name.String()
for _, arg := range actionNode.Arguments {
args = append(args, arg.ValueString())
}
if actionNode.Properties != nil {
if val, ok := actionNode.Properties.Get("focus"); ok {
args = append(args, "focus="+val.String())
}
}
}
var description string
var hideOnOverlay bool
var cooldownMs int
if node.Properties != nil {
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
switch val.ValueString() {
case "null", "":
hideOnOverlay = true
default:
description = val.ValueString()
}
}
if val, ok := node.Properties.Get("cooldown-ms"); ok {
cooldownMs, _ = strconv.Atoi(val.String())
}
}
return &NiriKeyBinding{
Mods: mods,
Key: key,
Action: action,
Args: args,
Description: description,
HideOnOverlay: hideOnOverlay,
CooldownMs: cooldownMs,
Source: p.currentSource,
}
}
func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
parts := strings.Split(combo, "+")
switch len(parts) {
case 0:
return nil, combo
case 1:
return nil, parts[0]
default:
return parts[:len(parts)-1], parts[len(parts)-1]
}
}
type NiriParseResult struct {
Section *NiriSection
DMSBindsIncluded bool
DMSStatus *DMSBindsStatusInfo
ConflictingConfigs map[string]*NiriKeyBinding
}
type DMSBindsStatusInfo struct {
Exists bool
Included bool
IncludePosition int
TotalIncludes int
BindsAfterDMS int
Effective bool
OverriddenBy int
StatusMessage string
}
func (p *NiriParser) buildDMSStatus() *DMSBindsStatusInfo {
status := &DMSBindsStatusInfo{
Exists: p.dmsBindsExists,
Included: p.dmsBindsIncluded,
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS,
}
switch {
case !p.dmsBindsExists:
status.Effective = false
status.StatusMessage = "dms/binds.kdl does not exist"
case !p.dmsBindsIncluded:
status.Effective = false
status.StatusMessage = "dms/binds.kdl is not included in config.kdl"
case p.bindsAfterDMS > 0:
status.Effective = true
status.OverriddenBy = p.bindsAfterDMS
status.StatusMessage = "Some DMS binds may be overridden by config binds"
default:
status.Effective = true
status.StatusMessage = "DMS binds are active"
}
return status
}
func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
parser := NewNiriParser(configDir)
section, err := parser.Parse()
if err != nil {
return nil, err
}
return &NiriParseResult{
Section: section,
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
}, nil
}

View File

@@ -1,630 +0,0 @@
package providers
import (
"os"
"path/filepath"
"testing"
)
func TestNiriParseKeyCombo(t *testing.T) {
tests := []struct {
combo string
expectedMods []string
expectedKey string
}{
{"Mod+Q", []string{"Mod"}, "Q"},
{"Mod+Shift+F", []string{"Mod", "Shift"}, "F"},
{"Ctrl+Alt+Delete", []string{"Ctrl", "Alt"}, "Delete"},
{"Print", nil, "Print"},
{"XF86AudioMute", nil, "XF86AudioMute"},
{"Super+Tab", []string{"Super"}, "Tab"},
{"Mod+Shift+Ctrl+H", []string{"Mod", "Shift", "Ctrl"}, "H"},
}
parser := NewNiriParser("")
for _, tt := range tests {
t.Run(tt.combo, func(t *testing.T) {
mods, key := parser.parseKeyCombo(tt.combo)
if len(mods) != len(tt.expectedMods) {
t.Errorf("Mods length = %d, want %d", len(mods), len(tt.expectedMods))
} else {
for i := range mods {
if mods[i] != tt.expectedMods[i] {
t.Errorf("Mods[%d] = %q, want %q", i, mods[i], tt.expectedMods[i])
}
}
}
if key != tt.expectedKey {
t.Errorf("Key = %q, want %q", key, tt.expectedKey)
}
})
}
}
func TestNiriParseBasicBinds(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Q { close-window; }
Mod+F { fullscreen-window; }
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
foundClose := false
foundFullscreen := false
foundTerminal := false
for _, kb := range result.Section.Keybinds {
switch kb.Action {
case "close-window":
foundClose = true
if kb.Key != "Q" || len(kb.Mods) != 1 || kb.Mods[0] != "Mod" {
t.Errorf("close-window keybind mismatch: %+v", kb)
}
case "fullscreen-window":
foundFullscreen = true
case "spawn":
foundTerminal = true
if kb.Description != "Open Terminal" {
t.Errorf("spawn description = %q, want %q", kb.Description, "Open Terminal")
}
if len(kb.Args) != 1 || kb.Args[0] != "kitty" {
t.Errorf("spawn args = %v, want [kitty]", kb.Args)
}
}
}
if !foundClose {
t.Error("close-window keybind not found")
}
if !foundFullscreen {
t.Error("fullscreen-window keybind not found")
}
if !foundTerminal {
t.Error("spawn keybind not found")
}
}
func TestNiriParseRecentWindows(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
}
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(result.Section.Keybinds))
}
foundNext := false
foundPrev := false
for _, kb := range result.Section.Keybinds {
switch kb.Action {
case "next-window":
foundNext = true
case "previous-window":
foundPrev = true
}
}
if !foundNext {
t.Error("next-window keybind not found")
}
if !foundPrev {
t.Error("previous-window keybind not found")
}
}
func TestNiriParseInclude(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdir: %v", err)
}
mainConfig := filepath.Join(tmpDir, "config.kdl")
includeConfig := filepath.Join(subDir, "binds.kdl")
mainContent := `binds {
Mod+Q { close-window; }
}
include "dms/binds.kdl"
`
includeContent := `binds {
Mod+T hotkey-overlay-title="Terminal" { spawn "kitty"; }
}
`
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
t.Fatalf("Failed to write main config: %v", err)
}
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
t.Fatalf("Failed to write include config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(result.Section.Keybinds))
}
}
func TestNiriParseIncludeOverride(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdir: %v", err)
}
mainConfig := filepath.Join(tmpDir, "config.kdl")
includeConfig := filepath.Join(subDir, "binds.kdl")
mainContent := `binds {
Mod+T hotkey-overlay-title="Main Terminal" { spawn "alacritty"; }
}
include "dms/binds.kdl"
`
includeContent := `binds {
Mod+T hotkey-overlay-title="Override Terminal" { spawn "kitty"; }
}
`
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
t.Fatalf("Failed to write main config: %v", err)
}
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
t.Fatalf("Failed to write include config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 1 {
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(result.Section.Keybinds))
}
if len(result.Section.Keybinds) > 0 {
kb := result.Section.Keybinds[0]
if kb.Description != "Override Terminal" {
t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description)
}
if len(kb.Args) != 1 || kb.Args[0] != "kitty" {
t.Errorf("Expected args [kitty] (from include), got %v", kb.Args)
}
}
}
func TestNiriParseCircularInclude(t *testing.T) {
tmpDir := t.TempDir()
mainConfig := filepath.Join(tmpDir, "config.kdl")
otherConfig := filepath.Join(tmpDir, "other.kdl")
mainContent := `binds {
Mod+Q { close-window; }
}
include "other.kdl"
`
otherContent := `binds {
Mod+T { spawn "kitty"; }
}
include "config.kdl"
`
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
t.Fatalf("Failed to write main config: %v", err)
}
if err := os.WriteFile(otherConfig, []byte(otherContent), 0644); err != nil {
t.Fatalf("Failed to write other config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err)
}
if len(result.Section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(result.Section.Keybinds))
}
}
func TestNiriParseMissingInclude(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Q { close-window; }
}
include "nonexistent/file.kdl"
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err)
}
if len(result.Section.Keybinds) != 1 {
t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(result.Section.Keybinds))
}
}
func TestNiriParseNoBinds(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `cursor {
xcursor-theme "Bibata"
xcursor-size 24
}
input {
keyboard {
numlock
}
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 0 {
t.Errorf("Expected 0 keybinds, got %d", len(result.Section.Keybinds))
}
}
func TestNiriParseErrors(t *testing.T) {
tests := []struct {
name string
path string
}{
{
name: "nonexistent_directory",
path: "/nonexistent/path/that/does/not/exist",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseNiriKeys(tt.path)
if err == nil {
t.Error("Expected error, got nil")
}
})
}
}
func TestNiriBindOverrideBehavior(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+T hotkey-overlay-title="First" { spawn "first"; }
Mod+Q { close-window; }
Mod+T hotkey-overlay-title="Second" { spawn "second"; }
Mod+F { fullscreen-window; }
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Fatalf("Expected 3 unique keybinds, got %d", len(result.Section.Keybinds))
}
var modT *NiriKeyBinding
for i := range result.Section.Keybinds {
kb := &result.Section.Keybinds[i]
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" {
modT = kb
break
}
}
if modT == nil {
t.Fatal("Mod+T keybind not found")
}
if modT.Description != "Third" {
t.Errorf("Mod+T description = %q, want 'Third' (last definition wins)", modT.Description)
}
if len(modT.Args) != 1 || modT.Args[0] != "third" {
t.Errorf("Mod+T args = %v, want [third] (last definition wins)", modT.Args)
}
}
func TestNiriBindOverrideWithIncludes(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "custom")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdir: %v", err)
}
mainConfig := filepath.Join(tmpDir, "config.kdl")
includeConfig := filepath.Join(subDir, "overrides.kdl")
mainContent := `binds {
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+T hotkey-overlay-title="Default Terminal" { spawn "xterm"; }
}
include "custom/overrides.kdl"
binds {
Mod+3 { focus-workspace 3; }
}
`
includeContent := `binds {
Mod+T hotkey-overlay-title="Custom Terminal" { spawn "kitty"; }
Mod+2 { focus-workspace 22; }
}
`
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
t.Fatalf("Failed to write main config: %v", err)
}
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
t.Fatalf("Failed to write include config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 unique keybinds, got %d", len(result.Section.Keybinds))
}
bindMap := make(map[string]*NiriKeyBinding)
for i := range result.Section.Keybinds {
kb := &result.Section.Keybinds[i]
key := ""
for _, m := range kb.Mods {
key += m + "+"
}
key += kb.Key
bindMap[key] = kb
}
if kb, ok := bindMap["Mod+T"]; ok {
if kb.Description != "Custom Terminal" {
t.Errorf("Mod+T should be overridden by include, got description %q", kb.Description)
}
} else {
t.Error("Mod+T not found")
}
if kb, ok := bindMap["Mod+2"]; ok {
if len(kb.Args) != 1 || kb.Args[0] != "22" {
t.Errorf("Mod+2 should be overridden by include with workspace 22, got args %v", kb.Args)
}
} else {
t.Error("Mod+2 not found")
}
if _, ok := bindMap["Mod+1"]; !ok {
t.Error("Mod+1 should exist (not overridden)")
}
if _, ok := bindMap["Mod+3"]; !ok {
t.Error("Mod+3 should exist (added after include)")
}
}
func TestNiriParseMultipleArgs(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 1 {
t.Fatalf("Expected 1 keybind, got %d", len(result.Section.Keybinds))
}
kb := result.Section.Keybinds[0]
if len(kb.Args) != 5 {
t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args)
}
expectedArgs := []string{"dms", "ipc", "call", "spotlight", "toggle"}
for i, arg := range expectedArgs {
if i < len(kb.Args) && kb.Args[i] != arg {
t.Errorf("Args[%d] = %q, want %q", i, kb.Args[i], arg)
}
}
}
func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
Mod+2 hotkey-overlay-title="Focus Workspace 2" { focus-workspace 2; }
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
switch kb.Key {
case "1":
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" {
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "1" {
t.Errorf("Mod+1 action/args mismatch: %+v", kb)
}
if kb.Description != "Focus Workspace 1" {
t.Errorf("Mod+1 description = %q, want 'Focus Workspace 1'", kb.Description)
}
}
case "0":
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "10" {
t.Errorf("Mod+0 action/args mismatch: %+v", kb)
}
}
}
}
func TestNiriParseQuotedStringArgs(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
if kb.Action == "set-column-width" {
if len(kb.Args) != 1 {
t.Errorf("set-column-width should have 1 arg, got %d", len(kb.Args))
continue
}
if kb.Args[0] != "-10%" && kb.Args[0] != "+10%" {
t.Errorf("set-column-width arg = %q, want -10%% or +10%%", kb.Args[0])
}
}
}
}
func TestNiriParseActionWithProperties(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1 focus=false; }
Mod+Shift+2 hotkey-overlay-title="Move to Workspace 2" { move-column-to-workspace 2 focus=false; }
Alt+Tab { next-window scope="output"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
switch kb.Action {
case "move-column-to-workspace":
if len(kb.Args) != 2 {
t.Errorf("move-column-to-workspace should have 2 args (index + focus), got %d", len(kb.Args))
}
hasIndex := false
hasFocus := false
for _, arg := range kb.Args {
if arg == "1" || arg == "2" {
hasIndex = true
}
if arg == "focus=false" {
hasFocus = true
}
}
if !hasIndex {
t.Errorf("move-column-to-workspace missing index arg")
}
if !hasFocus {
t.Errorf("move-column-to-workspace missing focus=false arg")
}
case "next-window":
if kb.Key != "Tab" {
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
}
}
}
}

View File

@@ -1,607 +0,0 @@
package providers
import (
"os"
"path/filepath"
"testing"
)
func TestNiriProviderName(t *testing.T) {
provider := NewNiriProvider("")
if provider.Name() != "niri" {
t.Errorf("Name() = %q, want %q", provider.Name(), "niri")
}
}
func TestNiriProviderGetCheatSheet(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Q { close-window; }
Mod+F { fullscreen-window; }
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
Mod+1 { focus-workspace 1; }
Mod+Shift+1 { move-column-to-workspace 1; }
Print { screenshot; }
Mod+Shift+E { quit; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
provider := NewNiriProvider(tmpDir)
cheatSheet, err := provider.GetCheatSheet()
if err != nil {
t.Fatalf("GetCheatSheet failed: %v", err)
}
if cheatSheet.Title != "Niri Keybinds" {
t.Errorf("Title = %q, want %q", cheatSheet.Title, "Niri Keybinds")
}
if cheatSheet.Provider != "niri" {
t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri")
}
windowBinds := cheatSheet.Binds["Window"]
if len(windowBinds) < 2 {
t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds))
}
execBinds := cheatSheet.Binds["Execute"]
if len(execBinds) < 1 {
t.Errorf("Expected at least 1 Execute bind, got %d", len(execBinds))
}
workspaceBinds := cheatSheet.Binds["Workspace"]
if len(workspaceBinds) < 2 {
t.Errorf("Expected at least 2 Workspace binds, got %d", len(workspaceBinds))
}
screenshotBinds := cheatSheet.Binds["Screenshot"]
if len(screenshotBinds) < 1 {
t.Errorf("Expected at least 1 Screenshot bind, got %d", len(screenshotBinds))
}
systemBinds := cheatSheet.Binds["System"]
if len(systemBinds) < 1 {
t.Errorf("Expected at least 1 System bind, got %d", len(systemBinds))
}
}
func TestNiriCategorizeByAction(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
action string
expected string
}{
{"focus-workspace", "Workspace"},
{"focus-workspace-up", "Workspace"},
{"move-column-to-workspace", "Workspace"},
{"focus-monitor-left", "Monitor"},
{"move-column-to-monitor-right", "Monitor"},
{"close-window", "Window"},
{"fullscreen-window", "Window"},
{"maximize-column", "Window"},
{"toggle-window-floating", "Window"},
{"focus-column-left", "Window"},
{"move-column-right", "Window"},
{"spawn", "Execute"},
{"quit", "System"},
{"power-off-monitors", "System"},
{"screenshot", "Screenshot"},
{"screenshot-window", "Screenshot"},
{"toggle-overview", "Overview"},
{"show-hotkey-overlay", "Overview"},
{"next-window", "Alt-Tab"},
{"previous-window", "Alt-Tab"},
{"unknown-action", "Other"},
}
for _, tt := range tests {
t.Run(tt.action, func(t *testing.T) {
result := provider.categorizeByAction(tt.action)
if result != tt.expected {
t.Errorf("categorizeByAction(%q) = %q, want %q", tt.action, result, tt.expected)
}
})
}
}
func TestNiriFormatRawAction(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
action string
args []string
expected string
}{
{"spawn", []string{"kitty"}, "spawn kitty"},
{"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"},
{"close-window", nil, "close-window"},
{"fullscreen-window", nil, "fullscreen-window"},
{"focus-workspace", []string{"1"}, "focus-workspace 1"},
{"move-column-to-workspace", []string{"5"}, "move-column-to-workspace 5"},
{"set-column-width", []string{"+10%"}, "set-column-width +10%"},
}
for _, tt := range tests {
t.Run(tt.action, func(t *testing.T) {
result := provider.formatRawAction(tt.action, tt.args)
if result != tt.expected {
t.Errorf("formatRawAction(%q, %v) = %q, want %q", tt.action, tt.args, result, tt.expected)
}
})
}
}
func TestNiriFormatKey(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
mods []string
key string
expected string
}{
{[]string{"Mod"}, "Q", "Mod+Q"},
{[]string{"Mod", "Shift"}, "F", "Mod+Shift+F"},
{[]string{"Ctrl", "Alt"}, "Delete", "Ctrl+Alt+Delete"},
{nil, "Print", "Print"},
{[]string{}, "XF86AudioMute", "XF86AudioMute"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
kb := &NiriKeyBinding{
Mods: tt.mods,
Key: tt.key,
}
result := provider.formatKey(kb)
if result != tt.expected {
t.Errorf("formatKey(%v) = %q, want %q", kb, result, tt.expected)
}
})
}
}
func TestNiriDefaultConfigDir(t *testing.T) {
originalXDG := os.Getenv("XDG_CONFIG_HOME")
defer os.Setenv("XDG_CONFIG_HOME", originalXDG)
os.Setenv("XDG_CONFIG_HOME", "/custom/config")
dir := defaultNiriConfigDir()
if dir != "/custom/config/niri" {
t.Errorf("With XDG_CONFIG_HOME set, got %q, want %q", dir, "/custom/config/niri")
}
os.Unsetenv("XDG_CONFIG_HOME")
dir = defaultNiriConfigDir()
home, _ := os.UserHomeDir()
expected := filepath.Join(home, ".config", "niri")
if dir != expected {
t.Errorf("Without XDG_CONFIG_HOME, got %q, want %q", dir, expected)
}
}
func TestNiriGenerateBindsContent(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
name string
binds map[string]*overrideBind
expected string
}{
{
name: "empty binds",
binds: map[string]*overrideBind{},
expected: "binds {}\n",
},
{
name: "simple spawn bind",
binds: map[string]*overrideBind{
"Mod+T": {
Key: "Mod+T",
Action: "spawn kitty",
Description: "Open Terminal",
},
},
expected: `binds {
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
}
`,
},
{
name: "spawn with multiple args",
binds: map[string]*overrideBind{
"Mod+Space": {
Key: "Mod+Space",
Action: `spawn "dms" "ipc" "call" "spotlight" "toggle"`,
Description: "Application Launcher",
},
},
expected: `binds {
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
}
`,
},
{
name: "bind with allow-when-locked",
binds: map[string]*overrideBind{
"XF86AudioMute": {
Key: "XF86AudioMute",
Action: `spawn "dms" "ipc" "call" "audio" "mute"`,
Options: map[string]any{"allow-when-locked": true},
},
},
expected: `binds {
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
}
`,
},
{
name: "simple action without args",
binds: map[string]*overrideBind{
"Mod+Q": {
Key: "Mod+Q",
Action: "close-window",
Description: "Close Window",
},
},
expected: `binds {
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
}
`,
},
{
name: "recent-windows action",
binds: map[string]*overrideBind{
"Alt+Tab": {
Key: "Alt+Tab",
Action: "next-window",
},
},
expected: `binds {
}
recent-windows {
binds {
Alt+Tab { next-window; }
}
}
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.generateBindsContent(tt.binds)
if result != tt.expected {
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
}
})
}
}
func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Mod+Space": {
Key: "Mod+Space",
Action: `spawn "dms" "ipc" "call" "spotlight" "toggle"`,
Description: "Application Launcher",
},
"XF86AudioMute": {
Key: "XF86AudioMute",
Action: `spawn "dms" "ipc" "call" "audio" "mute"`,
Options: map[string]any{"allow-when-locked": true},
},
"Mod+Q": {
Key: "Mod+Q",
Action: "close-window",
Description: "Close Window",
},
}
content := provider.generateBindsContent(binds)
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds after round-trip, got %d", len(result.Section.Keybinds))
}
}
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Shift+Ctrl+D { debug-toggle-damage; }
Super+D { spawn "niri" "msg" "action" "toggle-overview"; }
Super+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
Mod+Shift+E { quit; }
}
recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
}
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
provider := NewNiriProvider(tmpDir)
cheatSheet, err := provider.GetCheatSheet()
if err != nil {
t.Fatalf("GetCheatSheet failed: %v", err)
}
totalBinds := 0
for _, binds := range cheatSheet.Binds {
totalBinds += len(binds)
}
if totalBinds < 20 {
t.Errorf("Expected at least 20 keybinds, got %d", totalBinds)
}
if len(cheatSheet.Binds["Alt-Tab"]) < 2 {
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
}
}
func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
name string
binds map[string]*overrideBind
expected string
}{
{
name: "workspace with numeric arg",
binds: map[string]*overrideBind{
"Mod+1": {
Key: "Mod+1",
Action: "focus-workspace 1",
Description: "Focus Workspace 1",
},
},
expected: `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
}
`,
},
{
name: "workspace with large numeric arg",
binds: map[string]*overrideBind{
"Mod+0": {
Key: "Mod+0",
Action: "focus-workspace 10",
Description: "Focus Workspace 10",
},
},
expected: `binds {
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
}
`,
},
{
name: "percentage string arg (should be quoted)",
binds: map[string]*overrideBind{
"Super+Minus": {
Key: "Super+Minus",
Action: `set-column-width "-10%"`,
Description: "Adjust Column Width -10%",
},
},
expected: `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
}
`,
},
{
name: "positive percentage string arg",
binds: map[string]*overrideBind{
"Super+Equal": {
Key: "Super+Equal",
Action: `set-column-width "+10%"`,
Description: "Adjust Column Width +10%",
},
},
expected: `binds {
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
}
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.generateBindsContent(tt.binds)
if result != tt.expected {
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
}
})
}
}
func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Super+Equal": {
Key: "Super+Equal",
Action: "set-window-height +10%",
Description: "Adjust Window Height +10%",
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86AudioLowerVolume": {
Key: "XF86AudioLowerVolume",
Action: `spawn "dms" "ipc" "call" "audio" "decrement" "3"`,
Options: map[string]any{"allow-when-locked": true},
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86AudioLowerVolume": {
Key: "XF86AudioLowerVolume",
Action: "spawn dms ipc call audio decrement 3",
Options: map[string]any{"allow-when-locked": true},
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Mod+1": {
Key: "Mod+1",
Action: "focus-workspace 1",
Description: "Focus Workspace 1",
},
"Mod+2": {
Key: "Mod+2",
Action: "focus-workspace 2",
Description: "Focus Workspace 2",
},
"Mod+Shift+1": {
Key: "Mod+Shift+1",
Action: "move-column-to-workspace 1",
Description: "Move to Workspace 1",
},
"Super+Minus": {
Key: "Super+Minus",
Action: "set-column-width -10%",
Description: "Adjust Column Width -10%",
},
}
content := provider.generateBindsContent(binds)
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 keybinds after round-trip, got %d", len(result.Section.Keybinds))
}
foundFocusWS1 := false
foundMoveWS1 := false
foundSetWidth := false
for _, kb := range result.Section.Keybinds {
switch {
case kb.Action == "focus-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
foundFocusWS1 = true
case kb.Action == "move-column-to-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
foundMoveWS1 = true
case kb.Action == "set-column-width" && len(kb.Args) > 0 && kb.Args[0] == "-10%":
foundSetWidth = true
}
}
if !foundFocusWS1 {
t.Error("focus-workspace 1 not found after round-trip")
}
if !foundMoveWS1 {
t.Error("move-column-to-workspace 1 not found after round-trip")
}
if !foundSetWidth {
t.Error("set-column-width -10% not found after round-trip")
}
}

View File

@@ -2,7 +2,6 @@ package providers
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -10,42 +9,18 @@ import (
type SwayProvider struct { type SwayProvider struct {
configPath string configPath string
isScroll bool
} }
func NewSwayProvider(configPath string) *SwayProvider { func NewSwayProvider(configPath string) *SwayProvider {
isScroll := false
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
if configPath == "" { if configPath == "" {
if scrollEnvSet { configPath = "$HOME/.config/sway"
configPath = "$HOME/.config/scroll"
isScroll = true
} else {
configPath = "$HOME/.config/sway"
}
} else {
// Determine isScroll based on the provided config path
isScroll = strings.Contains(configPath, "scroll")
} }
return &SwayProvider{ return &SwayProvider{
configPath: configPath, configPath: configPath,
isScroll: isScroll,
} }
} }
func (s *SwayProvider) Name() string { func (s *SwayProvider) Name() string {
if s != nil && s.isScroll {
return "scroll"
}
if s == nil {
_, ok := os.LookupEnv("SCROLLSOCK")
if ok {
return "scroll"
}
}
return "sway" return "sway"
} }
@@ -58,13 +33,8 @@ func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
categorizedBinds := make(map[string][]keybinds.Keybind) categorizedBinds := make(map[string][]keybinds.Keybind)
s.convertSection(section, "", categorizedBinds) s.convertSection(section, "", categorizedBinds)
cheatSheetTitle := "Sway Keybinds"
if s != nil && s.isScroll {
cheatSheetTitle = "Scroll Keybinds"
}
return &keybinds.CheatSheet{ return &keybinds.CheatSheet{
Title: cheatSheetTitle, Title: "Sway Keybinds",
Provider: s.Name(), Provider: s.Name(),
Binds: categorizedBinds, Binds: categorizedBinds,
}, nil }, nil
@@ -129,7 +99,6 @@ func (s *SwayProvider) convertKeybind(kb *SwayKeyBinding, subcategory string) ke
return keybinds.Keybind{ return keybinds.Keybind{
Key: key, Key: key,
Description: desc, Description: desc,
Action: kb.Command,
Subcategory: subcategory, Subcategory: subcategory,
} }
} }

View File

@@ -5,8 +5,6 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
const ( const (
@@ -44,9 +42,14 @@ func NewSwayParser() *SwayParser {
} }
func (p *SwayParser) ReadContent(path string) error { func (p *SwayParser) ReadContent(path string) error {
expandedPath, err := utils.ExpandPath(path) expandedPath := os.ExpandEnv(path)
if err != nil { expandedPath = filepath.Clean(expandedPath)
return err if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return err
}
expandedPath = filepath.Join(home, expandedPath[1:])
} }
info, err := os.Stat(expandedPath) info, err := os.Stat(expandedPath)

View File

@@ -1,43 +1,18 @@
package keybinds package keybinds
type Keybind struct { type Keybind struct {
Key string `json:"key"` Key string `json:"key"`
Description string `json:"desc"` Description string `json:"desc"`
Action string `json:"action,omitempty"` Subcategory string `json:"subcat,omitempty"`
Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"`
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
CooldownMs int `json:"cooldownMs,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
}
type DMSBindsStatus struct {
Exists bool `json:"exists"`
Included bool `json:"included"`
IncludePosition int `json:"includePosition"`
TotalIncludes int `json:"totalIncludes"`
BindsAfterDMS int `json:"bindsAfterDms"`
Effective bool `json:"effective"`
OverriddenBy int `json:"overriddenBy"`
StatusMessage string `json:"statusMessage"`
} }
type CheatSheet struct { type CheatSheet struct {
Title string `json:"title"` Title string `json:"title"`
Provider string `json:"provider"` Provider string `json:"provider"`
Binds map[string][]Keybind `json:"binds"` Binds map[string][]Keybind `json:"binds"`
DMSBindsIncluded bool `json:"dmsBindsIncluded"`
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
} }
type Provider interface { type Provider interface {
Name() string Name() string
GetCheatSheet() (*CheatSheet, error) GetCheatSheet() (*CheatSheet, error)
} }
type WritableProvider interface {
Provider
SetBind(key, action, description string, options map[string]any) error
RemoveBind(key string) error
GetOverridePath() string
}

View File

@@ -64,7 +64,7 @@ func (l *FileLogger) writeToFile(message string) {
redacted := l.redactPassword(message) redacted := l.redactPassword(message)
timestamp := time.Now().Format("15:04:05.000") timestamp := time.Now().Format("15:04:05.000")
fmt.Fprintf(l.writer, "[%s] %s\n", timestamp, redacted) l.writer.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, redacted))
l.writer.Flush() l.writer.Flush()
} }
@@ -93,7 +93,7 @@ func (l *FileLogger) Close() error {
defer l.mu.Unlock() defer l.mu.Unlock()
footer := fmt.Sprintf("\n=== DankInstall Log End ===\nCompleted: %s\n", time.Now().Format(time.RFC3339)) footer := fmt.Sprintf("\n=== DankInstall Log End ===\nCompleted: %s\n", time.Now().Format(time.RFC3339))
l.writer.WriteString(footer) //nolint:errcheck l.writer.WriteString(footer)
l.writer.Flush() l.writer.Flush()
if err := l.file.Sync(); err != nil { if err := l.file.Sync(); err != nil {

View File

@@ -13,10 +13,10 @@ import (
type Logger struct{ *cblog.Logger } type Logger struct{ *cblog.Logger }
// Printf routes goose/info-style logs through Infof. // Printf routes goose/info-style logs through Infof.
func (l *Logger) Printf(format string, v ...any) { l.Infof(format, v...) } func (l *Logger) Printf(format string, v ...interface{}) { l.Infof(format, v...) }
// Fatalf keeps gooses contract of exiting the program. // Fatalf keeps gooses contract of exiting the program.
func (l *Logger) Fatalf(format string, v ...any) { l.Logger.Fatalf(format, v...) } func (l *Logger) Fatalf(format string, v ...interface{}) { l.Logger.Fatalf(format, v...) }
var ( var (
logger *Logger logger *Logger
@@ -104,13 +104,13 @@ func GetLogger() *Logger {
// * Convenience wrappers // * Convenience wrappers
func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) } func Debug(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Debug(msg, keyvals...) }
func Debugf(format string, v ...any) { GetLogger().Debugf(format, v...) } func Debugf(format string, v ...interface{}) { GetLogger().Logger.Debugf(format, v...) }
func Info(msg any, keyvals ...any) { GetLogger().Info(msg, keyvals...) } func Info(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Info(msg, keyvals...) }
func Infof(format string, v ...any) { GetLogger().Infof(format, v...) } func Infof(format string, v ...interface{}) { GetLogger().Logger.Infof(format, v...) }
func Warn(msg any, keyvals ...any) { GetLogger().Warn(msg, keyvals...) } func Warn(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Warn(msg, keyvals...) }
func Warnf(format string, v ...any) { GetLogger().Warnf(format, v...) } func Warnf(format string, v ...interface{}) { GetLogger().Logger.Warnf(format, v...) }
func Error(msg any, keyvals ...any) { GetLogger().Error(msg, keyvals...) } func Error(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Error(msg, keyvals...) }
func Errorf(format string, v ...any) { GetLogger().Errorf(format, v...) } func Errorf(format string, v ...interface{}) { GetLogger().Logger.Errorf(format, v...) }
func Fatal(msg any, keyvals ...any) { GetLogger().Fatal(msg, keyvals...) } func Fatal(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Fatal(msg, keyvals...) }
func Fatalf(format string, v ...any) { GetLogger().Fatalf(format, v...) } func Fatalf(format string, v ...interface{}) { GetLogger().Logger.Fatalf(format, v...) }

View File

@@ -1,583 +0,0 @@
package matugen
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
var (
matugenVersionOnce sync.Once
matugenSupportsCOE bool
)
type Options struct {
StateDir string
ShellDir string
ConfigDir string
Kind string
Value string
Mode string
IconTheme string
MatugenType string
RunUserTemplates bool
StockColors string
SyncModeWithPortal bool
TerminalsAlwaysDark bool
}
type ColorsOutput struct {
Colors struct {
Dark map[string]string `json:"dark"`
Light map[string]string `json:"light"`
} `json:"colors"`
}
func (o *Options) ColorsOutput() string {
return filepath.Join(o.StateDir, "dms-colors.json")
}
func Run(opts Options) error {
if opts.StateDir == "" {
return fmt.Errorf("state-dir is required")
}
if opts.ShellDir == "" {
return fmt.Errorf("shell-dir is required")
}
if opts.ConfigDir == "" {
return fmt.Errorf("config-dir is required")
}
if opts.Kind == "" {
return fmt.Errorf("kind is required")
}
if opts.Value == "" {
return fmt.Errorf("value is required")
}
if opts.Mode == "" {
opts.Mode = "dark"
}
if opts.MatugenType == "" {
opts.MatugenType = "scheme-tonal-spot"
}
if opts.IconTheme == "" {
opts.IconTheme = "System Default"
}
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
return fmt.Errorf("failed to create state dir: %w", err)
}
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
if err := buildOnce(&opts); err != nil {
return err
}
if opts.SyncModeWithPortal {
syncColorScheme(opts.Mode)
}
log.Info("Done")
return nil
}
func buildOnce(opts *Options) error {
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
if err != nil {
return fmt.Errorf("failed to create temp config: %w", err)
}
defer os.Remove(cfgFile.Name())
defer cfgFile.Close()
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
if err != nil {
return fmt.Errorf("failed to create temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
return fmt.Errorf("failed to build config: %w", err)
}
cfgFile.Close()
var primaryDark, primaryLight, surface string
var dank16JSON string
var importArgs []string
if opts.StockColors != "" {
log.Info("Using stock/custom theme colors with matugen base")
primaryDark = extractNestedColor(opts.StockColors, "primary", "dark")
primaryLight = extractNestedColor(opts.StockColors, "primary", "light")
surface = extractNestedColor(opts.StockColors, "surface", "dark")
if primaryDark == "" {
return fmt.Errorf("failed to extract primary dark from stock colors")
}
if primaryLight == "" {
primaryLight = primaryDark
}
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
importData := fmt.Sprintf(`{"colors": %s, "dank16": %s}`, opts.StockColors, dank16JSON)
importArgs = []string{"--import-json-string", importData}
log.Info("Running matugen color hex with stock color overrides")
args := []string{"color", "hex", primaryDark, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name()}
args = append(args, importArgs...)
if err := runMatugen(args); err != nil {
return err
}
} else {
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
matJSON, err := runMatugenDryRun(opts)
if err != nil {
return fmt.Errorf("matugen dry-run failed: %w", err)
}
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
primaryLight = extractMatugenColor(matJSON, "primary", "light")
surface = extractMatugenColor(matJSON, "surface", "dark")
if primaryDark == "" {
return fmt.Errorf("failed to extract primary color")
}
if primaryLight == "" {
primaryLight = primaryDark
}
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
importData := fmt.Sprintf(`{"dank16": %s}`, dank16JSON)
importArgs = []string{"--import-json-string", importData}
log.Infof("Running matugen %s with dank16 injection", opts.Kind)
var args []string
switch opts.Kind {
case "hex":
args = []string{"color", "hex", opts.Value}
default:
args = []string{opts.Kind, opts.Value}
}
args = append(args, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name())
args = append(args, importArgs...)
if err := runMatugen(args); err != nil {
return err
}
}
refreshGTK(opts.ConfigDir, opts.Mode)
signalTerminals()
return nil
}
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
wroteConfig := false
if opts.RunUserTemplates {
if data, err := os.ReadFile(userConfigPath); err == nil {
configSection := extractTOMLSection(string(data), "[config]", "[templates]")
if configSection != "" {
cfgFile.WriteString(configSection)
cfgFile.WriteString("\n")
wroteConfig = true
}
}
}
if !wroteConfig {
cfgFile.WriteString("[config]\n\n")
}
baseConfigPath := filepath.Join(opts.ShellDir, "matugen", "configs", "base.toml")
if data, err := os.ReadFile(baseConfigPath); err == nil {
content := string(data)
lines := strings.Split(content, "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "[config]" {
continue
}
cfgFile.WriteString(substituteShellDir(line, opts.ShellDir) + "\n")
}
cfgFile.WriteString("\n")
}
fmt.Fprintf(cfgFile, `[templates.dank]
input_path = '%s/matugen/templates/dank.json'
output_path = '%s'
`, opts.ShellDir, opts.ColorsOutput())
switch opts.Mode {
case "light":
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
default:
appendConfig(opts, cfgFile, "skip", "gtk3-dark.toml")
}
appendConfig(opts, cfgFile, "niri", "niri.toml")
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
homeDir, _ := os.UserHomeDir()
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
if opts.RunUserTemplates {
if data, err := os.ReadFile(userConfigPath); err == nil {
templatesSection := extractTOMLSection(string(data), "[templates]", "")
if templatesSection != "" {
cfgFile.WriteString(templatesSection)
cfgFile.WriteString("\n")
}
}
}
userPluginConfigDir := filepath.Join(opts.ConfigDir, "matugen", "dms", "configs")
if entries, err := os.ReadDir(userPluginConfigDir); err == nil {
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".toml") {
continue
}
if data, err := os.ReadFile(filepath.Join(userPluginConfigDir, entry.Name())); err == nil {
cfgFile.WriteString(string(data))
cfgFile.WriteString("\n")
}
}
}
return nil
}
func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
if _, err := os.Stat(configPath); err != nil {
return
}
if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
return
}
data, err := os.ReadFile(configPath)
if err != nil {
return
}
cfgFile.WriteString(substituteShellDir(string(data), opts.ShellDir))
cfgFile.WriteString("\n")
}
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fileName string) {
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
if _, err := os.Stat(configPath); err != nil {
return
}
if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
return
}
data, err := os.ReadFile(configPath)
if err != nil {
return
}
content := string(data)
if !opts.TerminalsAlwaysDark {
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
cfgFile.WriteString("\n")
return
}
lines := strings.Split(content, "\n")
for _, line := range lines {
if !strings.Contains(line, "input_path") || !strings.Contains(line, "SHELL_DIR/matugen/templates/") {
continue
}
start := strings.Index(line, "'SHELL_DIR/matugen/templates/")
if start == -1 {
continue
}
end := strings.Index(line[start+1:], "'")
if end == -1 {
continue
}
templateName := line[start+len("'SHELL_DIR/matugen/templates/") : start+1+end]
origPath := filepath.Join(opts.ShellDir, "matugen", "templates", templateName)
origData, err := os.ReadFile(origPath)
if err != nil {
continue
}
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
tmpPath := filepath.Join(tmpDir, templateName)
if err := os.WriteFile(tmpPath, []byte(modified), 0644); err != nil {
continue
}
content = strings.ReplaceAll(content,
fmt.Sprintf("'SHELL_DIR/matugen/templates/%s'", templateName),
fmt.Sprintf("'%s'", tmpPath))
}
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
cfgFile.WriteString("\n")
}
func appendVSCodeConfig(cfgFile *os.File, name, extDir, shellDir string) {
if _, err := os.Stat(extDir); err != nil {
return
}
templateDir := filepath.Join(shellDir, "matugen", "templates")
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
input_path = '%s/vscode-color-theme-default.json'
output_path = '%s/themes/dankshell-default.json'
[templates.dms%sdark]
input_path = '%s/vscode-color-theme-dark.json'
output_path = '%s/themes/dankshell-dark.json'
[templates.dms%slight]
input_path = '%s/vscode-color-theme-light.json'
output_path = '%s/themes/dankshell-light.json'
`, name, templateDir, extDir,
name, templateDir, extDir,
name, templateDir, extDir)
log.Infof("Added %s theme config (extension found at %s)", name, extDir)
}
func substituteShellDir(content, shellDir string) string {
return strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/")
}
func extractTOMLSection(content, startMarker, endMarker string) string {
startIdx := strings.Index(content, startMarker)
if startIdx == -1 {
return ""
}
if endMarker == "" {
return content[startIdx:]
}
endIdx := strings.Index(content[startIdx:], endMarker)
if endIdx == -1 {
return content[startIdx:]
}
return content[startIdx : startIdx+endIdx]
}
func checkMatugenVersion() {
matugenVersionOnce.Do(func() {
cmd := exec.Command("matugen", "--version")
output, err := cmd.Output()
if err != nil {
return
}
versionStr := strings.TrimSpace(string(output))
versionStr = strings.TrimPrefix(versionStr, "matugen ")
parts := strings.Split(versionStr, ".")
if len(parts) < 2 {
return
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return
}
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
if matugenSupportsCOE {
log.Infof("Matugen %s supports --continue-on-error", versionStr)
}
})
}
func runMatugen(args []string) error {
checkMatugenVersion()
if matugenSupportsCOE {
args = append([]string{"--continue-on-error"}, args...)
}
cmd := exec.Command("matugen", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func runMatugenDryRun(opts *Options) (string, error) {
var args []string
switch opts.Kind {
case "hex":
args = []string{"color", "hex", opts.Value}
default:
args = []string{opts.Kind, opts.Value}
}
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
cmd := exec.Command("matugen", args...)
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.ReplaceAll(string(output), "\n", ""), nil
}
func extractMatugenColor(jsonStr, colorName, variant string) string {
var data map[string]any
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return ""
}
colors, ok := data["colors"].(map[string]any)
if !ok {
return ""
}
colorData, ok := colors[colorName].(map[string]any)
if !ok {
return ""
}
variantData, ok := colorData[variant].(string)
if !ok {
return ""
}
return variantData
}
func extractNestedColor(jsonStr, colorName, variant string) string {
var data map[string]any
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return ""
}
colorData, ok := data[colorName].(map[string]any)
if !ok {
return ""
}
variantData, ok := colorData[variant].(map[string]any)
if !ok {
return ""
}
color, ok := variantData["color"].(string)
if !ok {
return ""
}
return color
}
func generateDank16Variants(primaryDark, primaryLight, surface, mode string) string {
variantOpts := dank16.VariantOptions{
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
Background: surface,
UseDPS: true,
IsLightMode: mode == "light",
}
variantColors := dank16.GenerateVariantPalette(variantOpts)
return dank16.GenerateVariantJSON(variantColors)
}
func refreshGTK(configDir, mode string) {
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
info, err := os.Lstat(gtkCSS)
if err != nil {
return
}
shouldRun := false
if info.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(gtkCSS)
if err == nil && strings.Contains(target, "dank-colors.css") {
shouldRun = true
}
} else {
data, err := os.ReadFile(gtkCSS)
if err == nil && strings.Contains(string(data), "dank-colors.css") {
shouldRun = true
}
}
if !shouldRun {
return
}
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "adw-gtk3-"+mode).Run()
}
func signalTerminals() {
signalByName("kitty", syscall.SIGUSR1)
signalByName("ghostty", syscall.SIGUSR2)
signalByName(".kitty-wrapped", syscall.SIGUSR1)
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
}
func signalByName(name string, sig syscall.Signal) {
entries, err := os.ReadDir("/proc")
if err != nil {
return
}
for _, entry := range entries {
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
if err != nil {
continue
}
if strings.TrimSpace(string(comm)) == name {
syscall.Kill(pid, sig)
}
}
}
func syncColorScheme(mode string) {
scheme := "prefer-dark"
if mode == "light" {
scheme = "default"
}
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
}
}

View File

@@ -1,139 +0,0 @@
package matugen
import (
"context"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
type Result struct {
Success bool
Error error
}
type QueuedJob struct {
Options Options
Done chan Result
Ctx context.Context
Cancel context.CancelFunc
}
type Queue struct {
mu sync.Mutex
current *QueuedJob
pending *QueuedJob
jobDone chan struct{}
}
var globalQueue *Queue
var queueOnce sync.Once
func GetQueue() *Queue {
queueOnce.Do(func() {
globalQueue = &Queue{
jobDone: make(chan struct{}, 1),
}
})
return globalQueue
}
func (q *Queue) Submit(opts Options) <-chan Result {
result := make(chan Result, 1)
ctx, cancel := context.WithCancel(context.Background())
job := &QueuedJob{
Options: opts,
Done: result,
Ctx: ctx,
Cancel: cancel,
}
q.mu.Lock()
if q.pending != nil {
log.Info("Cancelling pending theme request")
q.pending.Cancel()
q.pending.Done <- Result{Success: false, Error: context.Canceled}
close(q.pending.Done)
}
if q.current != nil {
q.pending = job
q.mu.Unlock()
log.Info("Theme request queued (worker running)")
return result
}
q.current = job
q.mu.Unlock()
go q.runWorker()
return result
}
func (q *Queue) runWorker() {
for {
q.mu.Lock()
job := q.current
if job == nil {
q.mu.Unlock()
return
}
q.mu.Unlock()
select {
case <-job.Ctx.Done():
q.finishJob(Result{Success: false, Error: context.Canceled})
continue
default:
}
log.Infof("Processing theme: %s %s (%s)", job.Options.Kind, job.Options.Value, job.Options.Mode)
err := Run(job.Options)
var result Result
if err != nil {
result = Result{Success: false, Error: err}
} else {
result = Result{Success: true}
}
q.finishJob(result)
}
}
func (q *Queue) finishJob(result Result) {
q.mu.Lock()
defer q.mu.Unlock()
if q.current != nil {
select {
case q.current.Done <- result:
default:
}
close(q.current.Done)
}
q.current = q.pending
q.pending = nil
if q.current == nil {
select {
case q.jobDone <- struct{}{}:
default:
}
}
}
func (q *Queue) IsRunning() bool {
q.mu.Lock()
defer q.mu.Unlock()
return q.current != nil
}
func (q *Queue) HasPending() bool {
q.mu.Lock()
defer q.mu.Unlock()
return q.pending != nil
}

View File

@@ -93,7 +93,7 @@ type MockDBusConn_Object_Call struct {
// Object is a helper method to define mock.On call // Object is a helper method to define mock.On call
// - dest string // - dest string
// - path dbus.ObjectPath // - path dbus.ObjectPath
func (_e *MockDBusConn_Expecter) Object(dest any, path any) *MockDBusConn_Object_Call { func (_e *MockDBusConn_Expecter) Object(dest interface{}, path interface{}) *MockDBusConn_Object_Call {
return &MockDBusConn_Object_Call{Call: _e.mock.On("Object", dest, path)} return &MockDBusConn_Object_Call{Call: _e.mock.On("Object", dest, path)}
} }
@@ -119,8 +119,7 @@ func (_c *MockDBusConn_Object_Call) RunAndReturn(run func(string, dbus.ObjectPat
func NewMockDBusConn(t interface { func NewMockDBusConn(t interface {
mock.TestingT mock.TestingT
Cleanup(func()) Cleanup(func())
}, }) *MockDBusConn {
) *MockDBusConn {
mock := &MockDBusConn{} mock := &MockDBusConn{}
mock.Mock.Test(t) mock.Mock.Test(t)

View File

@@ -22,99 +22,6 @@ func (_m *MockCUPSClientInterface) EXPECT() *MockCUPSClientInterface_Expecter {
return &MockCUPSClientInterface_Expecter{mock: &_m.Mock} return &MockCUPSClientInterface_Expecter{mock: &_m.Mock}
} }
// AcceptJobs provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) AcceptJobs(printer string) error {
ret := _m.Called(printer)
if len(ret) == 0 {
panic("no return value specified for AcceptJobs")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_AcceptJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AcceptJobs'
type MockCUPSClientInterface_AcceptJobs_Call struct {
*mock.Call
}
// AcceptJobs is a helper method to define mock.On call
// - printer string
func (_e *MockCUPSClientInterface_Expecter) AcceptJobs(printer interface{}) *MockCUPSClientInterface_AcceptJobs_Call {
return &MockCUPSClientInterface_AcceptJobs_Call{Call: _e.mock.On("AcceptJobs", printer)}
}
func (_c *MockCUPSClientInterface_AcceptJobs_Call) Run(run func(printer string)) *MockCUPSClientInterface_AcceptJobs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_AcceptJobs_Call) Return(_a0 error) *MockCUPSClientInterface_AcceptJobs_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_AcceptJobs_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_AcceptJobs_Call {
_c.Call.Return(run)
return _c
}
// AddPrinterToClass provides a mock function with given fields: class, printer
func (_m *MockCUPSClientInterface) AddPrinterToClass(class string, printer string) error {
ret := _m.Called(class, printer)
if len(ret) == 0 {
panic("no return value specified for AddPrinterToClass")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(class, printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_AddPrinterToClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddPrinterToClass'
type MockCUPSClientInterface_AddPrinterToClass_Call struct {
*mock.Call
}
// AddPrinterToClass is a helper method to define mock.On call
// - class string
// - printer string
func (_e *MockCUPSClientInterface_Expecter) AddPrinterToClass(class interface{}, printer interface{}) *MockCUPSClientInterface_AddPrinterToClass_Call {
return &MockCUPSClientInterface_AddPrinterToClass_Call{Call: _e.mock.On("AddPrinterToClass", class, printer)}
}
func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) Run(run func(class string, printer string)) *MockCUPSClientInterface_AddPrinterToClass_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) Return(_a0 error) *MockCUPSClientInterface_AddPrinterToClass_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_AddPrinterToClass_Call {
_c.Call.Return(run)
return _c
}
// CancelAllJob provides a mock function with given fields: printer, purge // CancelAllJob provides a mock function with given fields: printer, purge
func (_m *MockCUPSClientInterface) CancelAllJob(printer string, purge bool) error { func (_m *MockCUPSClientInterface) CancelAllJob(printer string, purge bool) error {
ret := _m.Called(printer, purge) ret := _m.Called(printer, purge)
@@ -209,312 +116,6 @@ func (_c *MockCUPSClientInterface_CancelJob_Call) RunAndReturn(run func(int, boo
return _c return _c
} }
// CreatePrinter provides a mock function with given fields: name, deviceURI, ppd, shared, errorPolicy, information, location
func (_m *MockCUPSClientInterface) CreatePrinter(name string, deviceURI string, ppd string, shared bool, errorPolicy string, information string, location string) error {
ret := _m.Called(name, deviceURI, ppd, shared, errorPolicy, information, location)
if len(ret) == 0 {
panic("no return value specified for CreatePrinter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, bool, string, string, string) error); ok {
r0 = rf(name, deviceURI, ppd, shared, errorPolicy, information, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_CreatePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePrinter'
type MockCUPSClientInterface_CreatePrinter_Call struct {
*mock.Call
}
// CreatePrinter is a helper method to define mock.On call
// - name string
// - deviceURI string
// - ppd string
// - shared bool
// - errorPolicy string
// - information string
// - location string
func (_e *MockCUPSClientInterface_Expecter) CreatePrinter(name interface{}, deviceURI interface{}, ppd interface{}, shared interface{}, errorPolicy interface{}, information interface{}, location interface{}) *MockCUPSClientInterface_CreatePrinter_Call {
return &MockCUPSClientInterface_CreatePrinter_Call{Call: _e.mock.On("CreatePrinter", name, deviceURI, ppd, shared, errorPolicy, information, location)}
}
func (_c *MockCUPSClientInterface_CreatePrinter_Call) Run(run func(name string, deviceURI string, ppd string, shared bool, errorPolicy string, information string, location string)) *MockCUPSClientInterface_CreatePrinter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string), args[3].(bool), args[4].(string), args[5].(string), args[6].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_CreatePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_CreatePrinter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_CreatePrinter_Call) RunAndReturn(run func(string, string, string, bool, string, string, string) error) *MockCUPSClientInterface_CreatePrinter_Call {
_c.Call.Return(run)
return _c
}
// DeleteClass provides a mock function with given fields: class
func (_m *MockCUPSClientInterface) DeleteClass(class string) error {
ret := _m.Called(class)
if len(ret) == 0 {
panic("no return value specified for DeleteClass")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(class)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_DeleteClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteClass'
type MockCUPSClientInterface_DeleteClass_Call struct {
*mock.Call
}
// DeleteClass is a helper method to define mock.On call
// - class string
func (_e *MockCUPSClientInterface_Expecter) DeleteClass(class interface{}) *MockCUPSClientInterface_DeleteClass_Call {
return &MockCUPSClientInterface_DeleteClass_Call{Call: _e.mock.On("DeleteClass", class)}
}
func (_c *MockCUPSClientInterface_DeleteClass_Call) Run(run func(class string)) *MockCUPSClientInterface_DeleteClass_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_DeleteClass_Call) Return(_a0 error) *MockCUPSClientInterface_DeleteClass_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_DeleteClass_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_DeleteClass_Call {
_c.Call.Return(run)
return _c
}
// DeletePrinter provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) DeletePrinter(printer string) error {
ret := _m.Called(printer)
if len(ret) == 0 {
panic("no return value specified for DeletePrinter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_DeletePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePrinter'
type MockCUPSClientInterface_DeletePrinter_Call struct {
*mock.Call
}
// DeletePrinter is a helper method to define mock.On call
// - printer string
func (_e *MockCUPSClientInterface_Expecter) DeletePrinter(printer interface{}) *MockCUPSClientInterface_DeletePrinter_Call {
return &MockCUPSClientInterface_DeletePrinter_Call{Call: _e.mock.On("DeletePrinter", printer)}
}
func (_c *MockCUPSClientInterface_DeletePrinter_Call) Run(run func(printer string)) *MockCUPSClientInterface_DeletePrinter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_DeletePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_DeletePrinter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_DeletePrinter_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_DeletePrinter_Call {
_c.Call.Return(run)
return _c
}
// DeletePrinterFromClass provides a mock function with given fields: class, printer
func (_m *MockCUPSClientInterface) DeletePrinterFromClass(class string, printer string) error {
ret := _m.Called(class, printer)
if len(ret) == 0 {
panic("no return value specified for DeletePrinterFromClass")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(class, printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_DeletePrinterFromClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePrinterFromClass'
type MockCUPSClientInterface_DeletePrinterFromClass_Call struct {
*mock.Call
}
// DeletePrinterFromClass is a helper method to define mock.On call
// - class string
// - printer string
func (_e *MockCUPSClientInterface_Expecter) DeletePrinterFromClass(class interface{}, printer interface{}) *MockCUPSClientInterface_DeletePrinterFromClass_Call {
return &MockCUPSClientInterface_DeletePrinterFromClass_Call{Call: _e.mock.On("DeletePrinterFromClass", class, printer)}
}
func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) Run(run func(class string, printer string)) *MockCUPSClientInterface_DeletePrinterFromClass_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) Return(_a0 error) *MockCUPSClientInterface_DeletePrinterFromClass_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_DeletePrinterFromClass_Call {
_c.Call.Return(run)
return _c
}
// GetClasses provides a mock function with given fields: attributes
func (_m *MockCUPSClientInterface) GetClasses(attributes []string) (map[string]ipp.Attributes, error) {
ret := _m.Called(attributes)
if len(ret) == 0 {
panic("no return value specified for GetClasses")
}
var r0 map[string]ipp.Attributes
var r1 error
if rf, ok := ret.Get(0).(func([]string) (map[string]ipp.Attributes, error)); ok {
return rf(attributes)
}
if rf, ok := ret.Get(0).(func([]string) map[string]ipp.Attributes); ok {
r0 = rf(attributes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]ipp.Attributes)
}
}
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(attributes)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCUPSClientInterface_GetClasses_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetClasses'
type MockCUPSClientInterface_GetClasses_Call struct {
*mock.Call
}
// GetClasses is a helper method to define mock.On call
// - attributes []string
func (_e *MockCUPSClientInterface_Expecter) GetClasses(attributes interface{}) *MockCUPSClientInterface_GetClasses_Call {
return &MockCUPSClientInterface_GetClasses_Call{Call: _e.mock.On("GetClasses", attributes)}
}
func (_c *MockCUPSClientInterface_GetClasses_Call) Run(run func(attributes []string)) *MockCUPSClientInterface_GetClasses_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]string))
})
return _c
}
func (_c *MockCUPSClientInterface_GetClasses_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetClasses_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCUPSClientInterface_GetClasses_Call) RunAndReturn(run func([]string) (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetClasses_Call {
_c.Call.Return(run)
return _c
}
// GetDevices provides a mock function with no fields
func (_m *MockCUPSClientInterface) GetDevices() (map[string]ipp.Attributes, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetDevices")
}
var r0 map[string]ipp.Attributes
var r1 error
if rf, ok := ret.Get(0).(func() (map[string]ipp.Attributes, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() map[string]ipp.Attributes); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]ipp.Attributes)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCUPSClientInterface_GetDevices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDevices'
type MockCUPSClientInterface_GetDevices_Call struct {
*mock.Call
}
// GetDevices is a helper method to define mock.On call
func (_e *MockCUPSClientInterface_Expecter) GetDevices() *MockCUPSClientInterface_GetDevices_Call {
return &MockCUPSClientInterface_GetDevices_Call{Call: _e.mock.On("GetDevices")}
}
func (_c *MockCUPSClientInterface_GetDevices_Call) Run(run func()) *MockCUPSClientInterface_GetDevices_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCUPSClientInterface_GetDevices_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetDevices_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCUPSClientInterface_GetDevices_Call) RunAndReturn(run func() (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetDevices_Call {
_c.Call.Return(run)
return _c
}
// GetJobs provides a mock function with given fields: printer, class, whichJobs, myJobs, firstJobId, limit, attributes // GetJobs provides a mock function with given fields: printer, class, whichJobs, myJobs, firstJobId, limit, attributes
func (_m *MockCUPSClientInterface) GetJobs(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string) (map[int]ipp.Attributes, error) { func (_m *MockCUPSClientInterface) GetJobs(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string) (map[int]ipp.Attributes, error) {
ret := _m.Called(printer, class, whichJobs, myJobs, firstJobId, limit, attributes) ret := _m.Called(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
@@ -579,63 +180,6 @@ func (_c *MockCUPSClientInterface_GetJobs_Call) RunAndReturn(run func(string, st
return _c return _c
} }
// GetPPDs provides a mock function with no fields
func (_m *MockCUPSClientInterface) GetPPDs() (map[string]ipp.Attributes, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetPPDs")
}
var r0 map[string]ipp.Attributes
var r1 error
if rf, ok := ret.Get(0).(func() (map[string]ipp.Attributes, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() map[string]ipp.Attributes); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]ipp.Attributes)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCUPSClientInterface_GetPPDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPPDs'
type MockCUPSClientInterface_GetPPDs_Call struct {
*mock.Call
}
// GetPPDs is a helper method to define mock.On call
func (_e *MockCUPSClientInterface_Expecter) GetPPDs() *MockCUPSClientInterface_GetPPDs_Call {
return &MockCUPSClientInterface_GetPPDs_Call{Call: _e.mock.On("GetPPDs")}
}
func (_c *MockCUPSClientInterface_GetPPDs_Call) Run(run func()) *MockCUPSClientInterface_GetPPDs_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockCUPSClientInterface_GetPPDs_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetPPDs_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCUPSClientInterface_GetPPDs_Call) RunAndReturn(run func() (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetPPDs_Call {
_c.Call.Return(run)
return _c
}
// GetPrinters provides a mock function with given fields: attributes // GetPrinters provides a mock function with given fields: attributes
func (_m *MockCUPSClientInterface) GetPrinters(attributes []string) (map[string]ipp.Attributes, error) { func (_m *MockCUPSClientInterface) GetPrinters(attributes []string) (map[string]ipp.Attributes, error) {
ret := _m.Called(attributes) ret := _m.Called(attributes)
@@ -694,100 +238,6 @@ func (_c *MockCUPSClientInterface_GetPrinters_Call) RunAndReturn(run func([]stri
return _c return _c
} }
// HoldJobUntil provides a mock function with given fields: jobID, holdUntil
func (_m *MockCUPSClientInterface) HoldJobUntil(jobID int, holdUntil string) error {
ret := _m.Called(jobID, holdUntil)
if len(ret) == 0 {
panic("no return value specified for HoldJobUntil")
}
var r0 error
if rf, ok := ret.Get(0).(func(int, string) error); ok {
r0 = rf(jobID, holdUntil)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_HoldJobUntil_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HoldJobUntil'
type MockCUPSClientInterface_HoldJobUntil_Call struct {
*mock.Call
}
// HoldJobUntil is a helper method to define mock.On call
// - jobID int
// - holdUntil string
func (_e *MockCUPSClientInterface_Expecter) HoldJobUntil(jobID interface{}, holdUntil interface{}) *MockCUPSClientInterface_HoldJobUntil_Call {
return &MockCUPSClientInterface_HoldJobUntil_Call{Call: _e.mock.On("HoldJobUntil", jobID, holdUntil)}
}
func (_c *MockCUPSClientInterface_HoldJobUntil_Call) Run(run func(jobID int, holdUntil string)) *MockCUPSClientInterface_HoldJobUntil_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_HoldJobUntil_Call) Return(_a0 error) *MockCUPSClientInterface_HoldJobUntil_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_HoldJobUntil_Call) RunAndReturn(run func(int, string) error) *MockCUPSClientInterface_HoldJobUntil_Call {
_c.Call.Return(run)
return _c
}
// MoveJob provides a mock function with given fields: jobID, destPrinter
func (_m *MockCUPSClientInterface) MoveJob(jobID int, destPrinter string) error {
ret := _m.Called(jobID, destPrinter)
if len(ret) == 0 {
panic("no return value specified for MoveJob")
}
var r0 error
if rf, ok := ret.Get(0).(func(int, string) error); ok {
r0 = rf(jobID, destPrinter)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_MoveJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MoveJob'
type MockCUPSClientInterface_MoveJob_Call struct {
*mock.Call
}
// MoveJob is a helper method to define mock.On call
// - jobID int
// - destPrinter string
func (_e *MockCUPSClientInterface_Expecter) MoveJob(jobID interface{}, destPrinter interface{}) *MockCUPSClientInterface_MoveJob_Call {
return &MockCUPSClientInterface_MoveJob_Call{Call: _e.mock.On("MoveJob", jobID, destPrinter)}
}
func (_c *MockCUPSClientInterface_MoveJob_Call) Run(run func(jobID int, destPrinter string)) *MockCUPSClientInterface_MoveJob_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_MoveJob_Call) Return(_a0 error) *MockCUPSClientInterface_MoveJob_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_MoveJob_Call) RunAndReturn(run func(int, string) error) *MockCUPSClientInterface_MoveJob_Call {
_c.Call.Return(run)
return _c
}
// PausePrinter provides a mock function with given fields: printer // PausePrinter provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) PausePrinter(printer string) error { func (_m *MockCUPSClientInterface) PausePrinter(printer string) error {
ret := _m.Called(printer) ret := _m.Called(printer)
@@ -834,156 +284,6 @@ func (_c *MockCUPSClientInterface_PausePrinter_Call) RunAndReturn(run func(strin
return _c return _c
} }
// PrintTestPage provides a mock function with given fields: printer, testPageData, size
func (_m *MockCUPSClientInterface) PrintTestPage(printer string, testPageData io.Reader, size int) (int, error) {
ret := _m.Called(printer, testPageData, size)
if len(ret) == 0 {
panic("no return value specified for PrintTestPage")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func(string, io.Reader, int) (int, error)); ok {
return rf(printer, testPageData, size)
}
if rf, ok := ret.Get(0).(func(string, io.Reader, int) int); ok {
r0 = rf(printer, testPageData, size)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func(string, io.Reader, int) error); ok {
r1 = rf(printer, testPageData, size)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCUPSClientInterface_PrintTestPage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrintTestPage'
type MockCUPSClientInterface_PrintTestPage_Call struct {
*mock.Call
}
// PrintTestPage is a helper method to define mock.On call
// - printer string
// - testPageData io.Reader
// - size int
func (_e *MockCUPSClientInterface_Expecter) PrintTestPage(printer interface{}, testPageData interface{}, size interface{}) *MockCUPSClientInterface_PrintTestPage_Call {
return &MockCUPSClientInterface_PrintTestPage_Call{Call: _e.mock.On("PrintTestPage", printer, testPageData, size)}
}
func (_c *MockCUPSClientInterface_PrintTestPage_Call) Run(run func(printer string, testPageData io.Reader, size int)) *MockCUPSClientInterface_PrintTestPage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(io.Reader), args[2].(int))
})
return _c
}
func (_c *MockCUPSClientInterface_PrintTestPage_Call) Return(_a0 int, _a1 error) *MockCUPSClientInterface_PrintTestPage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCUPSClientInterface_PrintTestPage_Call) RunAndReturn(run func(string, io.Reader, int) (int, error)) *MockCUPSClientInterface_PrintTestPage_Call {
_c.Call.Return(run)
return _c
}
// RejectJobs provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) RejectJobs(printer string) error {
ret := _m.Called(printer)
if len(ret) == 0 {
panic("no return value specified for RejectJobs")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(printer)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_RejectJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RejectJobs'
type MockCUPSClientInterface_RejectJobs_Call struct {
*mock.Call
}
// RejectJobs is a helper method to define mock.On call
// - printer string
func (_e *MockCUPSClientInterface_Expecter) RejectJobs(printer interface{}) *MockCUPSClientInterface_RejectJobs_Call {
return &MockCUPSClientInterface_RejectJobs_Call{Call: _e.mock.On("RejectJobs", printer)}
}
func (_c *MockCUPSClientInterface_RejectJobs_Call) Run(run func(printer string)) *MockCUPSClientInterface_RejectJobs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_RejectJobs_Call) Return(_a0 error) *MockCUPSClientInterface_RejectJobs_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_RejectJobs_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_RejectJobs_Call {
_c.Call.Return(run)
return _c
}
// RestartJob provides a mock function with given fields: jobID
func (_m *MockCUPSClientInterface) RestartJob(jobID int) error {
ret := _m.Called(jobID)
if len(ret) == 0 {
panic("no return value specified for RestartJob")
}
var r0 error
if rf, ok := ret.Get(0).(func(int) error); ok {
r0 = rf(jobID)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_RestartJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestartJob'
type MockCUPSClientInterface_RestartJob_Call struct {
*mock.Call
}
// RestartJob is a helper method to define mock.On call
// - jobID int
func (_e *MockCUPSClientInterface_Expecter) RestartJob(jobID interface{}) *MockCUPSClientInterface_RestartJob_Call {
return &MockCUPSClientInterface_RestartJob_Call{Call: _e.mock.On("RestartJob", jobID)}
}
func (_c *MockCUPSClientInterface_RestartJob_Call) Run(run func(jobID int)) *MockCUPSClientInterface_RestartJob_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int))
})
return _c
}
func (_c *MockCUPSClientInterface_RestartJob_Call) Return(_a0 error) *MockCUPSClientInterface_RestartJob_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_RestartJob_Call) RunAndReturn(run func(int) error) *MockCUPSClientInterface_RestartJob_Call {
_c.Call.Return(run)
return _c
}
// ResumePrinter provides a mock function with given fields: printer // ResumePrinter provides a mock function with given fields: printer
func (_m *MockCUPSClientInterface) ResumePrinter(printer string) error { func (_m *MockCUPSClientInterface) ResumePrinter(printer string) error {
ret := _m.Called(printer) ret := _m.Called(printer)
@@ -1090,147 +390,6 @@ func (_c *MockCUPSClientInterface_SendRequest_Call) RunAndReturn(run func(string
return _c return _c
} }
// SetPrinterInformation provides a mock function with given fields: printer, information
func (_m *MockCUPSClientInterface) SetPrinterInformation(printer string, information string) error {
ret := _m.Called(printer, information)
if len(ret) == 0 {
panic("no return value specified for SetPrinterInformation")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(printer, information)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_SetPrinterInformation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterInformation'
type MockCUPSClientInterface_SetPrinterInformation_Call struct {
*mock.Call
}
// SetPrinterInformation is a helper method to define mock.On call
// - printer string
// - information string
func (_e *MockCUPSClientInterface_Expecter) SetPrinterInformation(printer interface{}, information interface{}) *MockCUPSClientInterface_SetPrinterInformation_Call {
return &MockCUPSClientInterface_SetPrinterInformation_Call{Call: _e.mock.On("SetPrinterInformation", printer, information)}
}
func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) Run(run func(printer string, information string)) *MockCUPSClientInterface_SetPrinterInformation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterInformation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_SetPrinterInformation_Call {
_c.Call.Return(run)
return _c
}
// SetPrinterIsShared provides a mock function with given fields: printer, shared
func (_m *MockCUPSClientInterface) SetPrinterIsShared(printer string, shared bool) error {
ret := _m.Called(printer, shared)
if len(ret) == 0 {
panic("no return value specified for SetPrinterIsShared")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(printer, shared)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_SetPrinterIsShared_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterIsShared'
type MockCUPSClientInterface_SetPrinterIsShared_Call struct {
*mock.Call
}
// SetPrinterIsShared is a helper method to define mock.On call
// - printer string
// - shared bool
func (_e *MockCUPSClientInterface_Expecter) SetPrinterIsShared(printer interface{}, shared interface{}) *MockCUPSClientInterface_SetPrinterIsShared_Call {
return &MockCUPSClientInterface_SetPrinterIsShared_Call{Call: _e.mock.On("SetPrinterIsShared", printer, shared)}
}
func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) Run(run func(printer string, shared bool)) *MockCUPSClientInterface_SetPrinterIsShared_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool))
})
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterIsShared_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) RunAndReturn(run func(string, bool) error) *MockCUPSClientInterface_SetPrinterIsShared_Call {
_c.Call.Return(run)
return _c
}
// SetPrinterLocation provides a mock function with given fields: printer, location
func (_m *MockCUPSClientInterface) SetPrinterLocation(printer string, location string) error {
ret := _m.Called(printer, location)
if len(ret) == 0 {
panic("no return value specified for SetPrinterLocation")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(printer, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockCUPSClientInterface_SetPrinterLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterLocation'
type MockCUPSClientInterface_SetPrinterLocation_Call struct {
*mock.Call
}
// SetPrinterLocation is a helper method to define mock.On call
// - printer string
// - location string
func (_e *MockCUPSClientInterface_Expecter) SetPrinterLocation(printer interface{}, location interface{}) *MockCUPSClientInterface_SetPrinterLocation_Call {
return &MockCUPSClientInterface_SetPrinterLocation_Call{Call: _e.mock.On("SetPrinterLocation", printer, location)}
}
func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) Run(run func(printer string, location string)) *MockCUPSClientInterface_SetPrinterLocation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterLocation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_SetPrinterLocation_Call {
_c.Call.Return(run)
return _c
}
// NewMockCUPSClientInterface creates a new instance of MockCUPSClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // NewMockCUPSClientInterface creates a new instance of MockCUPSClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value. // The first argument is typically a *testing.T value.
func NewMockCUPSClientInterface(t interface { func NewMockCUPSClientInterface(t interface {

View File

@@ -1,708 +0,0 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks_cups_pkhelper
import (
cups "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
mock "github.com/stretchr/testify/mock"
)
// MockPkHelper is an autogenerated mock type for the PkHelper type
type MockPkHelper struct {
mock.Mock
}
type MockPkHelper_Expecter struct {
mock *mock.Mock
}
func (_m *MockPkHelper) EXPECT() *MockPkHelper_Expecter {
return &MockPkHelper_Expecter{mock: &_m.Mock}
}
// ClassAddPrinter provides a mock function with given fields: className, printerName
func (_m *MockPkHelper) ClassAddPrinter(className string, printerName string) error {
ret := _m.Called(className, printerName)
if len(ret) == 0 {
panic("no return value specified for ClassAddPrinter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(className, printerName)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_ClassAddPrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassAddPrinter'
type MockPkHelper_ClassAddPrinter_Call struct {
*mock.Call
}
// ClassAddPrinter is a helper method to define mock.On call
// - className string
// - printerName string
func (_e *MockPkHelper_Expecter) ClassAddPrinter(className interface{}, printerName interface{}) *MockPkHelper_ClassAddPrinter_Call {
return &MockPkHelper_ClassAddPrinter_Call{Call: _e.mock.On("ClassAddPrinter", className, printerName)}
}
func (_c *MockPkHelper_ClassAddPrinter_Call) Run(run func(className string, printerName string)) *MockPkHelper_ClassAddPrinter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_ClassAddPrinter_Call) Return(_a0 error) *MockPkHelper_ClassAddPrinter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_ClassAddPrinter_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_ClassAddPrinter_Call {
_c.Call.Return(run)
return _c
}
// ClassDelete provides a mock function with given fields: className
func (_m *MockPkHelper) ClassDelete(className string) error {
ret := _m.Called(className)
if len(ret) == 0 {
panic("no return value specified for ClassDelete")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(className)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_ClassDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassDelete'
type MockPkHelper_ClassDelete_Call struct {
*mock.Call
}
// ClassDelete is a helper method to define mock.On call
// - className string
func (_e *MockPkHelper_Expecter) ClassDelete(className interface{}) *MockPkHelper_ClassDelete_Call {
return &MockPkHelper_ClassDelete_Call{Call: _e.mock.On("ClassDelete", className)}
}
func (_c *MockPkHelper_ClassDelete_Call) Run(run func(className string)) *MockPkHelper_ClassDelete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockPkHelper_ClassDelete_Call) Return(_a0 error) *MockPkHelper_ClassDelete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_ClassDelete_Call) RunAndReturn(run func(string) error) *MockPkHelper_ClassDelete_Call {
_c.Call.Return(run)
return _c
}
// ClassDeletePrinter provides a mock function with given fields: className, printerName
func (_m *MockPkHelper) ClassDeletePrinter(className string, printerName string) error {
ret := _m.Called(className, printerName)
if len(ret) == 0 {
panic("no return value specified for ClassDeletePrinter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(className, printerName)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_ClassDeletePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassDeletePrinter'
type MockPkHelper_ClassDeletePrinter_Call struct {
*mock.Call
}
// ClassDeletePrinter is a helper method to define mock.On call
// - className string
// - printerName string
func (_e *MockPkHelper_Expecter) ClassDeletePrinter(className interface{}, printerName interface{}) *MockPkHelper_ClassDeletePrinter_Call {
return &MockPkHelper_ClassDeletePrinter_Call{Call: _e.mock.On("ClassDeletePrinter", className, printerName)}
}
func (_c *MockPkHelper_ClassDeletePrinter_Call) Run(run func(className string, printerName string)) *MockPkHelper_ClassDeletePrinter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_ClassDeletePrinter_Call) Return(_a0 error) *MockPkHelper_ClassDeletePrinter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_ClassDeletePrinter_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_ClassDeletePrinter_Call {
_c.Call.Return(run)
return _c
}
// DevicesGet provides a mock function with given fields: timeout, limit, includeSchemes, excludeSchemes
func (_m *MockPkHelper) DevicesGet(timeout int, limit int, includeSchemes []string, excludeSchemes []string) ([]cups.Device, error) {
ret := _m.Called(timeout, limit, includeSchemes, excludeSchemes)
if len(ret) == 0 {
panic("no return value specified for DevicesGet")
}
var r0 []cups.Device
var r1 error
if rf, ok := ret.Get(0).(func(int, int, []string, []string) ([]cups.Device, error)); ok {
return rf(timeout, limit, includeSchemes, excludeSchemes)
}
if rf, ok := ret.Get(0).(func(int, int, []string, []string) []cups.Device); ok {
r0 = rf(timeout, limit, includeSchemes, excludeSchemes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]cups.Device)
}
}
if rf, ok := ret.Get(1).(func(int, int, []string, []string) error); ok {
r1 = rf(timeout, limit, includeSchemes, excludeSchemes)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockPkHelper_DevicesGet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DevicesGet'
type MockPkHelper_DevicesGet_Call struct {
*mock.Call
}
// DevicesGet is a helper method to define mock.On call
// - timeout int
// - limit int
// - includeSchemes []string
// - excludeSchemes []string
func (_e *MockPkHelper_Expecter) DevicesGet(timeout interface{}, limit interface{}, includeSchemes interface{}, excludeSchemes interface{}) *MockPkHelper_DevicesGet_Call {
return &MockPkHelper_DevicesGet_Call{Call: _e.mock.On("DevicesGet", timeout, limit, includeSchemes, excludeSchemes)}
}
func (_c *MockPkHelper_DevicesGet_Call) Run(run func(timeout int, limit int, includeSchemes []string, excludeSchemes []string)) *MockPkHelper_DevicesGet_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(int), args[2].([]string), args[3].([]string))
})
return _c
}
func (_c *MockPkHelper_DevicesGet_Call) Return(_a0 []cups.Device, _a1 error) *MockPkHelper_DevicesGet_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockPkHelper_DevicesGet_Call) RunAndReturn(run func(int, int, []string, []string) ([]cups.Device, error)) *MockPkHelper_DevicesGet_Call {
_c.Call.Return(run)
return _c
}
// JobCancelPurge provides a mock function with given fields: jobID, purge
func (_m *MockPkHelper) JobCancelPurge(jobID int, purge bool) error {
ret := _m.Called(jobID, purge)
if len(ret) == 0 {
panic("no return value specified for JobCancelPurge")
}
var r0 error
if rf, ok := ret.Get(0).(func(int, bool) error); ok {
r0 = rf(jobID, purge)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_JobCancelPurge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobCancelPurge'
type MockPkHelper_JobCancelPurge_Call struct {
*mock.Call
}
// JobCancelPurge is a helper method to define mock.On call
// - jobID int
// - purge bool
func (_e *MockPkHelper_Expecter) JobCancelPurge(jobID interface{}, purge interface{}) *MockPkHelper_JobCancelPurge_Call {
return &MockPkHelper_JobCancelPurge_Call{Call: _e.mock.On("JobCancelPurge", jobID, purge)}
}
func (_c *MockPkHelper_JobCancelPurge_Call) Run(run func(jobID int, purge bool)) *MockPkHelper_JobCancelPurge_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(bool))
})
return _c
}
func (_c *MockPkHelper_JobCancelPurge_Call) Return(_a0 error) *MockPkHelper_JobCancelPurge_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_JobCancelPurge_Call) RunAndReturn(run func(int, bool) error) *MockPkHelper_JobCancelPurge_Call {
_c.Call.Return(run)
return _c
}
// JobRestart provides a mock function with given fields: jobID
func (_m *MockPkHelper) JobRestart(jobID int) error {
ret := _m.Called(jobID)
if len(ret) == 0 {
panic("no return value specified for JobRestart")
}
var r0 error
if rf, ok := ret.Get(0).(func(int) error); ok {
r0 = rf(jobID)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_JobRestart_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobRestart'
type MockPkHelper_JobRestart_Call struct {
*mock.Call
}
// JobRestart is a helper method to define mock.On call
// - jobID int
func (_e *MockPkHelper_Expecter) JobRestart(jobID interface{}) *MockPkHelper_JobRestart_Call {
return &MockPkHelper_JobRestart_Call{Call: _e.mock.On("JobRestart", jobID)}
}
func (_c *MockPkHelper_JobRestart_Call) Run(run func(jobID int)) *MockPkHelper_JobRestart_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int))
})
return _c
}
func (_c *MockPkHelper_JobRestart_Call) Return(_a0 error) *MockPkHelper_JobRestart_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_JobRestart_Call) RunAndReturn(run func(int) error) *MockPkHelper_JobRestart_Call {
_c.Call.Return(run)
return _c
}
// JobSetHoldUntil provides a mock function with given fields: jobID, holdUntil
func (_m *MockPkHelper) JobSetHoldUntil(jobID int, holdUntil string) error {
ret := _m.Called(jobID, holdUntil)
if len(ret) == 0 {
panic("no return value specified for JobSetHoldUntil")
}
var r0 error
if rf, ok := ret.Get(0).(func(int, string) error); ok {
r0 = rf(jobID, holdUntil)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_JobSetHoldUntil_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobSetHoldUntil'
type MockPkHelper_JobSetHoldUntil_Call struct {
*mock.Call
}
// JobSetHoldUntil is a helper method to define mock.On call
// - jobID int
// - holdUntil string
func (_e *MockPkHelper_Expecter) JobSetHoldUntil(jobID interface{}, holdUntil interface{}) *MockPkHelper_JobSetHoldUntil_Call {
return &MockPkHelper_JobSetHoldUntil_Call{Call: _e.mock.On("JobSetHoldUntil", jobID, holdUntil)}
}
func (_c *MockPkHelper_JobSetHoldUntil_Call) Run(run func(jobID int, holdUntil string)) *MockPkHelper_JobSetHoldUntil_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_JobSetHoldUntil_Call) Return(_a0 error) *MockPkHelper_JobSetHoldUntil_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_JobSetHoldUntil_Call) RunAndReturn(run func(int, string) error) *MockPkHelper_JobSetHoldUntil_Call {
_c.Call.Return(run)
return _c
}
// PrinterAdd provides a mock function with given fields: name, uri, ppd, info, location
func (_m *MockPkHelper) PrinterAdd(name string, uri string, ppd string, info string, location string) error {
ret := _m.Called(name, uri, ppd, info, location)
if len(ret) == 0 {
panic("no return value specified for PrinterAdd")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(name, uri, ppd, info, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterAdd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterAdd'
type MockPkHelper_PrinterAdd_Call struct {
*mock.Call
}
// PrinterAdd is a helper method to define mock.On call
// - name string
// - uri string
// - ppd string
// - info string
// - location string
func (_e *MockPkHelper_Expecter) PrinterAdd(name interface{}, uri interface{}, ppd interface{}, info interface{}, location interface{}) *MockPkHelper_PrinterAdd_Call {
return &MockPkHelper_PrinterAdd_Call{Call: _e.mock.On("PrinterAdd", name, uri, ppd, info, location)}
}
func (_c *MockPkHelper_PrinterAdd_Call) Run(run func(name string, uri string, ppd string, info string, location string)) *MockPkHelper_PrinterAdd_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string), args[3].(string), args[4].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterAdd_Call) Return(_a0 error) *MockPkHelper_PrinterAdd_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterAdd_Call) RunAndReturn(run func(string, string, string, string, string) error) *MockPkHelper_PrinterAdd_Call {
_c.Call.Return(run)
return _c
}
// PrinterDelete provides a mock function with given fields: name
func (_m *MockPkHelper) PrinterDelete(name string) error {
ret := _m.Called(name)
if len(ret) == 0 {
panic("no return value specified for PrinterDelete")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(name)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterDelete'
type MockPkHelper_PrinterDelete_Call struct {
*mock.Call
}
// PrinterDelete is a helper method to define mock.On call
// - name string
func (_e *MockPkHelper_Expecter) PrinterDelete(name interface{}) *MockPkHelper_PrinterDelete_Call {
return &MockPkHelper_PrinterDelete_Call{Call: _e.mock.On("PrinterDelete", name)}
}
func (_c *MockPkHelper_PrinterDelete_Call) Run(run func(name string)) *MockPkHelper_PrinterDelete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterDelete_Call) Return(_a0 error) *MockPkHelper_PrinterDelete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterDelete_Call) RunAndReturn(run func(string) error) *MockPkHelper_PrinterDelete_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetAcceptJobs provides a mock function with given fields: name, enabled, reason
func (_m *MockPkHelper) PrinterSetAcceptJobs(name string, enabled bool, reason string) error {
ret := _m.Called(name, enabled, reason)
if len(ret) == 0 {
panic("no return value specified for PrinterSetAcceptJobs")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool, string) error); ok {
r0 = rf(name, enabled, reason)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetAcceptJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetAcceptJobs'
type MockPkHelper_PrinterSetAcceptJobs_Call struct {
*mock.Call
}
// PrinterSetAcceptJobs is a helper method to define mock.On call
// - name string
// - enabled bool
// - reason string
func (_e *MockPkHelper_Expecter) PrinterSetAcceptJobs(name interface{}, enabled interface{}, reason interface{}) *MockPkHelper_PrinterSetAcceptJobs_Call {
return &MockPkHelper_PrinterSetAcceptJobs_Call{Call: _e.mock.On("PrinterSetAcceptJobs", name, enabled, reason)}
}
func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) Run(run func(name string, enabled bool, reason string)) *MockPkHelper_PrinterSetAcceptJobs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool), args[2].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) Return(_a0 error) *MockPkHelper_PrinterSetAcceptJobs_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) RunAndReturn(run func(string, bool, string) error) *MockPkHelper_PrinterSetAcceptJobs_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetEnabled provides a mock function with given fields: name, enabled
func (_m *MockPkHelper) PrinterSetEnabled(name string, enabled bool) error {
ret := _m.Called(name, enabled)
if len(ret) == 0 {
panic("no return value specified for PrinterSetEnabled")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(name, enabled)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetEnabled'
type MockPkHelper_PrinterSetEnabled_Call struct {
*mock.Call
}
// PrinterSetEnabled is a helper method to define mock.On call
// - name string
// - enabled bool
func (_e *MockPkHelper_Expecter) PrinterSetEnabled(name interface{}, enabled interface{}) *MockPkHelper_PrinterSetEnabled_Call {
return &MockPkHelper_PrinterSetEnabled_Call{Call: _e.mock.On("PrinterSetEnabled", name, enabled)}
}
func (_c *MockPkHelper_PrinterSetEnabled_Call) Run(run func(name string, enabled bool)) *MockPkHelper_PrinterSetEnabled_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool))
})
return _c
}
func (_c *MockPkHelper_PrinterSetEnabled_Call) Return(_a0 error) *MockPkHelper_PrinterSetEnabled_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetEnabled_Call) RunAndReturn(run func(string, bool) error) *MockPkHelper_PrinterSetEnabled_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetInfo provides a mock function with given fields: name, info
func (_m *MockPkHelper) PrinterSetInfo(name string, info string) error {
ret := _m.Called(name, info)
if len(ret) == 0 {
panic("no return value specified for PrinterSetInfo")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(name, info)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetInfo'
type MockPkHelper_PrinterSetInfo_Call struct {
*mock.Call
}
// PrinterSetInfo is a helper method to define mock.On call
// - name string
// - info string
func (_e *MockPkHelper_Expecter) PrinterSetInfo(name interface{}, info interface{}) *MockPkHelper_PrinterSetInfo_Call {
return &MockPkHelper_PrinterSetInfo_Call{Call: _e.mock.On("PrinterSetInfo", name, info)}
}
func (_c *MockPkHelper_PrinterSetInfo_Call) Run(run func(name string, info string)) *MockPkHelper_PrinterSetInfo_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterSetInfo_Call) Return(_a0 error) *MockPkHelper_PrinterSetInfo_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetInfo_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_PrinterSetInfo_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetLocation provides a mock function with given fields: name, location
func (_m *MockPkHelper) PrinterSetLocation(name string, location string) error {
ret := _m.Called(name, location)
if len(ret) == 0 {
panic("no return value specified for PrinterSetLocation")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(name, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetLocation'
type MockPkHelper_PrinterSetLocation_Call struct {
*mock.Call
}
// PrinterSetLocation is a helper method to define mock.On call
// - name string
// - location string
func (_e *MockPkHelper_Expecter) PrinterSetLocation(name interface{}, location interface{}) *MockPkHelper_PrinterSetLocation_Call {
return &MockPkHelper_PrinterSetLocation_Call{Call: _e.mock.On("PrinterSetLocation", name, location)}
}
func (_c *MockPkHelper_PrinterSetLocation_Call) Run(run func(name string, location string)) *MockPkHelper_PrinterSetLocation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockPkHelper_PrinterSetLocation_Call) Return(_a0 error) *MockPkHelper_PrinterSetLocation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetLocation_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_PrinterSetLocation_Call {
_c.Call.Return(run)
return _c
}
// PrinterSetShared provides a mock function with given fields: name, shared
func (_m *MockPkHelper) PrinterSetShared(name string, shared bool) error {
ret := _m.Called(name, shared)
if len(ret) == 0 {
panic("no return value specified for PrinterSetShared")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = rf(name, shared)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockPkHelper_PrinterSetShared_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetShared'
type MockPkHelper_PrinterSetShared_Call struct {
*mock.Call
}
// PrinterSetShared is a helper method to define mock.On call
// - name string
// - shared bool
func (_e *MockPkHelper_Expecter) PrinterSetShared(name interface{}, shared interface{}) *MockPkHelper_PrinterSetShared_Call {
return &MockPkHelper_PrinterSetShared_Call{Call: _e.mock.On("PrinterSetShared", name, shared)}
}
func (_c *MockPkHelper_PrinterSetShared_Call) Run(run func(name string, shared bool)) *MockPkHelper_PrinterSetShared_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool))
})
return _c
}
func (_c *MockPkHelper_PrinterSetShared_Call) Return(_a0 error) *MockPkHelper_PrinterSetShared_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockPkHelper_PrinterSetShared_Call) RunAndReturn(run func(string, bool) error) *MockPkHelper_PrinterSetShared_Call {
_c.Call.Return(run)
return _c
}
// NewMockPkHelper creates a new instance of MockPkHelper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockPkHelper(t interface {
mock.TestingT
Cleanup(func())
}) *MockPkHelper {
mock := &MockPkHelper{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -52,7 +52,7 @@ type MockGitClient_HasUpdates_Call struct {
// HasUpdates is a helper method to define mock.On call // HasUpdates is a helper method to define mock.On call
// - path string // - path string
func (_e *MockGitClient_Expecter) HasUpdates(path any) *MockGitClient_HasUpdates_Call { func (_e *MockGitClient_Expecter) HasUpdates(path interface{}) *MockGitClient_HasUpdates_Call {
return &MockGitClient_HasUpdates_Call{Call: _e.mock.On("HasUpdates", path)} return &MockGitClient_HasUpdates_Call{Call: _e.mock.On("HasUpdates", path)}
} }
@@ -99,7 +99,7 @@ type MockGitClient_PlainClone_Call struct {
// PlainClone is a helper method to define mock.On call // PlainClone is a helper method to define mock.On call
// - path string // - path string
// - url string // - url string
func (_e *MockGitClient_Expecter) PlainClone(path any, url any) *MockGitClient_PlainClone_Call { func (_e *MockGitClient_Expecter) PlainClone(path interface{}, url interface{}) *MockGitClient_PlainClone_Call {
return &MockGitClient_PlainClone_Call{Call: _e.mock.On("PlainClone", path, url)} return &MockGitClient_PlainClone_Call{Call: _e.mock.On("PlainClone", path, url)}
} }
@@ -145,7 +145,7 @@ type MockGitClient_Pull_Call struct {
// Pull is a helper method to define mock.On call // Pull is a helper method to define mock.On call
// - path string // - path string
func (_e *MockGitClient_Expecter) Pull(path any) *MockGitClient_Pull_Call { func (_e *MockGitClient_Expecter) Pull(path interface{}) *MockGitClient_Pull_Call {
return &MockGitClient_Pull_Call{Call: _e.mock.On("Pull", path)} return &MockGitClient_Pull_Call{Call: _e.mock.On("Pull", path)}
} }

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