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

Compare commits

..

1 Commits

Author SHA1 Message Date
purian23 b40fbeef56 Freeze compact XBPS repository for R2 migration 2026-07-10 18:25:36 -04:00
1020 changed files with 32 additions and 294584 deletions
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
cd "$REPO_ROOT"
# =============================================================================
# Go CI checks (when core/ files are staged)
# =============================================================================
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
# =============================================================================
# i18n sync check (DISABLED for now)
# =============================================================================
# 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
# echo "Translations out of sync"
# echo "Run: python3 scripts/i18nsync.py sync"
# exit 1
# fi
# fi
# fi
exit 0
-15
View File
@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: [avengemedia]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: danklinux
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
-65
View File
@@ -1,65 +0,0 @@
---
name: Bug Report
about: Crashes or unexpected behaviors
title: ""
labels: "bug"
assignees: ""
---
<!-- If your issue is related to ICONS
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
- [ ] Other (specify)
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the issue -->
## Expected Behavior
<!-- Describe what you expected to happen -->
## Steps to Reproduce
<!-- Please provide detailed steps to reproduce the issue -->
1.
2.
3.
## Error Messages/Logs
<!-- Please include any error messages, stack traces, or relevant logs -->
<!-- you can get a log file with the following steps:
dms kill
mkdir ~/dms_logs
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
-->
```
Paste error messages or logs here
```
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->
-33
View File
@@ -1,33 +0,0 @@
---
name: Request a Feature
about: New widgets, new widget behavior, etc.
title: ""
labels: "enhancement"
assignees: ""
---
## Feature Description
<!-- Brief description of the feature requested -->
## Use Case
<!-- Explain the purpose of this feature/why it'd be useful to you -->
## Compositor
Is this feature specific to one compositor?
- [ ] All compositors
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
## Proposed Solution
<!-- If you have any ideas for how to implement this, please share! -->
## Alternatives/Existing Solutions
<!-- Include any similar/pre-existing products that solve this problem -->
-40
View File
@@ -1,40 +0,0 @@
---
name: Request Assistance or Support
about: Help with installation, usage, or general questions.
title: ""
labels: "support"
assignees: ""
---
## Compositor
- [ ] niri
- [ ] Hyprland
- [ ] dwl (MangoWC)
- [ ] sway
- [ ] other
## Distribution
<!-- Arch, Fedora, Debian, etc. -->
## dms version
<!-- Output of dms version command -->
## Description
<!-- Brief description of the support needed -->
## Solutions Tried
<!-- Describe what you've tried so far -->
<!-- Outlining what you've tried so far helps us make improvements to the user experience and documentation to avoid recurrent issues -->
## Configuration Details
<!-- Include any configuration if relevant -->
## Screenshots/Recordings
<!-- If applicable, add screenshots or screen recordings -->
-60
View File
@@ -1,60 +0,0 @@
name: Go CI
on:
push:
branches:
- "**"
paths:
- "core/**"
- ".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:
lint-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: core
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Format check
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
gofmt -s -l .
exit 1
fi
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.6
working-directory: core
- name: Test
run: go test -v ./...
- name: Build dms
run: go build -v ./cmd/dms
- name: Build dms (distropkg)
run: go build -v -tags distro_binary ./cmd/dms
- name: Build dankinstall
run: go build -v ./cmd/dankinstall
-23
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
-691
View File
@@ -1,691 +0,0 @@
name: Release
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g., v1.0.1)'
required: true
type: string
permissions:
contents: write
actions: write
concurrency:
group: release-${{ inputs.tag }}
cancel-in-progress: true
jobs:
build-core:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [amd64, arm64]
defaults:
run:
working-directory: core
env:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Format check
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
gofmt -s -l .
exit 1
fi
- name: Run tests
run: go test -v ./...
- name: Build dankinstall (${{ matrix.arch }})
env:
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dankinstall
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dankinstall-${{ matrix.arch }}
cd ../..
gzip -9 -k dankinstall-${{ matrix.arch }}
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
- name: Build dms (${{ matrix.arch }})
env:
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-${{ matrix.arch }}
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
- name: Generate shell completions
if: matrix.arch == 'amd64'
run: |
set -eux
chmod +x dms-amd64
./dms-amd64 completion bash > completion.bash
./dms-amd64 completion fish > completion.fish
./dms-amd64 completion zsh > completion.zsh
- name: Build dms-distropkg (${{ matrix.arch }})
env:
GOOS: linux
CGO_ENABLED: 0
GOARCH: ${{ matrix.arch }}
run: |
set -eux
cd cmd/dms
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
-o ../../dms-distropkg-${{ matrix.arch }}
cd ../..
gzip -9 -k dms-distropkg-${{ matrix.arch }}
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
- name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v4
with:
name: core-assets-${{ matrix.arch }}
path: |
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
if-no-files-found: error
- name: Upload artifacts with completions
if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v4
with:
name: core-assets-${{ matrix.arch }}
path: |
core/dankinstall-${{ matrix.arch }}.gz
core/dankinstall-${{ matrix.arch }}.gz.sha256
core/dms-${{ matrix.arch }}.gz
core/dms-${{ matrix.arch }}.gz.sha256
core/dms-distropkg-${{ matrix.arch }}.gz
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
core/completion.bash
core/completion.fish
core/completion.zsh
if-no-files-found: error
# update-versions:
# runs-on: ubuntu-latest
# needs: build-core
# steps:
# - name: Create GitHub App token
# id: app_token
# uses: actions/create-github-app-token@v1
# with:
# app-id: ${{ secrets.APP_ID }}
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
# - name: Checkout
# uses: actions/checkout@v4
# with:
# token: ${{ steps.app_token.outputs.token }}
# fetch-depth: 0
# - name: Update VERSION
# env:
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
# run: |
# set -euo pipefail
# git config user.name "dms-ci[bot]"
# git config user.email "dms-ci[bot]@users.noreply.github.com"
# version="${GITHUB_REF#refs/tags/}"
# echo "Updating to version: $version"
# echo "${version}" > quickshell/VERSION
# git add quickshell/VERSION
# if ! git diff --cached --quiet; then
# git commit -m "chore: bump version to $version"
# git pull --rebase origin master
# git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
# fi
# 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
needs: [build-core] #, update-versions]
env:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Fetch updated tag after version bump
run: |
git fetch origin --force tag ${TAG}
git checkout ${TAG}
- name: Download core artifacts
uses: actions/download-artifact@v4
with:
pattern: core-assets-*
merge-multiple: true
path: ./_core_assets
- name: Generate Changelog
id: changelog
run: |
set -e
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
else
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
fi
cat > RELEASE_BODY.md << 'EOF'
## Installation
```bash
curl -fsSL https://install.danklinux.com | sh
```
## Assets
### Complete Packages
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
### Individual Components
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-qml.tar.gz`** - QML source code only
### Checksums
- **`*.sha256`** - SHA256 checksums for verifying download integrity
**Installation:** Extract the `dms-full-*.tar.gz` package for your architecture and follow the `INSTALL.md` instructions inside.
---
EOF
cat >> RELEASE_BODY.md << EOF
## What's Changed
$CHANGELOG
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${TAG}
EOF
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Prepare release assets
run: |
set -euxo pipefail
mkdir -p _release_assets
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
for file in _core_assets/dms-*.gz*; do
if [ -f "$file" ]; then
basename=$(basename "$file")
if [[ "$basename" == dms-distropkg-* ]]; then
cp "$file" "_release_assets/$basename"
else
newname=$(echo "$basename" | sed 's/^dms-/dms-cli-/')
cp "$file" "_release_assets/$newname"
fi
fi
done
# Copy dankinstall binaries
cp _core_assets/dankinstall-*.gz* _release_assets/
# Copy completions
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
# Create QML source package (exclude build artifacts and git files)
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
cp LICENSE CONTRIBUTING.md quickshell/
# Tar the CONTENTS of quickshell/, not the directory itself
(cd quickshell && tar --exclude='.git' \
--exclude='.github' \
--exclude='*.tar.gz' \
-czf ../_release_assets/dms-qml.tar.gz .)
# Generate checksum for QML package
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
# Create full packages for each architecture
for arch in amd64 arm64; do
mkdir -p _temp_full/dms
mkdir -p _temp_full/bin
mkdir -p _temp_full/completions
# Extract QML source
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
# Add CLI binaries
if [ -f "_core_assets/dms-${arch}.gz" ]; then
gunzip -c "_core_assets/dms-${arch}.gz" > _temp_full/bin/dms
chmod +x _temp_full/bin/dms
fi
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
gunzip -c "_core_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
chmod +x _temp_full/bin/dms-distropkg
fi
# Add shell completions
for completion in _core_assets/completion.*; do
if [ -f "$completion" ]; then
cp "$completion" _temp_full/completions/
fi
done
# Copy docs directory
if [ -d "docs" ]; then
cp -r docs _temp_full/
fi
# Create installation guide
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
# DankMaterialShell Installation
## Requirements
- Wayland compositor (niri or Hyprland recommended)
- Quickshell framework
- Qt6
## Installation Steps
1. **Install quickshell assets:**
```bash
mkdir -p ~/.config/quickshell
cp -r dms ~/.config/quickshell/
```
2. **Install the DMS CLI binaries:**
```bash
sudo install -m 755 bin/dms /usr/local/bin/dms
```
3. **Install shell completions (optional):**
```bash
# Bash
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
# Fish
sudo install -m 644 completions/completion.fish /usr/share/fish/vendor_completions.d/dms.fish
# Zsh
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
```
4. **Start the shell:**
```bash
dms run
```
## Configuration
- Settings are stored in `~/.config/DankMaterialShell/settings.json`
- Plugins go in `~/.config/DankMaterialShell/plugins/`
- See the documentation in the `dms/` directory for more details
## Troubleshooting
- Run with verbose output: `DMS_LOG_LEVEL=debug dms run`
- Ensure all dependencies are installed
EOFINSTALL
# Create the full package
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
# Generate checksum
(cd _release_assets && sha256sum "dms-full-${arch}.tar.gz" > "dms-full-${arch}.tar.gz.sha256")
# Cleanup
rm -rf _temp_full
done
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
name: Release ${{ env.TAG }}
body: ${{ steps.changelog.outputs.changelog }}
files: _release_assets/**
draft: false
prerelease: ${{ contains(env.TAG, '-') }}
env:
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:
runs-on: ubuntu-latest
needs: release
env:
TAG: ${{ inputs.tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Determine version
id: version
run: |
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building DMS stable version: $VERSION"
- name: Setup build environment
run: |
sudo apt-get update
sudo apt-get install -y rpm wget curl jq gzip
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
- name: Download release assets
run: |
VERSION="${{ steps.version.outputs.version }}"
cd ~/rpmbuild/SOURCES
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1
}
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
# Spec for DMS stable releases - Generated by GitHub Actions
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
Name: dms
Version: %{version}
Release: 1%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli = %{version}-%{release}
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for the niri and hyprland compositors. Features notifications,
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%package -n dms-cli
Summary: DankMaterialShell CLI tool
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
%description -n dms-cli
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%prep
%setup -q -c -n dms-qml
# Download architecture-specific binaries during build
case "%{_arch}" in
x86_64)
ARCH_SUFFIX="amd64"
;;
aarch64)
ARCH_SUFFIX="arm64"
;;
*)
echo "Unsupported architecture: %{_arch}"
exit 1
;;
esac
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dms-cli for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
%build
%install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/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 || :
install -Dm644 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
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
# Signal running DMS instances to reload
pkill -USR1 -x dms >/dev/null 2>&1 || :
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%files -n dms-cli
%{_bindir}/dms
%{_datadir}/bash-completion/completions/dms
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
- name: Build SRPM
id: build
run: |
cd ~/rpmbuild/SPECS
rpmbuild -bs dms.spec
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
SRPM_NAME=$(basename "$SRPM")
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
echo "SRPM built: $SRPM_NAME"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
with:
name: dms-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI
run: |
sudo apt-get install -y python3-pip
pip3 install copr-cli
mkdir -p ~/.config
cat > ~/.config/copr << EOF
[copr-cli]
login = ${{ secrets.COPR_LOGIN }}
username = avengemedia
token = ${{ secrets.COPR_TOKEN }}
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
- name: Upload to Copr
run: |
SRPM="${{ steps.build.outputs.srpm_path }}"
VERSION="${{ steps.version.outputs.version }}"
echo "Uploading SRPM to avengemedia/dms..."
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
echo "$BUILD_OUTPUT"
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
if [ "$BUILD_ID" != "unknown" ]; then
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
fi
-288
View File
@@ -1,288 +0,0 @@
name: DMS Copr Stable Release
on:
workflow_dispatch:
inputs:
version:
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
required: false
default: ''
release:
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
required: false
default: '1'
jobs:
build-and-upload:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
# Get version from manual input or latest release
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
echo "Using manual version: $VERSION"
else
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
echo "Using latest release version: $VERSION"
fi
RELEASE="${{ github.event.inputs.release }}"
if [ -z "$RELEASE" ]; then
RELEASE="1"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release=$RELEASE" >> $GITHUB_OUTPUT
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
- name: Setup build environment
run: |
sudo apt-get update
sudo apt-get install -y rpm wget curl jq gzip
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
echo "✅ RPM build environment ready"
- name: Download release assets
run: |
VERSION="${{ steps.version.outputs.version }}"
cd ~/rpmbuild/SOURCES
echo "📦 Downloading DMS QML source for v${VERSION}..."
# Download DMS QML source
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
exit 1
}
echo "✅ Source downloaded"
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
ls -lh
- name: Generate stable spec file
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE="${{ steps.version.outputs.release }}"
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
# Spec for DMS stable releases - Generated by GitHub Actions
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
Name: dms
Version: %{version}
Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: (quickshell or quickshell-git)
Requires: accountsservice
Requires: dms-cli = %{version}-%{release}
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for the niri and hyprland compositors. Features notifications,
app launcher, wallpaper customization, and fully customizable with plugins.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%package -n dms-cli
Summary: DankMaterialShell CLI tool
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
%description -n dms-cli
Command-line interface for DankMaterialShell configuration and management.
Provides native DBus bindings, NetworkManager integration, and system utilities.
%prep
%setup -q -c -n dms-qml
# Download architecture-specific binaries during build
# This ensures the correct architecture is used for each build target
case "%{_arch}" in
x86_64)
ARCH_SUFFIX="amd64"
;;
aarch64)
ARCH_SUFFIX="arm64"
;;
*)
echo "Unsupported architecture: %{_arch}"
exit 1
;;
esac
# Download dms-cli for target architecture
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
echo "Failed to download dms-cli for architecture %{_arch}"
exit 1
}
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
chmod +x %{_builddir}/dms-cli
%build
%install
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
# Shell completions
install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/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 || :
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
%posttrans
# Clean up old installation path from previous versions (only if empty)
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
# Remove directories only if empty (preserves any user-added files)
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
# Signal running DMS instances to reload (harmless if none running)
pkill -USR1 -x dms >/dev/null 2>&1 || :
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%files -n dms-cli
%{_bindir}/dms
%{_datadir}/bash-completion/completions/dms
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/fish/vendor_completions.d/dms.fish
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER
- Built from GitHub release
SPECEOF
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
echo ""
echo "=== Spec file preview ==="
head -40 ~/rpmbuild/SPECS/dms.spec
- name: Build SRPM
id: build
run: |
cd ~/rpmbuild/SPECS
echo "🔨 Building SRPM..."
rpmbuild -bs dms.spec
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
SRPM_NAME=$(basename "$SRPM")
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
echo "✅ SRPM built: $SRPM_NAME"
echo ""
echo "=== SRPM Info ==="
rpm -qpi "$SRPM"
- name: Upload SRPM artifact
uses: actions/upload-artifact@v4
with:
name: dms-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }}
retention-days: 90
- name: Install Copr CLI
run: |
sudo apt-get install -y python3-pip
pip3 install copr-cli
mkdir -p ~/.config
cat > ~/.config/copr << EOF
[copr-cli]
login = ${{ secrets.COPR_LOGIN }}
username = avengemedia
token = ${{ secrets.COPR_TOKEN }}
copr_url = https://copr.fedorainfracloud.org
EOF
chmod 600 ~/.config/copr
echo "✅ Copr CLI configured"
- name: Upload to Copr
run: |
SRPM="${{ steps.build.outputs.srpm_path }}"
VERSION="${{ steps.version.outputs.version }}"
echo "🚀 Uploading SRPM to avengemedia/dms..."
echo " SRPM: $(basename $SRPM)"
echo " Version: $VERSION"
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
echo "$BUILD_OUTPUT"
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
if [ "$BUILD_ID" != "unknown" ]; then
echo "✅ Build submitted successfully!"
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
else
echo "⚠️ Could not extract build ID, but upload may have succeeded"
fi
- name: Build summary
if: always()
run: |
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
-276
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
-188
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
-66
View File
@@ -1,66 +0,0 @@
name: Update Vendor Hash
on:
workflow_dispatch:
push:
paths:
- "core/go.mod"
- "core/go.sum"
branches:
- master
permissions:
contents: write
jobs:
update-vendor-hash:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Update vendorHash in flake.nix
run: |
set -euo pipefail
echo "Attempting nix build to get new vendorHash..."
if output=$(nix build .#dmsCli 2>&1); then
echo "Build succeeded, no hash update needed"
exit 0
fi
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; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
echo "Verifying build with new vendorHash..."
nix build .#dmsCli
echo "vendorHash updated successfully!"
- name: Commit and push vendorHash update
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
if ! git diff --quiet flake.nix; then
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
else
echo "No changes to flake.nix"
fi
-115
View File
@@ -1,115 +0,0 @@
# C++ objects and libs
*.slo
*.lo
*.o
*.a
*.la
*.lai
*.so
*.so.*
*.dll
*.dylib
# Qt-es
object_script.*.Release
object_script.*.Debug
*_plugin_import.cpp
/.qmake.cache
/.qmake.stash
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.moc
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.qmlc
*.jsc
*build-*
*.qm
*.prl
# Qt unit tests
target_wrapper.*
# QtCreator
*.autosave
# QtCreator Qml
*.qmlproject.user
*.qmlproject.user.*
# QtCreator CMake
CMakeLists.txt.user*
# QtCreator 4.8< compilation database
compile_commands.json
# QtCreator local machine specific files for imported projects
*creator.user*
*_qmlcache.qrc
UNUSED
.qmlls.ini
CLAUDE-activeContext.md
CLAUDE-temp.md
# Auto-generated theme files
*.generated.*
niri-colors.generated.kdl
ghostty-colors.generated.conf
# Notepad files (should be in ~/.local/state/DankMaterialShell/)
untitled-*.txt
file:*
notepad-files/
result
# 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/
# 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
-67
View File
@@ -1,67 +0,0 @@
# Contributing
Contributions are welcome and encouraged.
To contribute fork this repository, make your changes, and open a pull request.
## Setup
Enable pre-commit hooks to catch CI failures before pushing:
```bash
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
{
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
}
```
3. Create empty `.qmlls.ini` file in `quickshell/` directory
```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
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Avenge Media LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-156
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"
-195
View File
@@ -1,195 +0,0 @@
# DankMaterialShell
<div align="center">
<a href="https://danklinux.com">
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
</a>
### A modern desktop shell for Wayland
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)
[![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 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 (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)
</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.
## Repository Structure
This is a monorepo containing both the shell interface and the core backend services:
```
DankMaterialShell/
├── quickshell/ # QML-based shell interface
│ ├── Modules/ # UI components (panels, widgets, overlays)
│ ├── Services/ # System integration (audio, network, bluetooth)
│ ├── Widgets/ # Reusable UI controls
│ └── Common/ # Shared resources and themes
├── core/ # Go backend and CLI
│ ├── cmd/ # dms CLI and dankinstall binaries
│ ├── internal/ # System integration, IPC, distro support
│ └── pkg/ # Shared packages
├── distro/ # Distribution packaging
│ ├── fedora/ # Fedora RPM specs
│ ├── debian/ # Debian packaging
│ └── nix/ # NixOS/home-manager modules
└── flake.nix # Nix flake for declarative installation
```
## See it in Action
<div align="center">
https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
</div>
<details><summary><strong>More Screenshots</strong></summary>
<div align="center">
<img src="https://github.com/user-attachments/assets/203a9678-c3b7-4720-bb97-853a511ac5c8" width="600" alt="Desktop" />
<img src="https://github.com/user-attachments/assets/a937cf35-a43b-4558-8c39-5694ff5fcac4" width="600" alt="Dashboard" />
<img src="https://github.com/user-attachments/assets/2da00ea1-8921-4473-a2a9-44a44535a822" width="450" alt="Launcher" />
<img src="https://github.com/user-attachments/assets/732c30de-5f4a-4a2b-a995-c8ab656cecd5" width="600" alt="Control Center" />
</div>
</details>
## Installation
```bash
curl -fsSL https://install.danklinux.com | sh
```
One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
**[Manual installation guide](https://danklinux.com/docs/dankmaterialshell/installation)**
## Features
**Dynamic Theming**
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
**System Monitoring**
Real-time CPU, RAM, GPU metrics and temperatures with [dgop](https://github.com/AvengeMedia/dgop). Process list with search and management.
**Powerful Launcher**
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
**Control Center**
Unified interface for network, Bluetooth, audio devices, display settings, and night mode.
**Smart Notifications**
Notification center with grouping, rich text support, and keyboard navigation.
**Media Integration**
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
**Plugin System**
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## 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.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
## Command Line Interface
Control the shell from the command line or keybinds:
```bash
dms run # Start the shell
dms ipc call spotlight toggle
dms ipc call audio setvolume 50
dms ipc call wallpaper set /path/to/image.jpg
dms brightness list # List available displays
dms plugins search # Browse plugin registry
```
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
## Documentation
- **Website:** [danklinux.com](https://danklinux.com)
- **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)
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
## Development
See component-specific documentation:
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
- **[core/](core/)** - Go backend, CLI tools, and system integration
- **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
### Building from Source
**Core + Dankinstall:**
```bash
cd core
make # Build dms CLI
make dankinstall # Build installer
```
**Shell:**
```bash
quickshell -p quickshell/
```
**NixOS:**
```nix
{
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
# Use in home-manager or NixOS configuration
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
}
```
## Contributing
Contributions welcome. Bug fixes, widgets, features, documentation, and plugins all help.
1. Fork the repository
2. Make your changes
3. Test thoroughly
4. Open a pull request
For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs).
## Credits
- [quickshell](https://quickshell.org/) - Shell framework
- [niri](https://github.com/YaLTeR/niri) - Scrolling window manager
- [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
- [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
## 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
MIT License - See [LICENSE](LICENSE) for details.
-113
View File
@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="482.90668"
height="558.15088"
viewBox="0 0 482.90667 558.15088"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
shape-rendering="auto"
style="image-rendering: auto; filter: url(#smoothing);">
<defs
id="defs1">
<filter id="smoothing" x="-0.05" y="-0.05" width="1.1" height="1.1">
<feGaussianBlur in="SourceGraphic" stdDeviation="0.5" />
</filter>
<color-profile
name="sRGB IEC61966-2.1"
xlink:href="data:application/vnd.iccprofile;base64,AAAMbExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
id="color-profile1" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-673.87432,-704.25842)"
id="path21" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath25">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-466.30451,-703.59782)"
id="path25" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath27">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-695.28002,-473.92741)"
id="path27" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath29">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-457.93881,-632.99062)"
id="path29" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath31">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-466.30451,-703.59782)"
id="path31" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath33">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-614.51722,-638.93302)"
id="path33" />
</clipPath>
</defs>
<g
id="layer-MC0"
transform="translate(-486.31024,-515.02722)">
<path
id="path20"
d="M 0,0 C -1.568,1.568 -3.163,3.098 -4.787,4.61 -5.944,3.966 -7.185,3.35 -8.529,2.762 -9.658,2.277 -10.815,1.82 -11.981,1.4 c 1.885,-1.689 3.742,-3.425 5.552,-5.207 0.448,-0.429 0.886,-0.868 1.325,-1.306 2.221,-2.221 4.386,-4.498 6.476,-6.84 27.639,-30.784 44.453,-71.459 44.453,-116.089 0,-29.347 -7.259,-56.977 -20.09,-81.21 -2.192,-4.134 -4.544,-8.174 -7.054,-12.102 -6.83,-10.74 -14.827,-20.669 -23.785,-29.636 -5.944,-5.944 -12.317,-11.459 -19.073,-16.498 -0.56,-0.42 -1.12,-0.83 -1.689,-1.231 -28.675,-20.893 -63.975,-33.201 -102.186,-33.201 -48.018,0 -91.464,19.456 -122.948,50.93 -0.737,0.737 -1.465,1.474 -2.174,2.221 -0.55,0.569 -1.101,1.147 -1.633,1.726 -15.545,16.553 -27.881,36.14 -36.018,57.779 -3.098,8.211 -5.58,16.712 -7.409,25.464 -2.417,11.534 -3.686,23.496 -3.686,35.758 0.01,42.326 15.117,81.097 40.246,111.246 -2.072,1.278 -3.975,2.809 -5.534,4.637 -26.174,-31.399 -41.934,-71.822 -41.934,-115.883 0,-18.187 2.678,-35.748 7.67,-52.311 3.359,-11.142 7.754,-21.835 13.092,-31.95 8.528,-16.208 19.446,-30.961 32.276,-43.801 1.251,-1.25 2.529,-2.491 3.817,-3.685 0.662,-0.644 1.334,-1.26 2.006,-1.876 10.862,-9.938 22.945,-18.578 36,-25.661 25.632,-13.913 55.016,-21.816 86.229,-21.816 2.454,0 4.908,0.047 7.334,0.14 36.056,1.446 69.424,13.427 97.054,32.976 0.569,0.392 1.148,0.793 1.698,1.204 7.82,5.655 15.164,11.925 21.966,18.718 7.904,7.904 15.07,16.526 21.406,25.773 2.556,3.723 4.973,7.558 7.25,11.477 15.499,26.697 24.382,57.723 24.382,90.812 C 53.038,-78.055 32.762,-32.762 0,0"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,898.49907,660.9888)"
clip-path="url(#clipPath21)" />
<path
id="path24"
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
clip-path="url(#clipPath25)" />
<path
id="path26"
d="m 0,0 c -6.336,-9.247 -13.502,-17.869 -21.406,-25.773 -6.802,-6.793 -14.146,-13.063 -21.965,-18.718 -0.551,-0.411 -1.129,-0.812 -1.699,-1.204 -27.629,-19.549 -60.998,-31.53 -97.053,-32.976 -33.247,1.129 -64.852,8.762 -93.564,21.676 -13.054,7.082 -25.138,15.723 -36,25.661 -0.672,0.616 -1.343,1.232 -2.006,1.876 -1.288,1.194 -2.566,2.435 -3.816,3.685 -12.831,12.84 -23.748,27.593 -32.277,43.801 -1.092,7.651 -1.941,15.378 -2.445,23.039 -0.102,1.502 -0.186,3.004 -0.261,4.497 0,0 -2.865,29.795 23.944,36.634 26.827,6.84 65.654,19.745 87.722,50.305 0,0 0.327,-8.38 5.506,-15.779 8.034,-11.422 46.674,-100.46 46.674,-100.46 l 6.121,51.135 -13.978,15.079 -4.553,-1.987 15.228,16.544 c 0,0 8.94,4.218 16.554,-0.653 7.605,-4.899 14.146,-16.153 14.146,-16.153 l -5.879,3.892 -4.227,-12.364 c -0.756,-2.184 -0.83,-4.535 -0.233,-6.784 l 16.796,-62.687 c 0,0 -0.243,87.415 -2.781,111.685 0,0 14.221,-10.367 21.621,-14.454 7.381,-4.078 47.215,-20.407 53.738,-22.395 6.504,-1.987 13.222,-5.841 15.882,-11.916 1.026,-2.351 8.594,-26.939 17.486,-56.229 C -1.829,6.028 -0.914,3.023 0,0"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,927.04,968.0968)"
clip-path="url(#clipPath27)" />
<path
id="path28"
d="m 0,0 c 0,0 -3.081,-67.22 -8.64,-90.616 -5.559,-23.397 30.316,0 30.316,0 l 20.9,68.629 z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,610.58507,756.01253)"
clip-path="url(#clipPath29)" />
<path
id="path30"
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
clip-path="url(#clipPath31)" />
<path
id="path32"
d="m 0,0 c 0,0 12.178,7.604 15.029,18.355 0,0 36.913,-9.243 52.904,-26.719 C 66.887,-8.091 29.642,2.031 0,0 m -36.152,-5.643 c -26.791,0.404 -31.781,16.05 -34.086,29.523 -0.511,2.958 -3.148,5.073 -6.13,4.883 l -46.275,-4.764 14.922,7.14 c 3.505,1.687 7.164,3.077 10.895,4.17 3.742,1.093 7.556,1.901 11.429,2.376 4.051,0.499 8.542,1.01 13.46,1.509 0,0 6.19,2.459 7.414,-4.479 1.259,-6.926 0.499,-31.864 28.287,-31.103 27.777,0.76 32.612,19.412 32.612,19.412 0,0 3.458,14.471 9.232,14.257 5.263,-0.202 7.27,-6.142 7.853,-10.479 0.142,-1.045 -0.06,-3.54 0.582,-4.324 -0.939,1.152 -2.162,2.032 -3.564,2.554 C 7.604,26.113 3.065,26.945 2.471,21.658 1.616,13.877 -9.599,-6.059 -36.152,-5.643 m 13.306,48.354 9.528,-2.377 c 0,0 -2.388,-17.5 -11.227,-21.979 -0.25,-0.13 4.194,14.281 1.699,24.356 m 59.022,3.041 3.79,-1.687 c 0,0 2.198,-14.209 -3.79,-20.577 0,0 2.174,12.474 0,22.264 m -187.451,35.63 c 12.225,2.067 24.45,4.229 36.664,6.332 l 36.663,6.392 73.303,12.748 c 2.958,0.522 5.774,-1.462 6.285,-4.408 0.522,-2.947 -1.462,-5.762 -4.42,-6.285 -0.119,-0.024 -0.261,-0.036 -0.38,-0.047 H -3.184 L -114.243,85.029 c -12.344,-1.224 -24.676,-2.495 -37.032,-3.647 M 43.97,16.93 c 3.54,4.693 5.215,23.096 5.215,23.096 l 1.497,2.518 v 8.744 C 29.381,61.327 11.869,50.682 11.869,50.682 l -16.491,0.749 c -3.112,0.142 -7.449,2.637 -10.644,3.433 -4.42,1.093 -8.804,2.068 -13.342,2.566 -16.407,1.735 -32.933,0 -49.091,-2.934 -7.021,-1.271 -13.971,-2.851 -20.957,-4.325 -6.701,-1.425 -12.878,-3.908 -19.151,-6.605 -4.313,-1.842 -8.649,-3.659 -12.641,-6.119 -2.436,-1.496 -4.741,-3.243 -6.737,-5.31 -2.126,-2.21 -3.659,-4.859 -5.476,-7.319 -1.545,-2.091 -4.907,-0.463 -4.147,2.032 0.024,0.059 0.048,0.119 0.06,0.154 0.867,2.044 2.221,4.646 3.659,6.345 3.588,4.241 8.958,8.292 13.686,11.12 10.039,6.071 21.48,9.766 32.767,12.985 24.771,7.057 51.442,8.138 77.009,10.55 14.114,1.331 28.43,2.091 42.473,4.016 12.784,1.734 37.935,4.859 36.176,22.882 -1.71,18.129 -59.355,18.712 -59.355,18.712 0,0 -21.943,45.027 -27.372,50.468 -5.418,5.418 -16.503,18.474 -74.254,4.384 -57.739,-14.078 -55.327,-42.259 -55.327,-42.259 l -2.412,-39.622 c 0,0 -43.198,-11.179 -41.832,-23.321 1.391,-12.142 39.171,-12.819 39.171,-12.819 0,0 8.577,0.439 20.696,1.152 l -5.382,-9.683 c -6.749,-12.13 -6.38,-25.258 -8.851,-38.908 -4.277,-23.631 11.963,-52.702 33.978,-62.147 21.1,-9.053 53.949,-13.782 99.012,6.368 11.085,4.954 20.328,13.282 26.66,23.618 l 0.499,0.796 c 22.728,26.125 80.574,9.386 80.574,9.386 C 81.774,1.699 43.97,16.93 43.97,16.93"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,819.35627,748.08933)"
clip-path="url(#clipPath33)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

-113
View File
@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="482.90668"
height="558.15088"
viewBox="0 0 482.90667 558.15088"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
shape-rendering="auto"
style="image-rendering: auto; filter: url(#smoothing);">
<defs
id="defs1">
<filter id="smoothing" x="-0.05" y="-0.05" width="1.1" height="1.1">
<feGaussianBlur in="SourceGraphic" stdDeviation="0.5" />
</filter>
<color-profile
name="sRGB IEC61966-2.1"
xlink:href="data:application/vnd.iccprofile;base64,AAAMbExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
id="color-profile1" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-673.87432,-704.25842)"
id="path21" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath25">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-466.30451,-703.59782)"
id="path25" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath27">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-695.28002,-473.92741)"
id="path27" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath29">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-457.93881,-632.99062)"
id="path29" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath31">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-466.30451,-703.59782)"
id="path31" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath33">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-614.51722,-638.93302)"
id="path33" />
</clipPath>
</defs>
<g
id="layer-MC0"
transform="translate(-486.31024,-515.02722)">
<path
id="path20"
d="M 0,0 C -1.568,1.568 -3.163,3.098 -4.787,4.61 -5.944,3.966 -7.185,3.35 -8.529,2.762 -9.658,2.277 -10.815,1.82 -11.981,1.4 c 1.885,-1.689 3.742,-3.425 5.552,-5.207 0.448,-0.429 0.886,-0.868 1.325,-1.306 2.221,-2.221 4.386,-4.498 6.476,-6.84 27.639,-30.784 44.453,-71.459 44.453,-116.089 0,-29.347 -7.259,-56.977 -20.09,-81.21 -2.192,-4.134 -4.544,-8.174 -7.054,-12.102 -6.83,-10.74 -14.827,-20.669 -23.785,-29.636 -5.944,-5.944 -12.317,-11.459 -19.073,-16.498 -0.56,-0.42 -1.12,-0.83 -1.689,-1.231 -28.675,-20.893 -63.975,-33.201 -102.186,-33.201 -48.018,0 -91.464,19.456 -122.948,50.93 -0.737,0.737 -1.465,1.474 -2.174,2.221 -0.55,0.569 -1.101,1.147 -1.633,1.726 -15.545,16.553 -27.881,36.14 -36.018,57.779 -3.098,8.211 -5.58,16.712 -7.409,25.464 -2.417,11.534 -3.686,23.496 -3.686,35.758 0.01,42.326 15.117,81.097 40.246,111.246 -2.072,1.278 -3.975,2.809 -5.534,4.637 -26.174,-31.399 -41.934,-71.822 -41.934,-115.883 0,-18.187 2.678,-35.748 7.67,-52.311 3.359,-11.142 7.754,-21.835 13.092,-31.95 8.528,-16.208 19.446,-30.961 32.276,-43.801 1.251,-1.25 2.529,-2.491 3.817,-3.685 0.662,-0.644 1.334,-1.26 2.006,-1.876 10.862,-9.938 22.945,-18.578 36,-25.661 25.632,-13.913 55.016,-21.816 86.229,-21.816 2.454,0 4.908,0.047 7.334,0.14 36.056,1.446 69.424,13.427 97.054,32.976 0.569,0.392 1.148,0.793 1.698,1.204 7.82,5.655 15.164,11.925 21.966,18.718 7.904,7.904 15.07,16.526 21.406,25.773 2.556,3.723 4.973,7.558 7.25,11.477 15.499,26.697 24.382,57.723 24.382,90.812 C 53.038,-78.055 32.762,-32.762 0,0"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,898.49907,660.9888)"
clip-path="url(#clipPath21)" />
<path
id="path24"
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
clip-path="url(#clipPath25)" />
<path
id="path26"
d="m 0,0 c -6.336,-9.247 -13.502,-17.869 -21.406,-25.773 -6.802,-6.793 -14.146,-13.063 -21.965,-18.718 -0.551,-0.411 -1.129,-0.812 -1.699,-1.204 -27.629,-19.549 -60.998,-31.53 -97.053,-32.976 -33.247,1.129 -64.852,8.762 -93.564,21.676 -13.054,7.082 -25.138,15.723 -36,25.661 -0.672,0.616 -1.343,1.232 -2.006,1.876 -1.288,1.194 -2.566,2.435 -3.816,3.685 -12.831,12.84 -23.748,27.593 -32.277,43.801 -1.092,7.651 -1.941,15.378 -2.445,23.039 -0.102,1.502 -0.186,3.004 -0.261,4.497 0,0 -2.865,29.795 23.944,36.634 26.827,6.84 65.654,19.745 87.722,50.305 0,0 0.327,-8.38 5.506,-15.779 8.034,-11.422 46.674,-100.46 46.674,-100.46 l 6.121,51.135 -13.978,15.079 -4.553,-1.987 15.228,16.544 c 0,0 8.94,4.218 16.554,-0.653 7.605,-4.899 14.146,-16.153 14.146,-16.153 l -5.879,3.892 -4.227,-12.364 c -0.756,-2.184 -0.83,-4.535 -0.233,-6.784 l 16.796,-62.687 c 0,0 -0.243,87.415 -2.781,111.685 0,0 14.221,-10.367 21.621,-14.454 7.381,-4.078 47.215,-20.407 53.738,-22.395 6.504,-1.987 13.222,-5.841 15.882,-11.916 1.026,-2.351 8.594,-26.939 17.486,-56.229 C -1.829,6.028 -0.914,3.023 0,0"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,927.04,968.0968)"
clip-path="url(#clipPath27)" />
<path
id="path28"
d="m 0,0 c 0,0 -3.081,-67.22 -8.64,-90.616 -5.559,-23.397 30.316,0 30.316,0 l 20.9,68.629 z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,610.58507,756.01253)"
clip-path="url(#clipPath29)" />
<path
id="path30"
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
clip-path="url(#clipPath31)" />
<path
id="path32"
d="m 0,0 c 0,0 12.178,7.604 15.029,18.355 0,0 36.913,-9.243 52.904,-26.719 C 66.887,-8.091 29.642,2.031 0,0 m -36.152,-5.643 c -26.791,0.404 -31.781,16.05 -34.086,29.523 -0.511,2.958 -3.148,5.073 -6.13,4.883 l -46.275,-4.764 14.922,7.14 c 3.505,1.687 7.164,3.077 10.895,4.17 3.742,1.093 7.556,1.901 11.429,2.376 4.051,0.499 8.542,1.01 13.46,1.509 0,0 6.19,2.459 7.414,-4.479 1.259,-6.926 0.499,-31.864 28.287,-31.103 27.777,0.76 32.612,19.412 32.612,19.412 0,0 3.458,14.471 9.232,14.257 5.263,-0.202 7.27,-6.142 7.853,-10.479 0.142,-1.045 -0.06,-3.54 0.582,-4.324 -0.939,1.152 -2.162,2.032 -3.564,2.554 C 7.604,26.113 3.065,26.945 2.471,21.658 1.616,13.877 -9.599,-6.059 -36.152,-5.643 m 13.306,48.354 9.528,-2.377 c 0,0 -2.388,-17.5 -11.227,-21.979 -0.25,-0.13 4.194,14.281 1.699,24.356 m 59.022,3.041 3.79,-1.687 c 0,0 2.198,-14.209 -3.79,-20.577 0,0 2.174,12.474 0,22.264 m -187.451,35.63 c 12.225,2.067 24.45,4.229 36.664,6.332 l 36.663,6.392 73.303,12.748 c 2.958,0.522 5.774,-1.462 6.285,-4.408 0.522,-2.947 -1.462,-5.762 -4.42,-6.285 -0.119,-0.024 -0.261,-0.036 -0.38,-0.047 H -3.184 L -114.243,85.029 c -12.344,-1.224 -24.676,-2.495 -37.032,-3.647 M 43.97,16.93 c 3.54,4.693 5.215,23.096 5.215,23.096 l 1.497,2.518 v 8.744 C 29.381,61.327 11.869,50.682 11.869,50.682 l -16.491,0.749 c -3.112,0.142 -7.449,2.637 -10.644,3.433 -4.42,1.093 -8.804,2.068 -13.342,2.566 -16.407,1.735 -32.933,0 -49.091,-2.934 -7.021,-1.271 -13.971,-2.851 -20.957,-4.325 -6.701,-1.425 -12.878,-3.908 -19.151,-6.605 -4.313,-1.842 -8.649,-3.659 -12.641,-6.119 -2.436,-1.496 -4.741,-3.243 -6.737,-5.31 -2.126,-2.21 -3.659,-4.859 -5.476,-7.319 -1.545,-2.091 -4.907,-0.463 -4.147,2.032 0.024,0.059 0.048,0.119 0.06,0.154 0.867,2.044 2.221,4.646 3.659,6.345 3.588,4.241 8.958,8.292 13.686,11.12 10.039,6.071 21.48,9.766 32.767,12.985 24.771,7.057 51.442,8.138 77.009,10.55 14.114,1.331 28.43,2.091 42.473,4.016 12.784,1.734 37.935,4.859 36.176,22.882 -1.71,18.129 -59.355,18.712 -59.355,18.712 0,0 -21.943,45.027 -27.372,50.468 -5.418,5.418 -16.503,18.474 -74.254,4.384 -57.739,-14.078 -55.327,-42.259 -55.327,-42.259 l -2.412,-39.622 c 0,0 -43.198,-11.179 -41.832,-23.321 1.391,-12.142 39.171,-12.819 39.171,-12.819 0,0 8.577,0.439 20.696,1.152 l -5.382,-9.683 c -6.749,-12.13 -6.38,-25.258 -8.851,-38.908 -4.277,-23.631 11.963,-52.702 33.978,-62.147 21.1,-9.053 53.949,-13.782 99.012,6.368 11.085,4.954 20.328,13.282 26.66,23.618 l 0.499,0.796 c 22.728,26.125 80.574,9.386 80.574,9.386 C 81.774,1.699 43.97,16.93 43.97,16.93"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,819.35627,748.08933)"
clip-path="url(#clipPath33)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

-10
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;
-17
View File
@@ -1,17 +0,0 @@
[Unit]
Description=Dank Material Shell (DMS)
PartOf=graphical-session.target
After=graphical-session.target
Requisite=graphical-session.target
[Service]
Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms
Restart=always
RestartSec=1.23
TimeoutStopSec=10
[Install]
WantedBy=graphical-session.target
-108
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"
-58
View File
@@ -1,58 +0,0 @@
with-expecter: true
dir: "internal/mocks/{{.InterfaceDirRelative}}"
mockname: "Mock{{.InterfaceName}}"
outpkg: "{{.PackageName}}"
packages:
github.com/Wifx/gonetworkmanager/v2:
interfaces:
NetworkManager:
Device:
DeviceWireless:
AccessPoint:
Connection:
Settings:
ActiveConnection:
IP4Config:
net:
interfaces:
Conn:
github.com/AvengeMedia/danklinux/internal/plugins:
interfaces:
GitClient:
github.com/godbus/dbus/v5:
interfaces:
BusObject:
github.com/AvengeMedia/danklinux/internal/server/brightness:
config:
dir: "internal/mocks/brightness"
outpkg: mocks_brightness
interfaces:
DBusConn:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config:
dir: "internal/mocks/network"
outpkg: mocks_network
interfaces:
Backend:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups:
config:
dir: "internal/mocks/cups"
outpkg: mocks_cups
interfaces:
CUPSClientInterface:
PkHelper:
config:
dir: "internal/mocks/cups_pkhelper"
outpkg: mocks_cups_pkhelper
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
config:
dir: "internal/mocks/evdev"
outpkg: mocks_evdev
interfaces:
EvdevDevice:
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
config:
dir: "internal/mocks/version"
outpkg: mocks_version
interfaces:
VersionFetcher:
-163
View File
@@ -1,163 +0,0 @@
BINARY_NAME=dms
BINARY_NAME_INSTALL=dankinstall
SOURCE_DIR=cmd/dms
SOURCE_DIR_INSTALL=cmd/dankinstall
BUILD_DIR=bin
PREFIX ?= /usr/local
INSTALL_DIR=$(PREFIX)/bin
GO=go
GOFLAGS=-ldflags="-s -w"
# Version and build info
BASE_VERSION=$(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")
COMMIT_COUNT=$(shell git rev-list --count HEAD 2>/dev/null || echo "0")
COMMIT_HASH=$(shell git rev-parse --short=8 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)'
# Architecture to build for dist target (amd64, arm64, or 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
# Default target
all: build
# Build the main binary (dms)
build:
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
dankinstall:
@echo "Building $(BINARY_NAME_INSTALL)..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
dist:
ifeq ($(ARCH),all)
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
@mkdir -p $(BUILD_DIR)
@echo "Building for linux/amd64..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
@echo "Building for linux/arm64..."
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
@echo "Distribution builds complete:"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
else
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
@mkdir -p $(BUILD_DIR)
@echo "Building for linux/$(ARCH)..."
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
@echo "Distribution build complete:"
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
endif
build-all: build dankinstall
install: build
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installation complete"
install-all: build-all
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete"
install-dankinstall: dankinstall
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete"
uninstall:
@echo "Uninstalling $(BINARY_NAME) from $(INSTALL_DIR)..."
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Uninstall complete"
uninstall-all:
@echo "Uninstalling $(BINARY_NAME) from $(INSTALL_DIR)..."
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Uninstalling $(BINARY_NAME_INSTALL) from $(INSTALL_DIR)..."
@rm -f $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Uninstall complete"
uninstall-dankinstall:
@echo "Uninstalling $(BINARY_NAME_INSTALL) from $(INSTALL_DIR)..."
@rm -f $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Uninstall complete"
clean:
@echo "Cleaning build artifacts..."
@rm -rf $(BUILD_DIR)
@echo "Clean complete"
test:
@echo "Running tests..."
$(GO) test -v ./...
fmt:
@echo "Formatting Go code..."
$(GO) fmt ./...
vet:
@echo "Running go vet..."
$(GO) vet ./...
deps:
@echo "Updating dependencies..."
$(GO) mod tidy
$(GO) mod download
dev:
@echo "Building $(BINARY_NAME) for development..."
@mkdir -p $(BUILD_DIR)
$(GO) build -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
@echo "Development build complete: $(BUILD_DIR)/$(BINARY_NAME)"
check-go:
@echo "Checking Go version..."
@go version | grep -E "go1\.(2[2-9]|[3-9][0-9])" > /dev/null || (echo "ERROR: Go 1.22 or higher required" && exit 1)
@echo "Go version OK"
version: check-go
@echo "Version: $(VERSION)"
@echo "Build Time: $(BUILD_TIME)"
@echo "Commit: $(COMMIT)"
print-version:
@echo "$(VERSION)"
help:
@echo "Available targets:"
@echo " all - Build the main binary (dms) (default)"
@echo " build - Build the main binary (dms)"
@echo " dankinstall - Build dankinstall binary"
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
@echo " build-all - Build both binaries"
@echo " install - Install dms to $(INSTALL_DIR)"
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
@echo " install-dankinstall - Install only dankinstall to $(INSTALL_DIR)"
@echo " uninstall - Remove dms from $(INSTALL_DIR)"
@echo " uninstall-all - Remove both binaries from $(INSTALL_DIR)"
@echo " uninstall-dankinstall - Remove only dankinstall from $(INSTALL_DIR)"
@echo " clean - Clean build artifacts"
@echo " test - Run tests"
@echo " fmt - Format Go code"
@echo " vet - Run go vet"
@echo " deps - Update dependencies"
@echo " dev - Build with debug info"
@echo " check-go - Check Go version compatibility"
@echo " version - Show version information"
@echo " help - Show this help message"
-142
View File
@@ -1,142 +0,0 @@
# DMS Backend & CLI
Go-based backend for DankMaterialShell providing system integration, IPC, and installation tools.
**See [root README](../README.md) for project overview and installation.**
## Components
**dms CLI**
Command-line interface and daemon for shell management and system control.
**dankinstall**
Distribution-aware installer with TUI for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo.
## System Integration
**Wayland Protocols**
- `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
- `ext-workspace-v1` - Workspace protocol support
- `wlr-output-management-unstable-v1` - Display configuration
**DBus Interfaces**
- NetworkManager/iwd - Network management
- logind - Session control and inhibit locks
- accountsservice - User account information
- CUPS - Printer management
- Custom IPC via unix socket (JSON API)
**Hardware Control**
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
- Backlight control - Internal display brightness via `login1` or sysfs
- LED control - Keyboard/device LED management
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
**Plugin System**
- Plugin registry integration
- Plugin lifecycle management
- Settings persistence
## CLI Commands
- `dms run [-d]` - Start shell (optionally as daemon)
- `dms restart` / `dms kill` - Manage running processes
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
- `dms plugins [install|browse|search]` - Plugin management
- `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages)
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
### 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
Requires Go 1.24+
**Development build:**
```bash
make # Build dms CLI
make dankinstall # Build installer
make test # Run tests
```
**Distribution build:**
```bash
make dist # Build without update/greeter features
```
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
**Installation:**
```bash
sudo make install # Install to /usr/local/bin/dms
```
## 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:**
```bash
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
-pkg wlr_gamma_control -o internal/proto/wlr_gamma_control/gamma_control.go
```
**Module Structure:**
- `cmd/` - Binary entrypoints (dms, dankinstall)
- `internal/distros/` - Distribution-specific installation logic
- `internal/proto/` - Wayland protocol bindings
- `pkg/` - Shared packages
## Installation via dankinstall
```bash
curl -fsSL https://install.danklinux.com | sh
```
## Supported Distributions
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
**Arch Linux**
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
**Fedora**
Uses COPR repositories (`avengemedia/danklinux`, `avengemedia/dms`).
**Ubuntu**
Requires PPA support. Most packages built from source (slow first install).
**Debian**
Debian 13+ (Trixie). niri only, no Hyprland support. Builds from source.
**openSUSE**
Most packages available in standard repos. Minimal building required.
**Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
See installer output for distribution-specific details during installation.
-46
View File
@@ -1,46 +0,0 @@
<svg viewBox="0 0 136 50" xmlns="http://www.w3.org/2000/svg">
<!-- D -->
<rect x="0" y="5" width="24" height="8" fill="#CCBEFF"/>
<rect x="0" y="13" width="8" height="8" fill="#CCBEFF"/>
<rect x="20" y="13" width="8" height="8" fill="#CCBEFF"/>
<rect x="0" y="21" width="8" height="8" fill="#CCBEFF"/>
<rect x="20" y="21" width="8" height="8" fill="#CCBEFF"/>
<rect x="0" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="20" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="0" y="37" width="24" height="8" fill="#CCBEFF"/>
<!-- A -->
<rect x="36" y="5" width="20" height="8" fill="#CCBEFF"/>
<rect x="32" y="13" width="8" height="8" fill="#CCBEFF"/>
<rect x="52" y="13" width="8" height="8" fill="#CCBEFF"/>
<rect x="32" y="21" width="28" height="8" fill="#CCBEFF"/>
<rect x="32" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="52" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="32" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="52" y="37" width="8" height="8" fill="#CCBEFF"/>
<!-- N -->
<rect x="64" y="5" width="12" height="8" fill="#CCBEFF"/>
<rect x="92" y="5" width="8" height="8" fill="#CCBEFF"/>
<rect x="64" y="13" width="16" height="8" fill="#CCBEFF"/>
<rect x="92" y="13" width="8" height="8" fill="#CCBEFF"/>
<rect x="64" y="21" width="8" height="8" fill="#CCBEFF"/>
<rect x="76" y="21" width="8" height="8" fill="#CCBEFF"/>
<rect x="92" y="21" width="8" height="8" fill="#CCBEFF"/>
<rect x="64" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="80" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="92" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="64" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="84" y="37" width="16" height="8" fill="#CCBEFF"/>
<!-- K -->
<rect x="104" y="5" width="8" height="8" fill="#CCBEFF"/>
<rect x="124" y="5" width="8" height="8" fill="#CCBEFF"/>
<rect x="104" y="13" width="8" height="8" fill="#CCBEFF"/>
<rect x="120" y="13" width="8" height="8" fill="#CCBEFF"/>
<rect x="104" y="21" width="20" height="8" fill="#CCBEFF"/>
<rect x="104" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="120" y="29" width="8" height="8" fill="#CCBEFF"/>
<rect x="104" y="37" width="8" height="8" fill="#CCBEFF"/>
<rect x="124" y="37" width="8" height="8" fill="#CCBEFF"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

-113
View File
@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="482.90668"
height="558.15088"
viewBox="0 0 482.90667 558.15088"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
shape-rendering="auto"
style="image-rendering: auto; filter: url(#smoothing);">
<defs
id="defs1">
<filter id="smoothing" x="-0.05" y="-0.05" width="1.1" height="1.1">
<feGaussianBlur in="SourceGraphic" stdDeviation="0.5" />
</filter>
<color-profile
name="sRGB IEC61966-2.1"
xlink:href="data:application/vnd.iccprofile;base64,AAAMbExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAACQd3RwdAAAAhQAAAAUYmtwdAAAAigAAAAUclhZWgAAAjwAAAAUZ1hZWgAAAlAAAAAUYlhZWgAAAmQAAAAUZG1uZAAAAngAAABwZG1kZAAAAugAAACIdnVlZAAAA3AAAACGdmlldwAAA/gAAAAkbHVtaQAABBwAAAAUbWVhcwAABDAAAAAkdGVjaAAABFQAAAAMclRSQwAABGAAAAgMZ1RSQwAABGAAAAgMYlRSQwAABGAAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAASAHMAUgBHAEIAIABJAEUAQwA2ADEAOQA2ADYALQAyAC4AMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//"
id="color-profile1" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-673.87432,-704.25842)"
id="path21" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath25">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-466.30451,-703.59782)"
id="path25" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath27">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-695.28002,-473.92741)"
id="path27" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath29">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-457.93881,-632.99062)"
id="path29" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath31">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-466.30451,-703.59782)"
id="path31" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath33">
<path
d="M 0,1200 H 2000 V 0 H 0 Z"
transform="translate(-614.51722,-638.93302)"
id="path33" />
</clipPath>
</defs>
<g
id="layer-MC0"
transform="translate(-486.31024,-515.02722)">
<path
id="path20"
d="M 0,0 C -1.568,1.568 -3.163,3.098 -4.787,4.61 -5.944,3.966 -7.185,3.35 -8.529,2.762 -9.658,2.277 -10.815,1.82 -11.981,1.4 c 1.885,-1.689 3.742,-3.425 5.552,-5.207 0.448,-0.429 0.886,-0.868 1.325,-1.306 2.221,-2.221 4.386,-4.498 6.476,-6.84 27.639,-30.784 44.453,-71.459 44.453,-116.089 0,-29.347 -7.259,-56.977 -20.09,-81.21 -2.192,-4.134 -4.544,-8.174 -7.054,-12.102 -6.83,-10.74 -14.827,-20.669 -23.785,-29.636 -5.944,-5.944 -12.317,-11.459 -19.073,-16.498 -0.56,-0.42 -1.12,-0.83 -1.689,-1.231 -28.675,-20.893 -63.975,-33.201 -102.186,-33.201 -48.018,0 -91.464,19.456 -122.948,50.93 -0.737,0.737 -1.465,1.474 -2.174,2.221 -0.55,0.569 -1.101,1.147 -1.633,1.726 -15.545,16.553 -27.881,36.14 -36.018,57.779 -3.098,8.211 -5.58,16.712 -7.409,25.464 -2.417,11.534 -3.686,23.496 -3.686,35.758 0.01,42.326 15.117,81.097 40.246,111.246 -2.072,1.278 -3.975,2.809 -5.534,4.637 -26.174,-31.399 -41.934,-71.822 -41.934,-115.883 0,-18.187 2.678,-35.748 7.67,-52.311 3.359,-11.142 7.754,-21.835 13.092,-31.95 8.528,-16.208 19.446,-30.961 32.276,-43.801 1.251,-1.25 2.529,-2.491 3.817,-3.685 0.662,-0.644 1.334,-1.26 2.006,-1.876 10.862,-9.938 22.945,-18.578 36,-25.661 25.632,-13.913 55.016,-21.816 86.229,-21.816 2.454,0 4.908,0.047 7.334,0.14 36.056,1.446 69.424,13.427 97.054,32.976 0.569,0.392 1.148,0.793 1.698,1.204 7.82,5.655 15.164,11.925 21.966,18.718 7.904,7.904 15.07,16.526 21.406,25.773 2.556,3.723 4.973,7.558 7.25,11.477 15.499,26.697 24.382,57.723 24.382,90.812 C 53.038,-78.055 32.762,-32.762 0,0"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,898.49907,660.9888)"
clip-path="url(#clipPath21)" />
<path
id="path24"
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
clip-path="url(#clipPath25)" />
<path
id="path26"
d="m 0,0 c -6.336,-9.247 -13.502,-17.869 -21.406,-25.773 -6.802,-6.793 -14.146,-13.063 -21.965,-18.718 -0.551,-0.411 -1.129,-0.812 -1.699,-1.204 -27.629,-19.549 -60.998,-31.53 -97.053,-32.976 -33.247,1.129 -64.852,8.762 -93.564,21.676 -13.054,7.082 -25.138,15.723 -36,25.661 -0.672,0.616 -1.343,1.232 -2.006,1.876 -1.288,1.194 -2.566,2.435 -3.816,3.685 -12.831,12.84 -23.748,27.593 -32.277,43.801 -1.092,7.651 -1.941,15.378 -2.445,23.039 -0.102,1.502 -0.186,3.004 -0.261,4.497 0,0 -2.865,29.795 23.944,36.634 26.827,6.84 65.654,19.745 87.722,50.305 0,0 0.327,-8.38 5.506,-15.779 8.034,-11.422 46.674,-100.46 46.674,-100.46 l 6.121,51.135 -13.978,15.079 -4.553,-1.987 15.228,16.544 c 0,0 8.94,4.218 16.554,-0.653 7.605,-4.899 14.146,-16.153 14.146,-16.153 l -5.879,3.892 -4.227,-12.364 c -0.756,-2.184 -0.83,-4.535 -0.233,-6.784 l 16.796,-62.687 c 0,0 -0.243,87.415 -2.781,111.685 0,0 14.221,-10.367 21.621,-14.454 7.381,-4.078 47.215,-20.407 53.738,-22.395 6.504,-1.987 13.222,-5.841 15.882,-11.916 1.026,-2.351 8.594,-26.939 17.486,-56.229 C -1.829,6.028 -0.914,3.023 0,0"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,927.04,968.0968)"
clip-path="url(#clipPath27)" />
<path
id="path28"
d="m 0,0 c 0,0 -3.081,-67.22 -8.64,-90.616 -5.559,-23.397 30.316,0 30.316,0 l 20.9,68.629 z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,610.58507,756.01253)"
clip-path="url(#clipPath29)" />
<path
id="path30"
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
clip-path="url(#clipPath31)" />
<path
id="path32"
d="m 0,0 c 0,0 12.178,7.604 15.029,18.355 0,0 36.913,-9.243 52.904,-26.719 C 66.887,-8.091 29.642,2.031 0,0 m -36.152,-5.643 c -26.791,0.404 -31.781,16.05 -34.086,29.523 -0.511,2.958 -3.148,5.073 -6.13,4.883 l -46.275,-4.764 14.922,7.14 c 3.505,1.687 7.164,3.077 10.895,4.17 3.742,1.093 7.556,1.901 11.429,2.376 4.051,0.499 8.542,1.01 13.46,1.509 0,0 6.19,2.459 7.414,-4.479 1.259,-6.926 0.499,-31.864 28.287,-31.103 27.777,0.76 32.612,19.412 32.612,19.412 0,0 3.458,14.471 9.232,14.257 5.263,-0.202 7.27,-6.142 7.853,-10.479 0.142,-1.045 -0.06,-3.54 0.582,-4.324 -0.939,1.152 -2.162,2.032 -3.564,2.554 C 7.604,26.113 3.065,26.945 2.471,21.658 1.616,13.877 -9.599,-6.059 -36.152,-5.643 m 13.306,48.354 9.528,-2.377 c 0,0 -2.388,-17.5 -11.227,-21.979 -0.25,-0.13 4.194,14.281 1.699,24.356 m 59.022,3.041 3.79,-1.687 c 0,0 2.198,-14.209 -3.79,-20.577 0,0 2.174,12.474 0,22.264 m -187.451,35.63 c 12.225,2.067 24.45,4.229 36.664,6.332 l 36.663,6.392 73.303,12.748 c 2.958,0.522 5.774,-1.462 6.285,-4.408 0.522,-2.947 -1.462,-5.762 -4.42,-6.285 -0.119,-0.024 -0.261,-0.036 -0.38,-0.047 H -3.184 L -114.243,85.029 c -12.344,-1.224 -24.676,-2.495 -37.032,-3.647 M 43.97,16.93 c 3.54,4.693 5.215,23.096 5.215,23.096 l 1.497,2.518 v 8.744 C 29.381,61.327 11.869,50.682 11.869,50.682 l -16.491,0.749 c -3.112,0.142 -7.449,2.637 -10.644,3.433 -4.42,1.093 -8.804,2.068 -13.342,2.566 -16.407,1.735 -32.933,0 -49.091,-2.934 -7.021,-1.271 -13.971,-2.851 -20.957,-4.325 -6.701,-1.425 -12.878,-3.908 -19.151,-6.605 -4.313,-1.842 -8.649,-3.659 -12.641,-6.119 -2.436,-1.496 -4.741,-3.243 -6.737,-5.31 -2.126,-2.21 -3.659,-4.859 -5.476,-7.319 -1.545,-2.091 -4.907,-0.463 -4.147,2.032 0.024,0.059 0.048,0.119 0.06,0.154 0.867,2.044 2.221,4.646 3.659,6.345 3.588,4.241 8.958,8.292 13.686,11.12 10.039,6.071 21.48,9.766 32.767,12.985 24.771,7.057 51.442,8.138 77.009,10.55 14.114,1.331 28.43,2.091 42.473,4.016 12.784,1.734 37.935,4.859 36.176,22.882 -1.71,18.129 -59.355,18.712 -59.355,18.712 0,0 -21.943,45.027 -27.372,50.468 -5.418,5.418 -16.503,18.474 -74.254,4.384 -57.739,-14.078 -55.327,-42.259 -55.327,-42.259 l -2.412,-39.622 c 0,0 -43.198,-11.179 -41.832,-23.321 1.391,-12.142 39.171,-12.819 39.171,-12.819 0,0 8.577,0.439 20.696,1.152 l -5.382,-9.683 c -6.749,-12.13 -6.38,-25.258 -8.851,-38.908 -4.277,-23.631 11.963,-52.702 33.978,-62.147 21.1,-9.053 53.949,-13.782 99.012,6.368 11.085,4.954 20.328,13.282 26.66,23.618 l 0.499,0.796 c 22.728,26.125 80.574,9.386 80.574,9.386 C 81.774,1.699 43.97,16.93 43.97,16.93"
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,819.35627,748.08933)"
clip-path="url(#clipPath33)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

-38
View File
@@ -1,38 +0,0 @@
#!/bin/bash
set -euo pipefail
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Get latest version tag
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo -e "${GREEN}Building dankinstall ${VERSION}${NC}"
# Create bin directory if it doesn't exist
mkdir -p bin
# Build for each architecture
for ARCH in amd64 arm64; do
echo -e "${BLUE}Building for ${ARCH}...${NC}"
cd cmd/dankinstall
GOOS=linux CGO_ENABLED=0 GOARCH=${ARCH} \
go build -trimpath -ldflags "-s -w -X main.Version=${VERSION}" \
-o ../../bin/dankinstall-${ARCH}
cd ../..
# Compress
gzip -9 -k -f bin/dankinstall-${ARCH}
# Generate checksum
(cd bin && sha256sum dankinstall-${ARCH}.gz > dankinstall-${ARCH}.gz.sha256)
echo -e "${GREEN}✓ Built bin/dankinstall-${ARCH}.gz${NC}"
done
echo -e "${GREEN}Done! Files ready in bin/:${NC}"
ls -lh bin/dankinstall-*
-55
View File
@@ -1,55 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
tea "github.com/charmbracelet/bubbletea"
)
var Version = "dev"
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()
if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err)
fmt.Println("Continuing without file logging...")
}
logFilePath := ""
if fileLogger != nil {
logFilePath = fileLogger.GetLogPath()
fmt.Printf("Logging to: %s\n", logFilePath)
defer func() {
if err := fileLogger.Close(); err != nil {
fmt.Printf("Warning: Failed to close log file: %v\n", err)
}
}()
}
model := tui.NewModel(Version, logFilePath)
if fileLogger != nil {
fileLogger.StartListening(model.GetLogChan())
}
p := tea.NewProgram(model, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Printf("Error running program: %v\n", err)
if logFilePath != "" {
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
}
os.Exit(1)
}
if logFilePath != "" {
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
}
}
-310
View File
@@ -1,310 +0,0 @@
package main
import (
"fmt"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
"github.com/spf13/cobra"
)
var brightnessCmd = &cobra.Command{
Use: "brightness",
Short: "Control device brightness",
Long: "Control brightness for backlight and LED devices (use --ddc to include DDC/I2C monitors)",
}
var brightnessListCmd = &cobra.Command{
Use: "list",
Short: "List all brightness devices",
Long: "List all available brightness devices with their current values",
Run: runBrightnessList,
}
var brightnessSetCmd = &cobra.Command{
Use: "set <device_id> <percent>",
Short: "Set brightness for a device",
Long: "Set brightness percentage (0-100) for a specific device",
Args: cobra.ExactArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
includeDDC, _ := cmd.Flags().GetBool("ddc")
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
},
Run: runBrightnessSet,
}
var brightnessGetCmd = &cobra.Command{
Use: "get <device_id>",
Short: "Get brightness for a device",
Long: "Get current brightness percentage for a specific device",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
includeDDC, _ := cmd.Flags().GetBool("ddc")
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
},
Run: runBrightnessGet,
}
func init() {
brightnessListCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)")
brightnessSetCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)")
brightnessSetCmd.Flags().Bool("exponential", false, "Use exponential brightness scaling")
brightnessSetCmd.Flags().Float64("exponent", 1.2, "Exponent for exponential scaling (default 1.2)")
brightnessGetCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)")
brightnessCmd.SetHelpTemplate(`{{.Long}}
Usage:
{{.UseLine}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .Name .NamePadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`)
brightnessListCmd.SetHelpTemplate(`{{.Long}}
Usage:
{{.UseLine}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
`)
brightnessSetCmd.SetHelpTemplate(`{{.Long}}
Usage:
{{.UseLine}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
`)
brightnessGetCmd.SetHelpTemplate(`{{.Long}}
Usage:
{{.UseLine}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
`)
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
}
func getAllBrightnessDevices(includeDDC bool) []brightness.Device {
allDevices := []brightness.Device{}
sysfs, err := brightness.NewSysfsBackend()
if err != nil {
log.Debugf("Failed to initialize sysfs backend: %v", err)
} else {
devices, err := sysfs.GetDevices()
if err != nil {
log.Debugf("Failed to get sysfs devices: %v", err)
} else {
allDevices = append(allDevices, devices...)
}
}
if includeDDC {
ddc, err := brightness.NewDDCBackend()
if err != nil {
fmt.Printf("Warning: Failed to initialize DDC backend: %v\n", err)
} else {
time.Sleep(100 * time.Millisecond)
devices, err := ddc.GetDevices()
if err != nil {
fmt.Printf("Warning: Failed to get DDC devices: %v\n", err)
} else {
allDevices = append(allDevices, devices...)
}
ddc.Close()
}
}
return allDevices
}
func runBrightnessList(cmd *cobra.Command, args []string) {
includeDDC, _ := cmd.Flags().GetBool("ddc")
allDevices := getAllBrightnessDevices(includeDDC)
if len(allDevices) == 0 {
fmt.Println("No brightness devices found")
return
}
maxIDLen := len("Device")
maxNameLen := len("Name")
for _, dev := range allDevices {
if len(dev.ID) > maxIDLen {
maxIDLen = len(dev.ID)
}
if len(dev.Name) > maxNameLen {
maxNameLen = len(dev.Name)
}
}
idPad := maxIDLen + 2
namePad := maxNameLen + 2
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
for i := 0; i < sepLen; i++ {
fmt.Print("─")
}
fmt.Println()
for _, device := range allDevices {
fmt.Printf("%-*s %-12s %-*s %3d%%\n",
idPad,
device.ID,
device.Class,
namePad,
device.Name,
device.CurrentPercent,
)
}
}
func runBrightnessSet(cmd *cobra.Command, args []string) {
deviceID := args[0]
var percent int
if _, err := fmt.Sscanf(args[1], "%d", &percent); err != nil {
log.Fatalf("Invalid percent value: %s", args[1])
}
if percent < 0 || percent > 100 {
log.Fatalf("Percent must be between 0 and 100")
}
includeDDC, _ := cmd.Flags().GetBool("ddc")
exponential, _ := cmd.Flags().GetBool("exponential")
exponent, _ := cmd.Flags().GetFloat64("exponent")
parts := strings.SplitN(deviceID, ":", 2)
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
if ok := tryLogindBrightness(parts[0], parts[1], deviceID, percent, exponential, exponent); ok {
return
}
}
sysfs, err := brightness.NewSysfsBackend()
if err == nil {
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return
}
log.Debugf("sysfs.SetBrightness failed: %v", err)
} else {
log.Debugf("NewSysfsBackend failed: %v", err)
}
// Try DDC if requested
if includeDDC {
ddc, err := brightness.NewDDCBackend()
if err == nil {
defer ddc.Close()
time.Sleep(100 * time.Millisecond)
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return
}
log.Debugf("ddc.SetBrightness failed: %v", err)
} else {
log.Debugf("NewDDCBackend failed: %v", err)
}
}
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) {
deviceID := args[0]
includeDDC, _ := cmd.Flags().GetBool("ddc")
allDevices := getAllBrightnessDevices(includeDDC)
for _, device := range allDevices {
if device.ID == deviceID {
fmt.Printf("%s: %d%% (%d/%d)\n",
device.ID,
device.CurrentPercent,
device.Current,
device.Max,
)
return
}
}
log.Fatalf("Device not found: %s", deviceID)
}
-133
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()
}
-517
View File
@@ -1,517 +0,0 @@
package main
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show version information",
Run: runVersion,
}
var runCmd = &cobra.Command{
Use: "run",
Short: "Launch quickshell with DMS configuration",
Long: "Launch quickshell with DMS configuration (qs -c dms)",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
daemon, _ := cmd.Flags().GetBool("daemon")
session, _ := cmd.Flags().GetBool("session")
if daemon {
runShellDaemon(session)
} else {
runShellInteractive(session)
}
},
}
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Restart quickshell with DMS configuration",
Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
restartShell()
},
}
var restartDetachedCmd = &cobra.Command{
Use: "restart-detached <pid>",
Hidden: true,
Args: cobra.ExactArgs(1),
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
runDetachedRestart(args[0])
},
}
var killCmd = &cobra.Command{
Use: "kill",
Short: "Kill running DMS shell processes",
Long: "Kill all running quickshell processes with DMS configuration",
Run: func(cmd *cobra.Command, args []string) {
killShell()
},
}
var ipcCmd = &cobra.Command{
Use: "ipc",
Short: "Send IPC commands to running DMS shell",
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
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) {
runShellIPCCommand(args)
},
}
var debugSrvCmd = &cobra.Command{
Use: "debug-srv",
Short: "Start the debug server",
Long: "Start the Unix socket debug server for DMS",
Run: func(cmd *cobra.Command, args []string) {
if err := startDebugServer(); err != nil {
log.Fatalf("Error starting debug server: %v", err)
}
},
}
var pluginsCmd = &cobra.Command{
Use: "plugins",
Short: "Manage DMS plugins",
Long: "Browse and manage DMS plugins from the registry",
}
var pluginsBrowseCmd = &cobra.Command{
Use: "browse",
Short: "Browse available plugins",
Long: "Browse available plugins from the DMS plugin registry",
Run: func(cmd *cobra.Command, args []string) {
if err := browsePlugins(); err != nil {
log.Fatalf("Error browsing plugins: %v", err)
}
},
}
var pluginsListCmd = &cobra.Command{
Use: "list",
Short: "List installed plugins",
Long: "List all installed DMS plugins",
Run: func(cmd *cobra.Command, args []string) {
if err := listInstalledPlugins(); err != nil {
log.Fatalf("Error listing plugins: %v", err)
}
},
}
var pluginsInstallCmd = &cobra.Command{
Use: "install <plugin-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.",
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) {
if err := installPluginCLI(args[0]); err != nil {
log.Fatalf("Error installing plugin: %v", err)
}
},
}
var pluginsUninstallCmd = &cobra.Command{
Use: "uninstall <plugin-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.",
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 := uninstallPluginCLI(args[0]); err != nil {
log.Fatalf("Error uninstalling plugin: %v", err)
}
},
}
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) {
printASCII()
fmt.Printf("%s\n", formatVersion(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 {
server.CLIVersion = Version
return server.Start(true)
}
func browsePlugins() error {
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
fmt.Println("Fetching plugin registry...")
pluginList, err := registry.List()
if err != nil {
return fmt.Errorf("failed to list plugins: %w", err)
}
if len(pluginList) == 0 {
fmt.Println("No plugins found in registry.")
return nil
}
fmt.Printf("\nAvailable Plugins (%d):\n\n", len(pluginList))
for _, plugin := range pluginList {
installed, _ := manager.IsInstalled(plugin)
installedMarker := ""
if installed {
installedMarker = " [Installed]"
}
fmt.Printf(" %s%s\n", plugin.Name, installedMarker)
fmt.Printf(" ID: %s\n", plugin.ID)
fmt.Printf(" Category: %s\n", plugin.Category)
fmt.Printf(" Author: %s\n", plugin.Author)
fmt.Printf(" Description: %s\n", plugin.Description)
fmt.Printf(" Repository: %s\n", plugin.Repo)
if len(plugin.Capabilities) > 0 {
fmt.Printf(" Capabilities: %s\n", strings.Join(plugin.Capabilities, ", "))
}
if len(plugin.Compositors) > 0 {
fmt.Printf(" Compositors: %s\n", strings.Join(plugin.Compositors, ", "))
}
if len(plugin.Dependencies) > 0 {
fmt.Printf(" Dependencies: %s\n", strings.Join(plugin.Dependencies, ", "))
}
fmt.Println()
}
return nil
}
func listInstalledPlugins() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installedNames, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
if len(installedNames) == 0 {
fmt.Println("No plugins installed.")
return nil
}
allPlugins, err := registry.List()
if err != nil {
return fmt.Errorf("failed to list plugins: %w", err)
}
pluginMap := make(map[string]plugins.Plugin)
for _, p := range allPlugins {
pluginMap[p.ID] = p
}
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok {
fmt.Printf(" %s\n", plugin.Name)
fmt.Printf(" ID: %s\n", plugin.ID)
fmt.Printf(" Category: %s\n", plugin.Category)
fmt.Printf(" Author: %s\n", plugin.Author)
fmt.Println()
} else {
fmt.Printf(" %s (not in registry)\n\n", id)
}
}
return nil
}
func installPluginCLI(idOrName string) error {
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
pluginList, err := registry.List()
if err != nil {
return fmt.Errorf("failed to list plugins: %w", err)
}
// First, try to find by ID (preferred method)
var plugin *plugins.Plugin
for _, p := range pluginList {
if p.ID == idOrName {
plugin = &p
break
}
}
// Fallback to name for backward compatibility
if plugin == nil {
for _, p := range pluginList {
if p.Name == idOrName {
plugin = &p
break
}
}
}
if plugin == nil {
return fmt.Errorf("plugin not found: %s", idOrName)
}
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if installed {
return fmt.Errorf("plugin already installed: %s", plugin.Name)
}
fmt.Printf("Installing plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Install(*plugin); err != nil {
return fmt.Errorf("failed to install plugin: %w", err)
}
fmt.Printf("Plugin installed successfully: %s\n", plugin.Name)
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 {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
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)
if err := manager.UninstallByIDOrName(idOrName); err != nil {
return err
}
fmt.Printf("Plugin uninstalled successfully: %s\n", idOrName)
return nil
}
func updatePluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
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("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 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
}
func getCommonCommands() []*cobra.Command {
return []*cobra.Command{
versionCmd,
runCmd,
restartCmd,
restartDetachedCmd,
killCmd,
ipcCmd,
debugSrvCmd,
pluginsCmd,
dank16Cmd,
brightnessCmd,
dpmsCmd,
keybindsCmd,
greeterCmd,
setupCmd,
colorCmd,
screenshotCmd,
notifyActionCmd,
matugenCmd,
}
}
-122
View File
@@ -1,122 +0,0 @@
package main
import (
"fmt"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var dank16Cmd = &cobra.Command{
Use: "dank16 [hex_color]",
Short: "Generate Base16 color palettes",
Long: "Generate Base16 color palettes from a color with support for various output formats",
Args: cobra.MaximumNArgs(1),
Run: runDank16,
}
func init() {
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant (sets default to light)")
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty 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("ghostty", false, "Output in Ghostty terminal format")
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
dank16Cmd.Flags().String("background", "", "Custom background color")
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) {
isLight, _ := cmd.Flags().GetBool("light")
isJson, _ := cmd.Flags().GetBool("json")
isKitty, _ := cmd.Flags().GetBool("kitty")
isFoot, _ := cmd.Flags().GetBool("foot")
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
isGhostty, _ := cmd.Flags().GetBool("ghostty")
isWezterm, _ := cmd.Flags().GetBool("wezterm")
background, _ := cmd.Flags().GetString("background")
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, "#") {
background = "#" + background
}
if primaryDark != "" && !strings.HasPrefix(primaryDark, "#") {
primaryDark = "#" + primaryDark
}
if primaryLight != "" && !strings.HasPrefix(primaryLight, "#") {
primaryLight = "#" + primaryLight
}
contrastAlgo = strings.ToLower(contrastAlgo)
if contrastAlgo != "dps" && contrastAlgo != "wcag" {
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{
IsLight: isLight,
Background: background,
UseDPS: contrastAlgo == "dps",
}
colors := dank16.GeneratePalette(primaryColor, opts)
if isJson {
fmt.Print(dank16.GenerateJSON(colors))
} else if isKitty {
fmt.Print(dank16.GenerateKittyTheme(colors))
} else if isFoot {
fmt.Print(dank16.GenerateFootTheme(colors))
} else if isAlacritty {
fmt.Print(dank16.GenerateAlacrittyTheme(colors))
} else if isGhostty {
fmt.Print(dank16.GenerateGhosttyTheme(colors))
} else if isWezterm {
fmt.Print(dank16.GenerateWeztermTheme(colors))
} else {
fmt.Print(dank16.GenerateGhosttyTheme(colors))
}
}
-105
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)
}
}
-466
View File
@@ -1,466 +0,0 @@
//go:build !distro_binary
package main
import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/spf13/cobra"
)
var updateCmd = &cobra.Command{
Use: "update",
Short: "Update DankMaterialShell to the latest version",
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
PreRunE: findConfig,
Run: func(cmd *cobra.Command, args []string) {
runUpdate()
},
}
var updateCheckCmd = &cobra.Command{
Use: "check",
Short: "Check if updates are available for DankMaterialShell",
Long: "Check for available updates without performing the actual update",
Run: func(cmd *cobra.Command, args []string) {
runUpdateCheck()
},
}
func runUpdateCheck() {
fmt.Println("Checking for DankMaterialShell updates...")
fmt.Println()
versionInfo, err := version.GetDMSVersionInfo()
if err != nil {
log.Fatalf("Error checking for updates: %v", err)
}
fmt.Printf("Current version: %s\n", versionInfo.Current)
fmt.Printf("Latest version: %s\n", versionInfo.Latest)
fmt.Println()
if versionInfo.HasUpdate {
fmt.Println("✓ Update available!")
fmt.Println()
fmt.Println("Run 'dms update' to install the latest version.")
os.Exit(0)
} else {
fmt.Println("✓ You are running the latest version.")
os.Exit(0)
}
}
func runUpdate() {
osInfo, err := distros.GetOSInfo()
if err != nil {
log.Fatalf("Error detecting OS: %v", err)
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
log.Fatalf("Unsupported distribution: %s", osInfo.Distribution.ID)
}
var updateErr error
switch config.Family {
case distros.FamilyArch:
updateErr = updateArchLinux()
case distros.FamilySUSE:
updateErr = updateOtherDistros()
default:
updateErr = updateOtherDistros()
}
if updateErr != nil {
if errors.Is(updateErr, errdefs.ErrUpdateCancelled) {
log.Info("Update cancelled.")
return
}
if errors.Is(updateErr, errdefs.ErrNoUpdateNeeded) {
return
}
log.Fatalf("Error updating DMS: %v", updateErr)
}
log.Info("Update complete! Restarting DMS...")
restartShell()
}
func updateArchLinux() error {
homeDir, err := os.UserHomeDir()
if err == nil {
dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms")
if _, err := os.Stat(dmsPath); err == nil {
return updateOtherDistros()
}
}
var packageName string
if isArchPackageInstalled("dms-shell-bin") {
packageName = "dms-shell-bin"
} else if isArchPackageInstalled("dms-shell-git") {
packageName = "dms-shell-git"
} else {
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
fmt.Println("Info: Falling back to git-based update method...")
return updateOtherDistros()
}
var helper string
var updateCmd *exec.Cmd
if utils.CommandExists("yay") {
helper = "yay"
updateCmd = exec.Command("yay", "-S", packageName)
} else if utils.CommandExists("paru") {
helper = "paru"
updateCmd = exec.Command("paru", "-S", packageName)
} else {
fmt.Println("Error: Neither yay nor paru found - please install an AUR helper")
fmt.Println("Info: Falling back to git-based update method...")
return updateOtherDistros()
}
fmt.Printf("This will update DankMaterialShell using %s.\n", helper)
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Printf("\nRunning: %s -S %s\n", helper, packageName)
updateCmd.Stdout = os.Stdout
updateCmd.Stderr = os.Stderr
err = updateCmd.Run()
if err != nil {
fmt.Printf("Error: Failed to update using %s: %v\n", helper, err)
}
fmt.Println("dms successfully updated")
return nil
}
func updateOtherDistros() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms")
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
return fmt.Errorf("DMS configuration directory not found at %s", dmsPath)
}
fmt.Printf("Found DMS configuration at %s\n", dmsPath)
versionInfo, err := version.GetDMSVersionInfo()
if err == nil && !versionInfo.HasUpdate {
fmt.Println()
fmt.Printf("Current version: %s\n", versionInfo.Current)
fmt.Printf("Latest version: %s\n", versionInfo.Latest)
fmt.Println()
fmt.Println("✓ You are already running the latest version.")
return errdefs.ErrNoUpdateNeeded
}
fmt.Println("\nThis will update:")
fmt.Println(" 1. The dms binary from GitHub releases")
fmt.Println(" 2. DankMaterialShell configuration using git")
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Println("\n=== Updating dms binary ===")
if err := updateDMSBinary(); err != nil {
fmt.Printf("Warning: Failed to update dms binary: %v\n", err)
fmt.Println("Continuing with shell configuration update...")
} else {
fmt.Println("dms binary successfully updated")
}
fmt.Println("\n=== Updating DMS shell configuration ===")
if err := os.Chdir(dmsPath); err != nil {
return fmt.Errorf("failed to change to DMS directory: %w", err)
}
statusCmd := exec.Command("git", "status", "--porcelain")
statusOutput, _ := statusCmd.Output()
hasLocalChanges := len(strings.TrimSpace(string(statusOutput))) > 0
currentRefCmd := exec.Command("git", "symbolic-ref", "-q", "HEAD")
currentRefOutput, _ := currentRefCmd.Output()
onBranch := len(currentRefOutput) > 0
var currentTag string
var currentBranch string
if !onBranch {
tagCmd := exec.Command("git", "describe", "--exact-match", "--tags", "HEAD")
if tagOutput, err := tagCmd.Output(); err == nil {
currentTag = strings.TrimSpace(string(tagOutput))
}
} else {
branchCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
if branchOutput, err := branchCmd.Output(); err == nil {
currentBranch = strings.TrimSpace(string(branchOutput))
}
}
fmt.Println("Fetching latest changes...")
fetchCmd := exec.Command("git", "fetch", "origin", "--tags", "--force")
fetchCmd.Stdout = os.Stdout
fetchCmd.Stderr = os.Stderr
if err := fetchCmd.Run(); err != nil {
return fmt.Errorf("failed to fetch changes: %w", err)
}
if currentTag != "" {
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
latestTagOutput, err := latestTagCmd.Output()
if err != nil {
return fmt.Errorf("failed to get latest tag: %w", err)
}
tags := strings.Split(strings.TrimSpace(string(latestTagOutput)), "\n")
if len(tags) == 0 || tags[0] == "" {
return fmt.Errorf("no version tags found")
}
latestTag := tags[0]
if latestTag == currentTag {
fmt.Printf("Already on latest tag: %s\n", currentTag)
return nil
}
fmt.Printf("Current tag: %s\n", currentTag)
fmt.Printf("Latest tag: %s\n", latestTag)
if hasLocalChanges {
fmt.Println("\nWarning: You have local changes in your DMS configuration.")
if offerReclone(dmsPath) {
return nil
}
return errdefs.ErrUpdateCancelled
}
fmt.Printf("Updating to %s...\n", latestTag)
checkoutCmd := exec.Command("git", "checkout", latestTag)
checkoutCmd.Stdout = os.Stdout
checkoutCmd.Stderr = os.Stderr
if err := checkoutCmd.Run(); err != nil {
fmt.Printf("Error: Failed to checkout %s: %v\n", latestTag, err)
if offerReclone(dmsPath) {
return nil
}
return fmt.Errorf("update cancelled")
}
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
return nil
}
if currentBranch == "" {
currentBranch = "master"
}
fmt.Printf("Current branch: %s\n", currentBranch)
if hasLocalChanges {
fmt.Println("\nWarning: You have local changes in your DMS configuration.")
if offerReclone(dmsPath) {
return nil
}
return errdefs.ErrUpdateCancelled
}
pullCmd := exec.Command("git", "pull", "origin", currentBranch)
pullCmd.Stdout = os.Stdout
pullCmd.Stderr = os.Stderr
if err := pullCmd.Run(); err != nil {
fmt.Printf("Error: Failed to pull latest changes: %v\n", err)
if offerReclone(dmsPath) {
return nil
}
return fmt.Errorf("update cancelled")
}
fmt.Println("\nUpdate complete!")
return nil
}
func offerReclone(dmsPath string) bool {
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil || !strings.HasPrefix(strings.ToLower(strings.TrimSpace(response)), "y") {
return false
}
timestamp := time.Now().Unix()
backupPath := fmt.Sprintf("%s.backup-%d", dmsPath, timestamp)
fmt.Printf("Backing up current directory to %s...\n", backupPath)
if err := os.Rename(dmsPath, backupPath); err != nil {
fmt.Printf("Error: Failed to backup directory: %v\n", err)
return false
}
fmt.Println("Cloning fresh copy...")
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
if err := cloneCmd.Run(); err != nil {
fmt.Printf("Error: Failed to clone repository: %v\n", err)
fmt.Printf("Restoring backup...\n")
os.Rename(backupPath, dmsPath)
return false
}
fmt.Printf("Successfully re-cloned repository (backup at %s)\n", backupPath)
return true
}
func confirmUpdate() bool {
fmt.Print("Do you want to proceed with the update? (y/N): ")
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("Error reading input: %v\n", err)
return false
}
response = strings.TrimSpace(strings.ToLower(response))
return response == "y" || response == "yes"
}
func updateDMSBinary() error {
arch := ""
switch strings.ToLower(os.Getenv("HOSTTYPE")) {
case "x86_64", "amd64":
arch = "amd64"
case "aarch64", "arm64":
arch = "arm64"
default:
cmd := exec.Command("uname", "-m")
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to detect architecture: %w", err)
}
archStr := strings.TrimSpace(string(output))
switch archStr {
case "x86_64":
arch = "amd64"
case "aarch64":
arch = "arm64"
default:
return fmt.Errorf("unsupported architecture: %s", archStr)
}
}
fmt.Println("Fetching latest release version...")
cmd := exec.Command("curl", "-s", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest")
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to fetch latest release: %w", err)
}
version := ""
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, "\"tag_name\"") {
parts := strings.Split(line, "\"")
if len(parts) >= 4 {
version = parts[3]
break
}
}
}
if version == "" {
return fmt.Errorf("could not determine latest version")
}
fmt.Printf("Latest version: %s\n", version)
tempDir, err := os.MkdirTemp("", "dms-update-*")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
binaryURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch)
checksumURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz.sha256", version, arch)
binaryPath := filepath.Join(tempDir, "dms.gz")
checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
fmt.Println("Downloading dms binary...")
downloadCmd := exec.Command("curl", "-L", binaryURL, "-o", binaryPath)
if err := downloadCmd.Run(); err != nil {
return fmt.Errorf("failed to download binary: %w", err)
}
fmt.Println("Downloading checksum...")
downloadCmd = exec.Command("curl", "-L", checksumURL, "-o", checksumPath)
if err := downloadCmd.Run(); err != nil {
return fmt.Errorf("failed to download checksum: %w", err)
}
fmt.Println("Verifying checksum...")
checksumData, err := os.ReadFile(checksumPath)
if err != nil {
return fmt.Errorf("failed to read checksum file: %w", err)
}
expectedChecksum := strings.Fields(string(checksumData))[0]
actualCmd := exec.Command("sha256sum", binaryPath)
actualOutput, err := actualCmd.Output()
if err != nil {
return fmt.Errorf("failed to calculate checksum: %w", err)
}
actualChecksum := strings.Fields(string(actualOutput))[0]
if expectedChecksum != actualChecksum {
return fmt.Errorf("checksum verification failed\nExpected: %s\nGot: %s", expectedChecksum, actualChecksum)
}
fmt.Println("Decompressing binary...")
decompressCmd := exec.Command("gunzip", binaryPath)
if err := decompressCmd.Run(); err != nil {
return fmt.Errorf("failed to decompress binary: %w", err)
}
decompressedPath := filepath.Join(tempDir, "dms")
if err := os.Chmod(decompressedPath, 0755); err != nil {
return fmt.Errorf("failed to make binary executable: %w", err)
}
currentPath, err := exec.LookPath("dms")
if err != nil {
return fmt.Errorf("could not find current dms binary: %w", err)
}
fmt.Printf("Installing to %s...\n", currentPath)
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
replaceCmd.Stdin = os.Stdin
replaceCmd.Stdout = os.Stdout
replaceCmd.Stderr = os.Stderr
if err := replaceCmd.Run(); err != nil {
return fmt.Errorf("failed to replace binary: %w", err)
}
return nil
}
-721
View File
@@ -1,721 +0,0 @@
package main
import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var greeterCmd = &cobra.Command{
Use: "greeter",
Short: "Manage DMS greeter",
Long: "Manage DMS greeter (greetd)",
}
var greeterInstallCmd = &cobra.Command{
Use: "install",
Short: "Install and configure DMS greeter",
Long: "Install greetd and configure it to use DMS as the greeter interface",
Run: func(cmd *cobra.Command, args []string) {
if err := installGreeter(); err != nil {
log.Fatalf("Error installing greeter: %v", err)
}
},
}
var greeterSyncCmd = &cobra.Command{
Use: "sync",
Short: "Sync DMS theme and settings with greeter",
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
Run: func(cmd *cobra.Command, args []string) {
if err := syncGreeter(); err != nil {
log.Fatalf("Error syncing greeter: %v", err)
}
},
}
var greeterEnableCmd = &cobra.Command{
Use: "enable",
Short: "Enable DMS greeter in greetd config",
Long: "Configure greetd to use DMS as the greeter",
Run: func(cmd *cobra.Command, args []string) {
if err := enableGreeter(); err != nil {
log.Fatalf("Error enabling greeter: %v", err)
}
},
}
var greeterStatusCmd = &cobra.Command{
Use: "status",
Short: "Check greeter sync status",
Long: "Check the status of greeter installation and configuration sync",
Run: func(cmd *cobra.Command, args []string) {
if err := checkGreeterStatus(); err != nil {
log.Fatalf("Error checking greeter status: %v", err)
}
},
}
func installGreeter() error {
fmt.Println("=== DMS Greeter Installation ===")
logFunc := func(msg string) {
fmt.Println(msg)
}
if err := greeter.EnsureGreetdInstalled(logFunc, ""); err != nil {
return err
}
fmt.Println("\nDetecting DMS installation...")
dmsPath, err := greeter.DetectDMSPath()
if err != nil {
return err
}
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
fmt.Println("\nDetecting installed compositors...")
compositors := greeter.DetectCompositors()
if len(compositors) == 0 {
return fmt.Errorf("no supported compositors found (niri or Hyprland required)")
}
var selectedCompositor string
if len(compositors) == 1 {
selectedCompositor = compositors[0]
fmt.Printf("✓ Found compositor: %s\n", selectedCompositor)
} else {
var err error
selectedCompositor, err = greeter.PromptCompositorChoice(compositors)
if err != nil {
return err
}
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
}
fmt.Println("\nSetting up dms-greeter group and permissions...")
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
return err
}
fmt.Println("\nCopying greeter files...")
if err := greeter.CopyGreeterFiles(dmsPath, selectedCompositor, logFunc, ""); err != nil {
return err
}
fmt.Println("\nConfiguring greetd...")
if err := greeter.ConfigureGreetd(dmsPath, selectedCompositor, logFunc, ""); err != nil {
return err
}
fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
return err
}
fmt.Println("\n=== Installation Complete ===")
fmt.Println("\nTo test the greeter, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nTo enable on boot, run:")
fmt.Println(" sudo systemctl enable --now greetd")
return nil
}
func syncGreeter() error {
fmt.Println("=== DMS Greeter Theme Sync ===")
fmt.Println()
logFunc := func(msg string) {
fmt.Println(msg)
}
fmt.Println("Detecting DMS installation...")
dmsPath, err := greeter.DetectDMSPath()
if err != nil {
return err
}
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
cacheDir := "/var/cache/dms-greeter"
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
}
greeterGroupExists := checkGroupExists("greeter")
if greeterGroupExists {
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
groupsCmd := exec.Command("groups", currentUser.Username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return fmt.Errorf("failed to check groups: %w", err)
}
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
if !inGreeterGroup {
fmt.Println("\n⚠ Warning: You are not in the greeter group.")
fmt.Print("Would you like to add your user to the greeter group? (y/N): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" {
fmt.Println("\nAdding user to greeter group...")
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username)
addUserCmd.Stdout = os.Stdout
addUserCmd.Stderr = os.Stderr
if err := addUserCmd.Run(); err != nil {
return fmt.Errorf("failed to add user to greeter group: %w", err)
}
fmt.Println("✓ User added to greeter group")
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
}
}
}
fmt.Println("\nSetting up permissions and ACLs...")
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
return err
}
fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
return err
}
fmt.Println("\n=== Sync Complete ===")
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
fmt.Println("The changes will be visible on the next login screen.")
return nil
}
func checkGroupExists(groupName string) bool {
data, err := os.ReadFile("/etc/group")
if err != nil {
return false
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, groupName+":") {
return true
}
}
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 {
fmt.Println("=== DMS Greeter Enable ===")
fmt.Println()
configPath := "/etc/greetd/config.toml"
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath)
}
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read greetd config: %w", err)
}
configContent := string(data)
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
if configAlreadyCorrect {
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
}
fmt.Println("Detecting installed compositors...")
compositors := greeter.DetectCompositors()
if utils.CommandExists("sway") {
compositors = append(compositors, "sway")
}
if len(compositors) == 0 {
return fmt.Errorf("no supported compositors found (niri, Hyprland, or sway required)")
}
var selectedCompositor string
if len(compositors) == 1 {
selectedCompositor = compositors[0]
fmt.Printf("✓ Found compositor: %s\n", selectedCompositor)
} else {
var err error
selectedCompositor, err = promptCompositorChoice(compositors)
if err != nil {
return err
}
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
}
backupPath := configPath + ".backup"
backupCmd := exec.Command("sudo", "cp", configPath, backupPath)
if err := backupCmd.Run(); err != nil {
return fmt.Errorf("failed to backup config: %w", err)
}
fmt.Printf("✓ Backed up config to %s\n", backupPath)
lines := strings.Split(configContent, "\n")
var newLines []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
newLines = append(newLines, line)
}
}
wrapperCmd, err := findCommandPath("dms-greeter")
if err != nil {
return fmt.Errorf("dms-greeter not found in PATH. Please ensure it is installed and accessible")
}
compositorLower := strings.ToLower(selectedCompositor)
commandLine := fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
var finalLines []string
inDefaultSession := false
commandAdded := false
for _, line := range newLines {
finalLines = append(finalLines, line)
trimmed := strings.TrimSpace(line)
if trimmed == "[default_session]" {
inDefaultSession = true
}
if inDefaultSession && !commandAdded {
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
finalLines = append(finalLines, commandLine)
commandAdded = true
}
}
}
if !commandAdded {
finalLines = append(finalLines, commandLine)
}
newConfig := strings.Join(finalLines, "\n")
tmpFile := "/tmp/greetd-config.toml"
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
return fmt.Errorf("failed to write temp config: %w", err)
}
moveCmd := exec.Command("sudo", "mv", tmpFile, configPath)
if err := moveCmd.Run(); err != nil {
return fmt.Errorf("failed to update config: %w", err)
}
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("\nTo start the greeter now, run:")
fmt.Println(" sudo systemctl start greetd")
fmt.Println("\nOr reboot to see the greeter at boot time.")
return nil
}
func promptCompositorChoice(compositors []string) (string, error) {
fmt.Println("\nMultiple compositors detected:")
for i, comp := range compositors {
fmt.Printf("%d) %s\n", i+1, comp)
}
var response string
fmt.Print("Choose compositor for greeter: ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
choice := 0
fmt.Sscanf(response, "%d", &choice)
if choice < 1 || choice > len(compositors) {
return "", fmt.Errorf("invalid choice")
}
return compositors[choice-1], nil
}
func checkGreeterStatus() error {
fmt.Println("=== DMS Greeter Status ===")
fmt.Println()
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
configPath := "/etc/greetd/config.toml"
fmt.Println("Greeter Configuration:")
if data, err := os.ReadFile(configPath); err == nil {
configContent := string(data)
if strings.Contains(configContent, "dms-greeter") {
lines := strings.Split(configContent, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) == 2 {
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
fmt.Println(" ✓ Greeter is enabled")
if strings.Contains(command, "--command niri") {
fmt.Println(" Compositor: niri")
} else if strings.Contains(command, "--command hyprland") {
fmt.Println(" Compositor: Hyprland")
} else if strings.Contains(command, "--command sway") {
fmt.Println(" Compositor: sway")
} else {
fmt.Println(" Compositor: unknown")
}
}
break
}
}
} else {
fmt.Println(" ✗ Greeter is NOT enabled")
fmt.Println(" Run 'dms greeter enable' to enable it")
}
} else {
fmt.Println(" ✗ Greeter config not found")
fmt.Println(" Run 'dms greeter install' to install greeter")
}
fmt.Println("\nGroup Membership:")
groupsCmd := exec.Command("groups", currentUser.Username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return fmt.Errorf("failed to check groups: %w", err)
}
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
if inGreeterGroup {
fmt.Println(" ✓ User is in greeter group")
} else {
fmt.Println(" ✗ User is NOT in greeter group")
fmt.Println(" Run 'dms greeter install' to add user to greeter group")
}
cacheDir := "/var/cache/dms-greeter"
fmt.Println("\nGreeter Cache Directory:")
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
fmt.Printf(" ✓ %s exists\n", cacheDir)
} else {
fmt.Printf(" ✗ %s not found\n", cacheDir)
fmt.Println(" Run 'dms greeter install' to create cache directory")
return nil
}
fmt.Println("\nConfiguration Symlinks:")
symlinks := []struct {
source string
target string
desc string
}{
{
source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"),
target: filepath.Join(cacheDir, "settings.json"),
desc: "Settings",
},
{
source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"),
target: filepath.Join(cacheDir, "session.json"),
desc: "Session state",
},
{
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
target: filepath.Join(cacheDir, "colors.json"),
desc: "Color theme",
},
}
allGood := true
for _, link := range symlinks {
targetInfo, err := os.Lstat(link.target)
if err != nil {
fmt.Printf(" ✗ %s: symlink not found at %s\n", link.desc, link.target)
allGood = false
continue
}
if targetInfo.Mode()&os.ModeSymlink == 0 {
fmt.Printf(" ✗ %s: %s is not a symlink\n", link.desc, link.target)
allGood = false
continue
}
linkDest, err := os.Readlink(link.target)
if err != nil {
fmt.Printf(" ✗ %s: failed to read symlink\n", link.desc)
allGood = false
continue
}
if linkDest != link.source {
fmt.Printf(" ✗ %s: symlink points to wrong location\n", link.desc)
fmt.Printf(" Expected: %s\n", link.source)
fmt.Printf(" Got: %s\n", linkDest)
allGood = false
continue
}
if _, err := os.Stat(link.source); os.IsNotExist(err) {
fmt.Printf(" ⚠ %s: symlink OK, but source file doesn't exist yet\n", link.desc)
fmt.Printf(" Will be created when you run DMS\n")
continue
}
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
}
fmt.Println()
if allGood && inGreeterGroup {
fmt.Println("✓ All checks passed! Greeter is properly configured.")
} else if !allGood {
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to fix symlinks.")
}
return nil
}
-233
View File
@@ -1,233 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var keybindsCmd = &cobra.Command{
Use: "keybinds",
Aliases: []string{"cheatsheet", "chsht"},
Short: "Manage keybinds and cheatsheets",
Long: "Display and manage keybinds and cheatsheets for various applications",
}
var keybindsListCmd = &cobra.Command{
Use: "list",
Short: "List available providers",
Long: "List all available keybind/cheatsheet providers",
Run: runKeybindsList,
}
var keybindsShowCmd = &cobra.Command{
Use: "show <provider>",
Short: "Show keybinds for a provider",
Long: "Display keybinds/cheatsheet for the specified provider",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
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() {
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(keybindsShowCmd)
keybindsCmd.AddCommand(keybindsSetCmd)
keybindsCmd.AddCommand(keybindsRemoveCmd)
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
return providers.NewJSONFileProvider(filePath)
})
initializeProviders()
}
func initializeProviders() {
registry := keybinds.GetDefaultRegistry()
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
if err := registry.Register(hyprlandProvider); err != nil {
log.Warnf("Failed to register Hyprland provider: %v", err)
}
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
if err := registry.Register(mangowcProvider); err != nil {
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")
if err := registry.Register(swayProvider); err != nil {
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()
if err := keybinds.AutoDiscoverProviders(registry, config); err != nil {
log.Warnf("Failed to auto-discover providers: %v", err)
}
}
func runKeybindsList(_ *cobra.Command, _ []string) {
providerList := keybinds.GetDefaultRegistry().List()
if len(providerList) == 0 {
fmt.Fprintln(os.Stdout, "No providers available")
return
}
fmt.Fprintln(os.Stdout, "Available providers:")
for _, name := range providerList {
fmt.Fprintf(os.Stdout, " - %s\n", name)
}
}
func makeProviderWithPath(name, path string) keybinds.Provider {
switch name {
case "hyprland":
return providers.NewHyprlandProvider(path)
case "mangowc":
return providers.NewMangoWCProvider(path)
case "sway":
return providers.NewSwayProvider(path)
case "scroll":
return providers.NewSwayProvider(path)
case "niri":
return providers.NewNiriProvider(path)
default:
return nil
}
}
func printCheatSheet(provider keybinds.Provider) {
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))
}
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))
}
-182
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")
}
}
-227
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")
}
-90
View File
@@ -1,90 +0,0 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
)
var customConfigPath string
var configPath string
var rootCmd = &cobra.Command{
Use: "dms",
Short: "dms CLI",
Long: "dms is the DankMaterialShell management CLI and backend server.",
Run: runInteractiveMode,
}
func init() {
// Add the -c flag
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
}
func findConfig(cmd *cobra.Command, args []string) error {
if customConfigPath != "" {
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
shellPath := filepath.Join(customConfigPath, "shell.qml")
info, statErr := os.Stat(shellPath)
if statErr == nil && !info.IsDir() {
configPath = customConfigPath
log.Debug("Using config from: %s", configPath)
return nil // <-- Guard statement
}
if statErr != nil {
return fmt.Errorf("custom config path error: %w", statErr)
}
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
}
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
statePath := strings.TrimSpace(string(data))
shellPath := filepath.Join(statePath, "shell.qml")
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
log.Debug("Using config from active session state file: %s", statePath)
configPath = statePath
log.Debug("Using config from: %s", configPath)
return nil // <-- Guard statement
} else {
os.Remove(configStateFile)
}
}
log.Debug("No custom path or active session, searching default XDG locations...")
var err error
configPath, err = config.LocateDMSConfig()
if err != nil {
return err
}
log.Debug("Using config from: %s", configPath)
return nil
}
func runInteractiveMode(cmd *cobra.Command, args []string) {
detector, _ := dms.NewDetector()
if !detector.IsDMSInstalled() {
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
log.Info("Please install DMS using dankinstall before using this management interface.")
os.Exit(1)
}
model := dms.NewModel(Version)
p := tea.NewProgram(model, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
log.Fatalf("Error running program: %v", err)
}
}
-414
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)
}
}
-196
View File
@@ -1,196 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var setupCmd = &cobra.Command{
Use: "setup",
Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts",
Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err)
}
},
}
func runSetup() error {
fmt.Println("=== DMS Configuration Setup ===")
wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.")
return nil
}
if wmSelected || terminalSelected {
willBackup := checkExistingConfigs(wm, wmSelected, terminal, terminalSelected)
if willBackup {
fmt.Println("\n⚠ Existing configurations will be backed up with timestamps.")
}
fmt.Print("\nProceed with deployment? (y/N): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response != "y" && response != "yes" {
fmt.Println("Setup cancelled.")
return nil
}
}
fmt.Println("\nDeploying configurations...")
logChan := make(chan string, 100)
deployer := config.NewConfigDeployer(logChan)
go func() {
for msg := range logChan {
fmt.Println(" " + msg)
}
}()
ctx := context.Background()
var results []config.DeploymentResult
var err error
if wmSelected && terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
} else if wmSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
if len(results) > 1 {
results = results[:1]
}
} else if terminalSelected {
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
if len(results) > 0 && results[0].ConfigType == "Niri" {
results = results[1:]
}
}
close(logChan)
if err != nil {
return fmt.Errorf("deployment failed: %w", err)
}
fmt.Println("\n=== Deployment Complete ===")
for _, result := range results {
if result.Deployed {
fmt.Printf("✓ %s: %s\n", result.ConfigType, result.Path)
if result.BackupPath != "" {
fmt.Printf(" Backup: %s\n", result.BackupPath)
}
}
}
return nil
}
func promptCompositor() (deps.WindowManager, bool) {
fmt.Println("Select compositor:")
fmt.Println("1) Niri")
fmt.Println("2) Hyprland")
fmt.Println("3) None")
var response string
fmt.Print("\nChoice (1-3): ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
switch response {
case "1":
return deps.WindowManagerNiri, true
case "2":
return deps.WindowManagerHyprland, true
default:
return deps.WindowManagerNiri, false
}
}
func promptTerminal() (deps.Terminal, bool) {
fmt.Println("\nSelect terminal:")
fmt.Println("1) Ghostty")
fmt.Println("2) Kitty")
fmt.Println("3) Alacritty")
fmt.Println("4) None")
var response string
fmt.Print("\nChoice (1-4): ")
fmt.Scanln(&response)
response = strings.TrimSpace(response)
switch response {
case "1":
return deps.TerminalGhostty, true
case "2":
return deps.TerminalKitty, true
case "3":
return deps.TerminalAlacritty, true
default:
return deps.TerminalGhostty, false
}
}
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 {
homeDir := os.Getenv("HOME")
willBackup := false
if wmSelected {
var configPath string
switch wm {
case deps.WindowManagerNiri:
configPath = filepath.Join(homeDir, ".config", "niri", "config.kdl")
case deps.WindowManagerHyprland:
configPath = filepath.Join(homeDir, ".config", "hypr", "hyprland.conf")
}
if _, err := os.Stat(configPath); err == nil {
willBackup = true
}
}
if terminalSelected {
var configPath string
switch terminal {
case deps.TerminalGhostty:
configPath = filepath.Join(homeDir, ".config", "ghostty", "config")
case deps.TerminalKitty:
configPath = filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
case deps.TerminalAlacritty:
configPath = filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
}
if _, err := os.Stat(configPath); err == nil {
willBackup = true
}
}
return willBackup
}
-345
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
}
}
-44
View File
@@ -1,44 +0,0 @@
//go:build !distro_binary
package main
import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
var Version = "dev"
func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd)
rootCmd.SetHelpTemplate(getHelpTemplate())
}
func main() {
if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.")
}
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
-41
View File
@@ -1,41 +0,0 @@
//go:build distro_binary
package main
import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
var Version = "dev"
func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate())
}
func main() {
// Block root
if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.")
}
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
-561
View File
@@ -1,561 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
)
type ipcTargets map[string]map[string][]string
var isSessionManaged bool
func execDetachedRestart(targetPID int) {
selfPath, err := os.Executable()
if err != nil {
return
}
cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
cmd.Start()
}
func runDetachedRestart(targetPIDStr string) {
targetPID, err := strconv.Atoi(targetPIDStr)
if err != nil {
return
}
time.Sleep(200 * time.Millisecond)
proc, err := os.FindProcess(targetPID)
if err == nil {
proc.Signal(syscall.SIGTERM)
}
time.Sleep(500 * time.Millisecond)
killShell()
runShellDaemon(false)
}
func getRuntimeDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
return runtime
}
return os.TempDir()
}
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
}
func writePIDFile(childPID int) error {
pidFile := getPIDFilePath()
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
}
func removePIDFile() {
pidFile := getPIDFilePath()
os.Remove(pidFile)
}
func getAllDMSPIDs() []int {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var pids []int
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
pidFile := filepath.Join(dir, entry.Name())
data, err := os.ReadFile(pidFile)
if err != nil {
continue
}
childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
os.Remove(pidFile)
continue
}
proc, err := os.FindProcess(childPID)
if err != nil {
os.Remove(pidFile)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
os.Remove(pidFile)
continue
}
pids = append(pids, childPID)
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
if parentProc, err := os.FindProcess(parentPID); err == nil {
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
pids = append(pids, parentPID)
}
}
}
}
return pids
}
func runShellInteractive(session bool) {
isSessionManaged = session
go printASCII()
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
errChan := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
}()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
}
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting quickshell: %v", err)
}
// Write PID file for the quickshell child process
if err := writePIDFile(cmd.Process.Pid); err != nil {
log.Warnf("Failed to write PID file: %v", err)
}
defer removePIDFile()
defer func() {
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
if err := cmd.Wait(); err != nil {
errChan <- fmt.Errorf("quickshell exited: %w", err)
} else {
errChan <- fmt.Errorf("quickshell exited")
}
}()
for {
select {
case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes
if sig == syscall.SIGUSR1 && !isSessionManaged {
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
return
}
log.Infof("\nReceived signal %v, shutting down...", sig)
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
return
case err := <-errChan:
log.Error(err)
cancel()
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(1)
}
}
}
func restartShell() {
pids := getAllDMSPIDs()
if len(pids) == 0 {
log.Info("No running DMS shell instances found. Starting daemon...")
runShellDaemon(false)
return
}
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("Error finding process %d: %v", pid, err)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
if err := proc.Signal(syscall.SIGUSR1); err != nil {
log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
} else {
log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
}
}
}
func killShell() {
pids := getAllDMSPIDs()
if len(pids) == 0 {
log.Info("No running DMS shell instances found.")
return
}
currentPid := os.Getpid()
uniquePids := make(map[int]bool)
for _, pid := range pids {
if pid != currentPid {
uniquePids[pid] = true
}
}
for pid := range uniquePids {
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("Error finding process %d: %v", pid, err)
continue
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
continue
}
if err := proc.Kill(); err != nil {
log.Errorf("Error killing process %d: %v", pid, err)
} else {
log.Infof("Killed DMS process with PID %d", pid)
}
}
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), "danklinux-") && strings.HasSuffix(entry.Name(), ".pid") {
pidFile := filepath.Join(dir, entry.Name())
os.Remove(pidFile)
}
}
}
func runShellDaemon(session bool) {
isSessionManaged = session
isDaemonChild := false
for _, arg := range os.Args {
if arg == "--daemon-child" {
isDaemonChild = true
break
}
}
if !isDaemonChild {
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
cmd.Env = os.Environ()
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
return
}
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
socketPath := server.GetSocketPath()
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
log.Warnf("Failed to write config state file: %v", err)
}
defer os.Remove(configStateFile)
errChan := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("server panic: %v", r)
}
}()
server.CLIVersion = Version
if err := server.Start(false); err != nil {
errChan <- fmt.Errorf("server error: %w", err)
}
}()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
}
}
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
log.Fatalf("Error opening /dev/null: %v", err)
}
defer devNull.Close()
cmd.Stdin = devNull
cmd.Stdout = devNull
cmd.Stderr = devNull
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
// Write PID file for the quickshell child process
if err := writePIDFile(cmd.Process.Pid); err != nil {
log.Warnf("Failed to write PID file: %v", err)
}
defer removePIDFile()
defer func() {
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
if err := cmd.Wait(); err != nil {
errChan <- fmt.Errorf("quickshell exited: %w", err)
} else {
errChan <- fmt.Errorf("quickshell exited")
}
}()
for {
select {
case sig := <-sigChan:
// Handle SIGUSR1 restart for non-session managed processes
if sig == syscall.SIGUSR1 && !isSessionManaged {
log.Infof("Received SIGUSR1, spawning detached restart process...")
execDetachedRestart(os.Getpid())
// Exit immediately to avoid race conditions with detached restart
return
}
// All other signals: clean shutdown
cancel()
cmd.Process.Signal(syscall.SIGTERM)
os.Remove(socketPath)
return
case <-errChan:
cancel()
if cmd.Process != nil {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(1)
}
}
}
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) {
if len(args) == 0 {
log.Error("IPC command requires arguments")
log.Info("Usage: dms ipc <command> [args...]")
os.Exit(1)
}
if args[0] != "call" {
args = append([]string{"call"}, args...)
}
cmdArgs := append([]string{"-p", configPath, "ipc"}, args...)
cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("Error running IPC command: %v", err)
}
}
-53
View File
@@ -1,53 +0,0 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
"github.com/charmbracelet/lipgloss"
)
func printASCII() {
fmt.Print(getThemedASCII())
}
func getThemedASCII() string {
theme := tui.TerminalTheme()
logo := `
██████╗ █████╗ ███╗ ██╗██╗ ██╗
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
██║ ██║███████║██╔██╗ ██║█████╔╝
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
██████╔╝██║ ██║██║ ╚████║██║ ██╗
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
style := lipgloss.NewStyle().
Foreground(lipgloss.Color(theme.Primary)).
Bold(true)
return style.Render(logo) + "\n"
}
func getHelpTemplate() string {
return getThemedASCII() + `
{{.Long}}
Usage:
{{.UseLine}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .Name .NamePadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`
}
-86
View File
@@ -1,86 +0,0 @@
package main
import (
"fmt"
"os/exec"
"strings"
)
func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd)
if err != nil {
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
}
return path, nil
}
func isArchPackageInstalled(packageName string) bool {
cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run()
return err == nil
}
type systemdServiceState struct {
Name string
EnabledState string
NeedsDisable bool
Exists bool
}
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
cmd := exec.Command("systemctl", "is-enabled", serviceName)
output, err := cmd.Output()
stateStr := strings.TrimSpace(string(output))
if err != nil {
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
isKnownState := 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
}
-70
View File
@@ -1,70 +0,0 @@
module github.com/AvengeMedia/DankMaterialShell/core
go 1.24.6
require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2
github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.2.0
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/stretchr/testify v1.11.1
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/emirpasic/gods v1.18.1 // 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-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.2 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // 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/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-155
View File
@@ -1,155 +0,0 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
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/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
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/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
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.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
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/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
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/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
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/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.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/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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
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/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/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-86
View File
@@ -1,86 +0,0 @@
#!/bin/sh
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
# Check for root privileges
if [ "$(id -u)" == "0" ]; then
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
exit 1
fi
# Check if running on Linux
if [ "$(uname)" != "Linux" ]; then
printf "%bError: This installer only supports Linux systems%b\n" "$RED" "$NC"
exit 1
fi
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
ARCH="amd64"
;;
aarch64)
ARCH="arm64"
;;
*)
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
exit 1
;;
esac
# Get the latest release version
LATEST_VERSION=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$LATEST_VERSION" ]; then
printf "%bError: Could not fetch latest version%b\n" "$RED" "$NC"
exit 1
fi
printf "%bInstalling Dankinstall %s for %s...%b\n" "$GREEN" "$LATEST_VERSION" "$ARCH" "$NC"
# Download and install
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR" || exit 1
# Download the gzipped binary and its checksum
printf "%bDownloading installer...%b\n" "$GREEN" "$NC"
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz" -o "installer.gz"
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
# Get the expected checksum
EXPECTED_CHECKSUM=$(cat expected.sha256 | awk '{print $1}')
# Calculate actual checksum
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
ACTUAL_CHECKSUM=$(sha256sum installer.gz | awk '{print $1}')
# Compare checksums
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
printf "%bError: Checksum verification failed%b\n" "$RED" "$NC"
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
printf "The downloaded file may be corrupted or tampered with\n"
cd - > /dev/null
rm -rf "$TEMP_DIR"
exit 1
fi
# Decompress the binary
printf "%bDecompressing installer...%b\n" "$GREEN" "$NC"
gunzip installer.gz
chmod +x installer
# Execute the installer
printf "%bRunning installer...%b\n" "$GREEN" "$NC"
./installer
# Cleanup
cd - > /dev/null
rm -rf "$TEMP_DIR"
-306
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
}
-750
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()
}
}
-55
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
-625
View File
@@ -1,625 +0,0 @@
package config
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
type ConfigDeployer struct {
logChan chan<- string
}
type DeploymentResult struct {
ConfigType string
Path string
BackupPath string
Deployed bool
Error error
}
func NewConfigDeployer(logChan chan<- string) *ConfigDeployer {
return &ConfigDeployer{
logChan: logChan,
}
}
func (cd *ConfigDeployer) log(message string) {
if cd.logChan != nil {
cd.logChan <- message
}
}
// DeployConfigurations deploys all necessary configurations based on the chosen window manager
func (cd *ConfigDeployer) DeployConfigurations(ctx context.Context, wm deps.WindowManager) ([]DeploymentResult, error) {
return cd.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
}
// DeployConfigurationsWithTerminal deploys all necessary configurations based on chosen window manager and terminal
func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]DeploymentResult, error) {
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) {
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) {
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
shouldReplaceConfig := func(configType string) bool {
if replaceConfigs == nil {
return true
}
replace, exists := replaceConfigs[configType]
return !exists || replace
}
switch wm {
case deps.WindowManagerNiri:
if shouldReplaceConfig("Niri") {
result, err := cd.deployNiriConfig(terminal, useSystemd)
results = append(results, result)
if err != nil {
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
}
}
case deps.WindowManagerHyprland:
if shouldReplaceConfig("Hyprland") {
result, err := cd.deployHyprlandConfig(terminal, useSystemd)
results = append(results, result)
if err != nil {
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
}
}
}
switch terminal {
case deps.TerminalGhostty:
if shouldReplaceConfig("Ghostty") {
ghosttyResults, err := cd.deployGhosttyConfig()
results = append(results, ghosttyResults...)
if err != nil {
return results, fmt.Errorf("failed to deploy Ghostty config: %w", err)
}
}
case deps.TerminalKitty:
if shouldReplaceConfig("Kitty") {
kittyResults, err := cd.deployKittyConfig()
results = append(results, kittyResults...)
if err != nil {
return results, fmt.Errorf("failed to deploy Kitty config: %w", err)
}
}
case deps.TerminalAlacritty:
if shouldReplaceConfig("Alacritty") {
alacrittyResults, err := cd.deployAlacrittyConfig()
results = append(results, alacrittyResults...)
if err != nil {
return results, fmt.Errorf("failed to deploy Alacritty config: %w", err)
}
}
}
return results, nil
}
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Niri",
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
}
configDir := filepath.Dir(result.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
result.Error = fmt.Errorf("failed to create config directory: %w", err)
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
if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Niri configuration")
existingData, err := os.ReadFile(result.Path)
if err != nil {
result.Error = fmt.Errorf("failed to read existing config: %w", err)
return result, result.Error
}
existingConfig = string(existingData)
timestamp := time.Now().Format("2006-01-02_15-04-05")
result.BackupPath = result.Path + ".backup." + timestamp
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
result.Error = fmt.Errorf("failed to create backup: %w", err)
return result, result.Error
}
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
}
var terminalCommand string
switch terminal {
case deps.TerminalGhostty:
terminalCommand = "ghostty"
case deps.TerminalKitty:
terminalCommand = "kitty"
case deps.TerminalAlacritty:
terminalCommand = "alacritty"
default:
terminalCommand = "ghostty"
}
newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
}
if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
if err != nil {
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
} else {
newConfig = mergedConfig
cd.log("Successfully merged existing output sections")
}
}
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
result.Error = fmt.Errorf("failed to write config: %w", err)
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
cd.log("Successfully deployed Niri configuration")
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) {
var results []DeploymentResult
mainResult := DeploymentResult{
ConfigType: "Ghostty",
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
}
configDir := filepath.Dir(mainResult.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
if _, err := os.Stat(mainResult.Path); err == nil {
cd.log("Found existing Ghostty configuration")
existingData, err := os.ReadFile(mainResult.Path)
if err != nil {
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
timestamp := time.Now().Format("2006-01-02_15-04-05")
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
}
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil {
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
mainResult.Deployed = true
cd.log("Successfully deployed Ghostty configuration")
results = append(results, mainResult)
colorResult := DeploymentResult{
ConfigType: "Ghostty Colors",
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
}
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
return results, colorResult.Error
}
colorResult.Deployed = true
cd.log("Successfully deployed Ghostty color configuration")
results = append(results, colorResult)
return results, nil
}
func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
var results []DeploymentResult
mainResult := DeploymentResult{
ConfigType: "Kitty",
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
}
configDir := filepath.Dir(mainResult.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
if _, err := os.Stat(mainResult.Path); err == nil {
cd.log("Found existing Kitty configuration")
existingData, err := os.ReadFile(mainResult.Path)
if err != nil {
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
timestamp := time.Now().Format("2006-01-02_15-04-05")
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
}
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil {
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
mainResult.Deployed = true
cd.log("Successfully deployed Kitty configuration")
results = append(results, mainResult)
themeResult := DeploymentResult{
ConfigType: "Kitty Theme",
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
}
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil {
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
return results, themeResult.Error
}
themeResult.Deployed = true
cd.log("Successfully deployed Kitty theme configuration")
results = append(results, themeResult)
tabsResult := DeploymentResult{
ConfigType: "Kitty Tabs",
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
}
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil {
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
return results, tabsResult.Error
}
tabsResult.Deployed = true
cd.log("Successfully deployed Kitty tabs configuration")
results = append(results, tabsResult)
return results, nil
}
func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
var results []DeploymentResult
mainResult := DeploymentResult{
ConfigType: "Alacritty",
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
}
configDir := filepath.Dir(mainResult.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
if _, err := os.Stat(mainResult.Path); err == nil {
cd.log("Found existing Alacritty configuration")
existingData, err := os.ReadFile(mainResult.Path)
if err != nil {
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
timestamp := time.Now().Format("2006-01-02_15-04-05")
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
}
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil {
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
return []DeploymentResult{mainResult}, mainResult.Error
}
mainResult.Deployed = true
cd.log("Successfully deployed Alacritty configuration")
results = append(results, mainResult)
themeResult := DeploymentResult{
ConfigType: "Alacritty Theme",
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
}
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil {
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
return results, themeResult.Error
}
themeResult.Deployed = true
cd.log("Successfully deployed Alacritty theme configuration")
results = append(results, themeResult)
return results, nil
}
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones)
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
// Find all output sections in the existing config
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
if len(existingOutputs) == 0 {
// No output sections to merge
return newConfig, nil
}
// Remove the example output section from the new config
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
// Find where to insert the output sections (after the input section)
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
if len(inputMatches) < 1 {
return "", fmt.Errorf("could not find insertion point for output sections")
}
// Insert after the first closing brace (end of input section)
insertPos := inputMatches[0][1]
var builder strings.Builder
builder.WriteString(mergedConfig[:insertPos])
builder.WriteString("\n// Outputs from existing configuration\n")
for _, output := range existingOutputs {
builder.WriteString(output)
builder.WriteString("\n")
}
builder.WriteString(mergedConfig[insertPos:])
return builder.String(), nil
}
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Hyprland",
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
}
configDir := filepath.Dir(result.Path)
if err := os.MkdirAll(configDir, 0755); err != nil {
result.Error = fmt.Errorf("failed to create config directory: %w", err)
return result, result.Error
}
var existingConfig string
if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Hyprland configuration")
existingData, err := os.ReadFile(result.Path)
if err != nil {
result.Error = fmt.Errorf("failed to read existing config: %w", err)
return result, result.Error
}
existingConfig = string(existingData)
timestamp := time.Now().Format("2006-01-02_15-04-05")
result.BackupPath = result.Path + ".backup." + timestamp
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
result.Error = fmt.Errorf("failed to create backup: %w", err)
return result, result.Error
}
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
}
var terminalCommand string
switch terminal {
case deps.TerminalGhostty:
terminalCommand = "ghostty"
case deps.TerminalKitty:
terminalCommand = "kitty"
case deps.TerminalAlacritty:
terminalCommand = "alacritty"
default:
terminalCommand = "ghostty"
}
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
if !useSystemd {
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
}
if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
if err != nil {
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
} else {
newConfig = mergedConfig
cd.log("Successfully merged existing monitor sections")
}
}
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
result.Error = fmt.Errorf("failed to write config: %w", err)
return result, result.Error
}
result.Deployed = true
cd.log("Successfully deployed Hyprland configuration")
return result, nil
}
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
if len(existingMonitors) == 0 {
return newConfig, nil
}
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
if headerMatch == nil {
return "", fmt.Errorf("could not find MONITOR CONFIG section")
}
insertPos := headerMatch[1] + 1
var builder strings.Builder
builder.WriteString(mergedConfig[:insertPos])
builder.WriteString("# Monitors from existing configuration\n")
for _, monitor := range existingMonitors {
builder.WriteString(monitor)
builder.WriteString("\n")
}
builder.WriteString(mergedConfig[insertPos:])
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
}
-627
View File
@@ -1,627 +0,0 @@
package config
import (
"os"
"path/filepath"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMergeNiriOutputSections(t *testing.T) {
cd := &ConfigDeployer{}
tests := []struct {
name string
newConfig string
existingConfig string
wantError bool
wantContains []string
}{
{
name: "no existing outputs",
newConfig: `input {
keyboard {
xkb {
}
}
}
layout {
gaps 5
}`,
existingConfig: `input {
keyboard {
xkb {
}
}
}
layout {
gaps 10
}`,
wantError: false,
wantContains: []string{"gaps 5"}, // Should keep new config
},
{
name: "merge single output",
newConfig: `input {
keyboard {
xkb {
}
}
}
/-output "eDP-2" {
mode "2560x1600@239.998993"
position x=2560 y=0
}
layout {
gaps 5
}`,
existingConfig: `input {
keyboard {
xkb {
}
}
}
output "eDP-1" {
mode "1920x1080@60.000000"
position x=0 y=0
scale 1.0
}
layout {
gaps 10
}`,
wantError: false,
wantContains: []string{
"gaps 5", // New config preserved
`output "eDP-1"`, // Existing output merged
"1920x1080@60.000000", // Existing output details
"Outputs from existing configuration", // Comment added
},
},
{
name: "merge multiple outputs",
newConfig: `input {
keyboard {
xkb {
}
}
}
/-output "eDP-2" {
mode "2560x1600@239.998993"
position x=2560 y=0
}
layout {
gaps 5
}`,
existingConfig: `input {
keyboard {
xkb {
}
}
}
output "eDP-1" {
mode "1920x1080@60.000000"
position x=0 y=0
scale 1.0
}
/-output "HDMI-1" {
mode "1920x1080@60.000000"
position x=1920 y=0
}
layout {
gaps 10
}`,
wantError: false,
wantContains: []string{
"gaps 5", // New config preserved
`output "eDP-1"`, // First existing output
`/-output "HDMI-1"`, // Second existing output (commented)
"1920x1080@60.000000", // Output details
},
},
{
name: "merge commented outputs",
newConfig: `input {
keyboard {
xkb {
}
}
}
/-output "eDP-2" {
mode "2560x1600@239.998993"
position x=2560 y=0
}
layout {
gaps 5
}`,
existingConfig: `input {
keyboard {
xkb {
}
}
}
/-output "eDP-1" {
mode "1920x1080@60.000000"
position x=0 y=0
scale 1.0
}
layout {
gaps 10
}`,
wantError: false,
wantContains: []string{
"gaps 5", // New config preserved
`/-output "eDP-1"`, // Commented output preserved
"1920x1080@60.000000", // Output details
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig)
if tt.wantError {
assert.Error(t, err)
return
}
require.NoError(t, err)
for _, want := range tt.wantContains {
assert.Contains(t, result, want, "merged config should contain: %s", want)
}
assert.NotContains(t, result, `/-output "eDP-2"`, "example output should be removed")
})
}
}
func TestConfigDeploymentFlow(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dankinstall-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
t.Run("deploy ghostty config to empty directory", func(t *testing.T) {
results, err := cd.deployGhosttyConfig()
require.NoError(t, err)
require.Len(t, results, 2)
mainResult := results[0]
assert.Equal(t, "Ghostty", mainResult.ConfigType)
assert.True(t, mainResult.Deployed)
assert.Empty(t, mainResult.BackupPath)
assert.FileExists(t, mainResult.Path)
content, err := os.ReadFile(mainResult.Path)
require.NoError(t, err)
assert.Contains(t, string(content), "window-decoration = false")
colorResult := results[1]
assert.Equal(t, "Ghostty Colors", colorResult.ConfigType)
assert.True(t, colorResult.Deployed)
assert.FileExists(t, colorResult.Path)
colorContent, err := os.ReadFile(colorResult.Path)
require.NoError(t, err)
assert.Contains(t, string(colorContent), "background = #101418")
})
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
existingContent := "# Old config\nfont-size = 14\n"
ghosttyPath := getGhosttyPath()
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
require.NoError(t, err)
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
require.NoError(t, err)
results, err := cd.deployGhosttyConfig()
require.NoError(t, err)
require.Len(t, results, 2)
mainResult := results[0]
assert.Equal(t, "Ghostty", mainResult.ConfigType)
assert.True(t, mainResult.Deployed)
assert.NotEmpty(t, mainResult.BackupPath)
assert.FileExists(t, mainResult.Path)
assert.FileExists(t, mainResult.BackupPath)
backupContent, err := os.ReadFile(mainResult.BackupPath)
require.NoError(t, err)
assert.Equal(t, existingContent, string(backupContent))
newContent, err := os.ReadFile(mainResult.Path)
require.NoError(t, err)
assert.NotContains(t, string(newContent), "# Old config")
colorResult := results[1]
assert.Equal(t, "Ghostty Colors", colorResult.ConfigType)
assert.True(t, colorResult.Deployed)
assert.FileExists(t, colorResult.Path)
})
}
func getGhosttyPath() string {
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
}
func TestMergeHyprlandMonitorSections(t *testing.T) {
cd := &ConfigDeployer{}
tests := []struct {
name string
newConfig string
existingConfig string
wantError bool
wantContains []string
wantNotContains []string
}{
{
name: "no existing monitors",
newConfig: `# ==================
# MONITOR CONFIG
# ==================
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
# ==================
# ENVIRONMENT VARS
# ==================
env = XDG_CURRENT_DESKTOP,niri`,
existingConfig: `# Some other config
input {
kb_layout = us
}`,
wantError: false,
wantContains: []string{"MONITOR CONFIG", "ENVIRONMENT VARS"},
},
{
name: "merge single monitor",
newConfig: `# ==================
# MONITOR CONFIG
# ==================
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
# ==================
# ENVIRONMENT VARS
# ==================`,
existingConfig: `# My config
monitor = DP-1, 1920x1080@144, 0x0, 1
input {
kb_layout = us
}`,
wantError: false,
wantContains: []string{
"MONITOR CONFIG",
"monitor = DP-1, 1920x1080@144, 0x0, 1",
"Monitors from existing configuration",
},
wantNotContains: []string{
"monitor = eDP-2", // Example monitor should be removed
},
},
{
name: "merge multiple monitors",
newConfig: `# ==================
# MONITOR CONFIG
# ==================
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
# ==================
# ENVIRONMENT VARS
# ==================`,
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
monitor = eDP-1, 2560x1440@165, auto, 1.25`,
wantError: false,
wantContains: []string{
"monitor = DP-1",
"# monitor = HDMI-A-1", // Commented monitor preserved
"monitor = eDP-1",
"Monitors from existing configuration",
},
wantNotContains: []string{
"monitor = eDP-2", // Example monitor should be removed
},
},
{
name: "preserve commented monitors",
newConfig: `# ==================
# MONITOR CONFIG
# ==================
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
# ==================`,
existingConfig: `# monitor = DP-1, 1920x1080@144, 0x0, 1
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1`,
wantError: false,
wantContains: []string{
"# monitor = DP-1",
"# monitor = HDMI-A-1",
"Monitors from existing configuration",
},
},
{
name: "no monitor config section",
newConfig: `# Some config without monitor section
input {
kb_layout = us
}`,
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1`,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig)
if tt.wantError {
assert.Error(t, err)
return
}
require.NoError(t, err)
for _, want := range tt.wantContains {
assert.Contains(t, result, want, "merged config should contain: %s", want)
}
for _, notWant := range tt.wantNotContains {
assert.NotContains(t, result, notWant, "merged config should NOT contain: %s", notWant)
}
})
}
}
func TestHyprlandConfigDeployment(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dankinstall-hyprland-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType)
assert.True(t, result.Deployed)
assert.Empty(t, result.BackupPath)
assert.FileExists(t, result.Path)
content, err := os.ReadFile(result.Path)
require.NoError(t, err)
assert.Contains(t, string(content), "# MONITOR CONFIG")
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
assert.Contains(t, string(content), "exec-once = ")
})
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
existingContent := `# My existing Hyprland config
monitor = DP-1, 1920x1080@144, 0x0, 1
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
general {
gaps_in = 10
}
`
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
err := os.MkdirAll(filepath.Dir(hyprPath), 0755)
require.NoError(t, err)
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
require.NoError(t, err)
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
require.NoError(t, err)
assert.Equal(t, "Hyprland", result.ConfigType)
assert.True(t, result.Deployed)
assert.NotEmpty(t, result.BackupPath)
assert.FileExists(t, result.Path)
assert.FileExists(t, result.BackupPath)
backupContent, err := os.ReadFile(result.BackupPath)
require.NoError(t, err)
assert.Equal(t, existingContent, string(backupContent))
newContent, err := os.ReadFile(result.Path)
require.NoError(t, err)
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
assert.NotContains(t, string(newContent), "monitor = eDP-2")
})
}
func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriBindsConfig, "binds {")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
}
func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
}
func TestGhosttyConfigStructure(t *testing.T) {
assert.Contains(t, GhosttyConfig, "window-decoration = false")
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
}
func TestGhosttyColorConfigStructure(t *testing.T) {
assert.Contains(t, GhosttyColorConfig, "background = #101418")
assert.Contains(t, GhosttyColorConfig, "foreground = #e0e2e8")
assert.Contains(t, GhosttyColorConfig, "cursor-color = #9dcbfb")
assert.Contains(t, GhosttyColorConfig, "palette = 0=#101418")
assert.Contains(t, GhosttyColorConfig, "palette = 15=#ffffff")
}
func TestKittyConfigStructure(t *testing.T) {
assert.Contains(t, KittyConfig, "font_size 12.0")
assert.Contains(t, KittyConfig, "window_padding_width 12")
assert.Contains(t, KittyConfig, "background_opacity 1.0")
assert.Contains(t, KittyConfig, "include dank-tabs.conf")
assert.Contains(t, KittyConfig, "include dank-theme.conf")
}
func TestKittyThemeConfigStructure(t *testing.T) {
assert.Contains(t, KittyThemeConfig, "foreground #e0e2e8")
assert.Contains(t, KittyThemeConfig, "background #101418")
assert.Contains(t, KittyThemeConfig, "cursor #e0e2e8")
assert.Contains(t, KittyThemeConfig, "color0 #101418")
assert.Contains(t, KittyThemeConfig, "color15 #ffffff")
}
func TestKittyTabsConfigStructure(t *testing.T) {
assert.Contains(t, KittyTabsConfig, "tab_bar_style powerline")
assert.Contains(t, KittyTabsConfig, "tab_powerline_style slanted")
assert.Contains(t, KittyTabsConfig, "active_tab_background #124a73")
assert.Contains(t, KittyTabsConfig, "inactive_tab_background #101418")
}
func TestAlacrittyConfigStructure(t *testing.T) {
assert.Contains(t, AlacrittyConfig, "[general]")
assert.Contains(t, AlacrittyConfig, "~/.config/alacritty/dank-theme.toml")
assert.Contains(t, AlacrittyConfig, "[window]")
assert.Contains(t, AlacrittyConfig, "decorations = \"None\"")
assert.Contains(t, AlacrittyConfig, "padding = { x = 12, y = 12 }")
assert.Contains(t, AlacrittyConfig, "[cursor]")
assert.Contains(t, AlacrittyConfig, "[keyboard]")
}
func TestAlacrittyThemeConfigStructure(t *testing.T) {
assert.Contains(t, AlacrittyThemeConfig, "[colors.primary]")
assert.Contains(t, AlacrittyThemeConfig, "background = '#101418'")
assert.Contains(t, AlacrittyThemeConfig, "foreground = '#e0e2e8'")
assert.Contains(t, AlacrittyThemeConfig, "[colors.cursor]")
assert.Contains(t, AlacrittyThemeConfig, "cursor = '#9dcbfb'")
assert.Contains(t, AlacrittyThemeConfig, "[colors.normal]")
assert.Contains(t, AlacrittyThemeConfig, "[colors.bright]")
}
func TestKittyConfigDeployment(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dankinstall-kitty-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
t.Run("deploy kitty config to empty directory", func(t *testing.T) {
results, err := cd.deployKittyConfig()
require.NoError(t, err)
require.Len(t, results, 3)
mainResult := results[0]
assert.Equal(t, "Kitty", mainResult.ConfigType)
assert.True(t, mainResult.Deployed)
assert.FileExists(t, mainResult.Path)
content, err := os.ReadFile(mainResult.Path)
require.NoError(t, err)
assert.Contains(t, string(content), "include dank-theme.conf")
themeResult := results[1]
assert.Equal(t, "Kitty Theme", themeResult.ConfigType)
assert.True(t, themeResult.Deployed)
assert.FileExists(t, themeResult.Path)
tabsResult := results[2]
assert.Equal(t, "Kitty Tabs", tabsResult.ConfigType)
assert.True(t, tabsResult.Deployed)
assert.FileExists(t, tabsResult.Path)
})
}
func TestAlacrittyConfigDeployment(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dankinstall-alacritty-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
t.Run("deploy alacritty config to empty directory", func(t *testing.T) {
results, err := cd.deployAlacrittyConfig()
require.NoError(t, err)
require.Len(t, results, 2)
mainResult := results[0]
assert.Equal(t, "Alacritty", mainResult.ConfigType)
assert.True(t, mainResult.Deployed)
assert.FileExists(t, mainResult.Path)
content, err := os.ReadFile(mainResult.Path)
require.NoError(t, err)
assert.Contains(t, string(content), "~/.config/alacritty/dank-theme.toml")
assert.Contains(t, string(content), "[window]")
themeResult := results[1]
assert.Equal(t, "Alacritty Theme", themeResult.ConfigType)
assert.True(t, themeResult.Deployed)
assert.FileExists(t, themeResult.Path)
themeContent, err := os.ReadFile(themeResult.Path)
require.NoError(t, err)
assert.Contains(t, string(themeContent), "[colors.primary]")
assert.Contains(t, string(themeContent), "background = '#101418'")
})
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0755)
require.NoError(t, err)
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
require.NoError(t, err)
results, err := cd.deployAlacrittyConfig()
require.NoError(t, err)
require.Len(t, results, 2)
mainResult := results[0]
assert.True(t, mainResult.Deployed)
assert.NotEmpty(t, mainResult.BackupPath)
assert.FileExists(t, mainResult.BackupPath)
backupContent, err := os.ReadFile(mainResult.BackupPath)
require.NoError(t, err)
assert.Equal(t, existingContent, string(backupContent))
newContent, err := os.ReadFile(mainResult.Path)
require.NoError(t, err)
assert.NotContains(t, string(newContent), "# Old alacritty config")
assert.Contains(t, string(newContent), "decorations = \"None\"")
})
}
-59
View File
@@ -1,59 +0,0 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
func LocateDMSConfig() (string, error) {
var primaryPaths []string
configHome := utils.XDGConfigHome()
if configHome != "" {
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
}
// System data directories
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")
if configDirs == "" {
configDirs = "/etc/xdg"
}
for _, dir := range strings.Split(configDirs, ":") {
if dir != "" {
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
}
}
// Build search paths with secondary (monorepo) paths interleaved
var searchPaths []string
for _, path := range primaryPaths {
searchPaths = append(searchPaths, path)
searchPaths = append(searchPaths, filepath.Join(path, "quickshell"))
}
for _, path := range searchPaths {
shellPath := filepath.Join(path, "shell.qml")
if info, err := os.Stat(shellPath); err == nil && !info.IsDir() {
return path, nil
}
}
return "", fmt.Errorf("could not find DMS config (shell.qml) in any valid config path")
}
@@ -1,31 +0,0 @@
[colors.primary]
background = '#101418'
foreground = '#e0e2e8'
[colors.selection]
text = '#e0e2e8'
background = '#124a73'
[colors.cursor]
text = '#101418'
cursor = '#9dcbfb'
[colors.normal]
black = '#101418'
red = '#d75a59'
green = '#8ed88c'
yellow = '#e0d99d'
blue = '#4087bc'
magenta = '#839fbc'
cyan = '#9dcbfb'
white = '#abb2bf'
[colors.bright]
black = '#5c6370'
red = '#e57e7e'
green = '#a2e5a0'
yellow = '#efe9b3'
blue = '#a7d9ff'
magenta = '#3d8197'
cyan = '#5c7ba3'
white = '#ffffff'
@@ -1,37 +0,0 @@
[general]
import = [
"~/.config/alacritty/dank-theme.toml"
]
[window]
decorations = "None"
padding = { x = 12, y = 12 }
opacity = 1.0
[scrolling]
history = 3023
[cursor]
style = { shape = "Block", blinking = "On" }
blink_interval = 500
unfocused_hollow = true
[mouse]
hide_when_typing = true
[selection]
save_to_clipboard = false
[bell]
duration = 0
[keyboard]
bindings = [
{ key = "C", mods = "Control|Shift", action = "Copy" },
{ key = "V", mods = "Control|Shift", action = "Paste" },
{ key = "N", mods = "Control|Shift", action = "SpawnNewInstance" },
{ key = "Equals", mods = "Control|Shift", action = "IncreaseFontSize" },
{ key = "Minus", mods = "Control", action = "DecreaseFontSize" },
{ key = "Key0", mods = "Control", action = "ResetFontSize" },
{ key = "Enter", mods = "Shift", chars = "\n" },
]
@@ -1,21 +0,0 @@
background = #101418
foreground = #e0e2e8
cursor-color = #9dcbfb
selection-background = #124a73
selection-foreground = #e0e2e8
palette = 0=#101418
palette = 1=#d75a59
palette = 2=#8ed88c
palette = 3=#e0d99d
palette = 4=#4087bc
palette = 5=#839fbc
palette = 6=#9dcbfb
palette = 7=#abb2bf
palette = 8=#5c6370
palette = 9=#e57e7e
palette = 10=#a2e5a0
palette = 11=#efe9b3
palette = 12=#a7d9ff
palette = 13=#3d8197
palette = 14=#5c7ba3
palette = 15=#ffffff
@@ -1,51 +0,0 @@
# Font Configuration
font-size = 12
# Window Configuration
window-decoration = false
window-padding-x = 12
window-padding-y = 12
background-opacity = 1.0
background-blur-radius = 32
# Cursor Configuration
cursor-style = block
cursor-style-blink = true
# Scrollback
scrollback-limit = 3023
# Terminal features
mouse-hide-while-typing = true
copy-on-select = false
confirm-close-surface = false
# Disable annoying copied to clipboard
app-notifications = no-clipboard-copy,no-config-reload
# Key bindings for common actions
#keybind = ctrl+c=copy_to_clipboard
#keybind = ctrl+v=paste_from_clipboard
keybind = ctrl+shift+n=new_window
keybind = ctrl+t=new_tab
keybind = ctrl+plus=increase_font_size:1
keybind = ctrl+minus=decrease_font_size:1
keybind = ctrl+zero=reset_font_size
# Material 3 UI elements
unfocused-split-opacity = 0.7
unfocused-split-fill = #44464f
# Tab configuration
gtk-titlebar = false
# Shell integration
shell-integration = detect
shell-integration-features = cursor,sudo,title,no-cursor
keybind = shift+enter=text:\n
# Rando stuff
gtk-single-instance = true
# Dank color generation
config-file = ./config-dankcolors
-280
View File
@@ -1,280 +0,0 @@
# Hyprland Configuration
# https://wiki.hypr.land/Configuring/
# ==================
# MONITOR CONFIG
# ==================
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
monitor = , preferred,auto,auto
# ==================
# 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 &"
# ==================
# INPUT CONFIG
# ==================
input {
kb_layout = us
numlock_by_default = true
}
# ==================
# GENERAL LAYOUT
# ==================
general {
gaps_in = 5
gaps_out = 5
border_size = 0 # off in niri
col.active_border = rgba(707070ff)
col.inactive_border = rgba(d0d0d0ff)
layout = dwindle
}
# ==================
# DECORATION
# ==================
decoration {
rounding = 12
active_opacity = 1.0
inactive_opacity = 0.9
shadow {
enabled = true
range = 30
render_power = 5
offset = 0 5
color = rgba(00000070)
}
}
# ==================
# ANIMATIONS
# ==================
animations {
enabled = true
animation = windowsIn, 1, 3, default
animation = windowsOut, 1, 3, default
animation = workspaces, 1, 5, default
animation = windowsMove, 1, 4, default
animation = fade, 1, 3, default
animation = border, 1, 3, default
}
# ==================
# LAYOUTS
# ==================
dwindle {
preserve_split = true
}
master {
mfact = 0.5
}
# ==================
# MISC
# ==================
misc {
disable_hyprland_logo = true
disable_splash_rendering = true
vrr = 1
}
# ==================
# WINDOW RULES
# ==================
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
windowrulev2 = rounding 12, class:^(org\.gnome\.)
windowrulev2 = noborder, class:^(org\.gnome\.)
windowrulev2 = tile, class:^(gnome-control-center)$
windowrulev2 = tile, class:^(pavucontrol)$
windowrulev2 = tile, class:^(nm-connection-editor)$
windowrulev2 = float, class:^(gnome-calculator)$
windowrulev2 = float, class:^(galculator)$
windowrulev2 = float, class:^(blueman-manager)$
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
windowrulev2 = float, class:^(steam)$
windowrulev2 = float, class:^(xdg-desktop-portal)$
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
windowrulev2 = noborder, class:^(Alacritty)$
windowrulev2 = noborder, class:^(zen)$
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
windowrulev2 = noborder, class:^(kitty)$
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
windowrulev2 = float, class:^(zoom)$
# DMS windows floating by default
windowrulev2 = float, class:^(org.quickshell)$
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
layerrule = noanim, ^(quickshell)$
# ==================
# KEYBINDINGS
# ==================
$mod = SUPER
# === Application Launchers ===
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
bind = $mod, comma, exec, dms ipc call settings focusOrToggle
bind = $mod, N, exec, dms ipc call notifications toggle
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
# === Cheat sheet
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
# === Security ===
bind = $mod ALT, L, exec, dms ipc call lock lock
bind = $mod SHIFT, E, exit
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
# === Audio Controls ===
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
bindl = , XF86AudioMute, exec, dms ipc call audio mute
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
# === Brightness Controls ===
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
# === Window Management ===
bind = $mod, Q, killactive
bind = $mod, F, fullscreen, 1
bind = $mod SHIFT, F, fullscreen, 0
bind = $mod SHIFT, T, togglefloating
bind = $mod, W, togglegroup
# === Focus Navigation ===
bind = $mod, left, movefocus, l
bind = $mod, down, movefocus, d
bind = $mod, up, movefocus, u
bind = $mod, right, movefocus, r
bind = $mod, H, movefocus, l
bind = $mod, J, movefocus, d
bind = $mod, K, movefocus, u
bind = $mod, L, movefocus, r
# === Window Movement ===
bind = $mod SHIFT, left, movewindow, l
bind = $mod SHIFT, down, movewindow, d
bind = $mod SHIFT, up, movewindow, u
bind = $mod SHIFT, right, movewindow, r
bind = $mod SHIFT, H, movewindow, l
bind = $mod SHIFT, J, movewindow, d
bind = $mod SHIFT, K, movewindow, u
bind = $mod SHIFT, L, movewindow, r
# === Column Navigation ===
bind = $mod, Home, focuswindow, first
bind = $mod, End, focuswindow, last
# === Monitor Navigation ===
bind = $mod CTRL, left, focusmonitor, l
bind = $mod CTRL, right, focusmonitor, r
bind = $mod CTRL, H, focusmonitor, l
bind = $mod CTRL, J, focusmonitor, d
bind = $mod CTRL, K, focusmonitor, u
bind = $mod CTRL, L, focusmonitor, r
# === Move to Monitor ===
bind = $mod SHIFT CTRL, left, movewindow, mon:l
bind = $mod SHIFT CTRL, down, movewindow, mon:d
bind = $mod SHIFT CTRL, up, movewindow, mon:u
bind = $mod SHIFT CTRL, right, movewindow, mon:r
bind = $mod SHIFT CTRL, H, movewindow, mon:l
bind = $mod SHIFT CTRL, J, movewindow, mon:d
bind = $mod SHIFT CTRL, K, movewindow, mon:u
bind = $mod SHIFT CTRL, L, movewindow, mon:r
# === Workspace Navigation ===
bind = $mod, Page_Down, workspace, e+1
bind = $mod, Page_Up, workspace, e-1
bind = $mod, U, workspace, e+1
bind = $mod, I, workspace, e-1
bind = $mod CTRL, down, movetoworkspace, e+1
bind = $mod CTRL, up, movetoworkspace, e-1
bind = $mod CTRL, U, movetoworkspace, e+1
bind = $mod CTRL, I, movetoworkspace, e-1
# === Move Workspaces ===
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
bind = $mod SHIFT, U, movetoworkspace, e+1
bind = $mod SHIFT, I, movetoworkspace, e-1
# === Mouse Wheel Navigation ===
bind = $mod, mouse_down, workspace, e+1
bind = $mod, mouse_up, workspace, e-1
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
# === Numbered Workspaces ===
bind = $mod, 1, workspace, 1
bind = $mod, 2, workspace, 2
bind = $mod, 3, workspace, 3
bind = $mod, 4, workspace, 4
bind = $mod, 5, workspace, 5
bind = $mod, 6, workspace, 6
bind = $mod, 7, workspace, 7
bind = $mod, 8, workspace, 8
bind = $mod, 9, workspace, 9
# === Move to Numbered Workspaces ===
bind = $mod SHIFT, 1, movetoworkspace, 1
bind = $mod SHIFT, 2, movetoworkspace, 2
bind = $mod SHIFT, 3, movetoworkspace, 3
bind = $mod SHIFT, 4, movetoworkspace, 4
bind = $mod SHIFT, 5, movetoworkspace, 5
bind = $mod SHIFT, 6, movetoworkspace, 6
bind = $mod SHIFT, 7, movetoworkspace, 7
bind = $mod SHIFT, 8, movetoworkspace, 8
bind = $mod SHIFT, 9, movetoworkspace, 9
# === Column Management ===
bind = $mod, bracketleft, layoutmsg, preselect l
bind = $mod, bracketright, layoutmsg, preselect r
# === Sizing & Layout ===
bind = $mod, R, layoutmsg, togglesplit
bind = $mod CTRL, F, resizeactive, exact 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = $mod, mouse:272, Move window, movewindow
bindmd = $mod, mouse:273, Resize window, resizewindow
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
# === Manual Sizing ===
binde = $mod, minus, resizeactive, -10% 0
binde = $mod, equal, resizeactive, 10% 0
binde = $mod SHIFT, minus, resizeactive, 0 -10%
binde = $mod SHIFT, equal, resizeactive, 0 10%
# === Screenshots ===
bind = , Print, exec, dms screenshot
bind = CTRL, Print, exec, dms screenshot full
bind = ALT, Print, exec, dms screenshot window
# === System Controls ===
bind = $mod SHIFT, P, dpms, off
@@ -1,24 +0,0 @@
tab_bar_edge top
tab_bar_style powerline
tab_powerline_style slanted
tab_bar_align left
tab_bar_min_tabs 2
tab_bar_margin_width 0.0
tab_bar_margin_height 2.5 1.5
tab_bar_margin_color #101418
tab_bar_background #101418
active_tab_foreground #cfe5ff
active_tab_background #124a73
active_tab_font_style bold
inactive_tab_foreground #c2c7cf
inactive_tab_background #101418
inactive_tab_font_style normal
tab_activity_symbol " ● "
tab_numbers_style 1
tab_title_template "{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title[:30]}{title[30:] and '…'} [{index}]"
active_tab_title_template "{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title[:30]}{title[30:] and '…'} [{index}]"
@@ -1,24 +0,0 @@
cursor #e0e2e8
cursor_text_color #c2c7cf
foreground #e0e2e8
background #101418
selection_foreground #243240
selection_background #b9c8da
url_color #9dcbfb
color0 #101418
color1 #d75a59
color2 #8ed88c
color3 #e0d99d
color4 #4087bc
color5 #839fbc
color6 #9dcbfb
color7 #abb2bf
color8 #5c6370
color9 #e57e7e
color10 #a2e5a0
color11 #efe9b3
color12 #a7d9ff
color13 #3d8197
color14 #5c7ba3
color15 #ffffff
-37
View File
@@ -1,37 +0,0 @@
# Font Configuration
font_size 12.0
# Window Configuration
window_padding_width 12
background_opacity 1.0
background_blur 32
hide_window_decorations yes
# Cursor Configuration
cursor_shape block
cursor_blink_interval 1
# Scrollback
scrollback_lines 3000
# Terminal features
copy_on_select yes
strip_trailing_spaces smart
# Key bindings for common actions
map ctrl+shift+n new_window
map ctrl+t new_tab
map ctrl+plus change_font_size all +1.0
map ctrl+minus change_font_size all -1.0
map ctrl+0 change_font_size all 0
# Tab configuration
tab_bar_style powerline
tab_bar_align left
# Shell integration
shell_integration enabled
# Dank color generation
include dank-tabs.conf
include dank-theme.conf
@@ -1,5 +0,0 @@
recent-windows {
highlight {
corner-radius 12
}
}
@@ -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; }
}
@@ -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"
}
}
@@ -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
}
-227
View File
@@ -1,227 +0,0 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
config-notification {
disable-failed
}
gestures {
hot-corners {
off
}
}
// Input device configuration.
// Find the full list of options on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
input {
keyboard {
xkb {
}
numlock
}
touchpad {
}
mouse {
}
trackpoint {
}
}
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-2" {
mode "2560x1600@239.998993"
position x=2560 y=0
variable-refresh-rate
}
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
background-color "transparent"
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
// or right edge of the screen.
// - "always", the focused column will always be centered.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
center-focused-column "never"
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.33333
proportion 0.5
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decorations can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
//
// Alternatively, you can override it with a window rule called
// `draw-border-with-background`.
border {
off
width 4
active-color "#707070" // Neutral gray
inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red
}
shadow {
softness 30
spread 5
offset x=0 y=5
color "#0007"
}
struts {
}
}
layer-rule {
match namespace="^quickshell$"
place-within-backdrop true
}
overview {
workspace-shadow {
off
}
}
// Add lines like this to spawn processes at startup.
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
environment {
XDG_CURRENT_DESKTOP "niri"
}
hotkey-overlay {
skip-at-startup
}
prefer-no-csd
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
animations {
workspace-switch {
spring damping-ratio=0.80 stiffness=523 epsilon=0.0001
}
window-open {
duration-ms 150
curve "ease-out-expo"
}
window-close {
duration-ms 150
curve "ease-out-quad"
}
horizontal-view-movement {
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
}
window-movement {
spring damping-ratio=0.75 stiffness=323 epsilon=0.0001
}
window-resize {
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
}
config-notification-open-close {
spring damping-ratio=0.65 stiffness=923 epsilon=0.001
}
screenshot-ui-open {
duration-ms 200
curve "ease-out-quad"
}
overview-open-close {
spring damping-ratio=0.85 stiffness=800 epsilon=0.0001
}
}
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
window-rule {
// This regular expression is intentionally made as specific as possible,
// since this is the default config, and we want no false positives.
// You can get away with just app-id="wezterm" if you want.
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
window-rule {
match app-id=r#"^org\.gnome\."#
draw-border-with-background false
geometry-corner-radius 12
clip-to-geometry true
}
window-rule {
match app-id=r#"^gnome-control-center$"#
match app-id=r#"^pavucontrol$"#
match app-id=r#"^nm-connection-editor$"#
default-column-width { proportion 0.5; }
open-floating false
}
window-rule {
match app-id=r#"^gnome-calculator$"#
match app-id=r#"^galculator$"#
match app-id=r#"^blueman-manager$"#
match app-id=r#"^org\.gnome\.Nautilus$"#
match app-id=r#"^steam$"#
match app-id=r#"^xdg-desktop-portal$"#
open-floating true
}
window-rule {
match app-id=r#"^org\.wezfurlong\.wezterm$"#
match app-id="Alacritty"
match app-id="zen"
match app-id="com.mitchellh.ghostty"
match app-id="kitty"
draw-border-with-background false
}
window-rule {
match is-active=false
opacity 0.9
}
window-rule {
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
match app-id="zoom"
open-floating true
}
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
open-floating true
}
debug {
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"
Binary file not shown.
-6
View File
@@ -1,6 +0,0 @@
package config
import _ "embed"
//go:embed embedded/hyprland.conf
var HyprlandConfig string
-18
View File
@@ -1,18 +0,0 @@
package config
import _ "embed"
//go:embed embedded/niri.kdl
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
-24
View File
@@ -1,24 +0,0 @@
package config
import _ "embed"
//go:embed embedded/ghostty.conf
var GhosttyConfig string
//go:embed embedded/ghostty-colors.conf
var GhosttyColorConfig string
//go:embed embedded/kitty.conf
var KittyConfig string
//go:embed embedded/kitty-theme.conf
var KittyThemeConfig string
//go:embed embedded/kitty-tabs.conf
var KittyTabsConfig string
//go:embed embedded/alacritty.toml
var AlacrittyConfig string
//go:embed embedded/alacritty-theme.toml
var AlacrittyThemeConfig string
-6
View File
@@ -1,6 +0,0 @@
package config
import _ "embed"
//go:embed embedded/testpage.pdf
var TestPage string
-576
View File
@@ -1,576 +0,0 @@
package dank16
import (
"fmt"
"math"
"github.com/lucasb-eyer/go-colorful"
)
type RGB struct {
R, G, B float64
}
type HSV struct {
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 {
if hex[0] == '#' {
hex = hex[1:]
}
var r, g, b uint8
fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b)
return RGB{
R: float64(r) / 255.0,
G: float64(g) / 255.0,
B: float64(b) / 255.0,
}
}
func RGBToHex(rgb RGB) string {
r := math.Max(0, math.Min(1, rgb.R))
g := math.Max(0, math.Min(1, rgb.G))
b := math.Max(0, math.Min(1, rgb.B))
return fmt.Sprintf("#%02x%02x%02x", int(r*255), int(g*255), int(b*255))
}
func RGBToHSV(rgb RGB) HSV {
max := math.Max(math.Max(rgb.R, rgb.G), rgb.B)
min := math.Min(math.Min(rgb.R, rgb.G), rgb.B)
delta := max - min
var h float64
switch {
case delta == 0:
h = 0
case max == rgb.R:
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
case max == rgb.G:
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
default:
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
}
if h < 0 {
h += 1.0
}
var s float64
if max == 0 {
s = 0
} else {
s = delta / max
}
return HSV{H: h, S: s, V: max}
}
func HSVToRGB(hsv HSV) RGB {
h := hsv.H * 6.0
c := hsv.V * hsv.S
x := c * (1.0 - math.Abs(math.Mod(h, 2.0)-1.0))
m := hsv.V - c
var r, g, b float64
switch int(h) {
case 0:
r, g, b = c, x, 0
case 1:
r, g, b = x, c, 0
case 2:
r, g, b = 0, c, x
case 3:
r, g, b = 0, x, c
case 4:
r, g, b = x, 0, c
case 5:
r, g, b = c, 0, x
default:
r, g, b = c, 0, x
}
return RGB{R: r + m, G: g + m, B: b + m}
}
func sRGBToLinear(c float64) float64 {
if c <= 0.04045 {
return c / 12.92
}
return math.Pow((c+0.055)/1.055, 2.4)
}
func Luminance(hex string) float64 {
rgb := HexToRGB(hex)
return 0.2126*sRGBToLinear(rgb.R) + 0.7152*sRGBToLinear(rgb.G) + 0.0722*sRGBToLinear(rgb.B)
}
func ContrastRatio(hexFg, hexBg string) float64 {
lumFg := Luminance(hexFg)
lumBg := Luminance(hexBg)
lighter := math.Max(lumFg, lumBg)
darker := math.Min(lumFg, lumBg)
return (lighter + 0.05) / (darker + 0.05)
}
func getLstar(hex string) float64 {
rgb := HexToRGB(hex)
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
L, _, _ := col.Lab()
return L * 100.0 // go-colorful uses 0-1, we need 0-100 for DPS
}
// Lab to hex, clamping if needed
func labToHex(L, a, b float64) string {
c := colorful.Lab(L/100.0, a, b) // back to 0-1 for go-colorful
r, g, b2 := c.Clamped().RGB255()
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
}
// Adjust brightness while keeping the same hue
func retoneToL(hex string, Ltarget float64) string {
rgb := HexToRGB(hex)
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
L, a, b := col.Lab()
L100 := L * 100.0
scale := 1.0
if L100 != 0 {
scale = Ltarget / L100
}
a2, b2 := a*scale, b*scale
// Don't let it get too saturated
maxChroma := 0.4
if math.Hypot(a2, b2) > maxChroma {
k := maxChroma / math.Hypot(a2, b2)
a2 *= k
b2 *= k
}
return labToHex(Ltarget, a2, b2)
}
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
Lf := getLstar(hexFg)
Lb := getLstar(hexBg)
phi := 1.618
inv := 0.618
lc := math.Pow(math.Abs(math.Pow(Lb, phi)-math.Pow(Lf, phi)), inv)*1.414 - 40
if negativePolarity {
lc += 5
}
return lc
}
func DeltaPhiStarContrast(hexFg, hexBg string, isLightMode bool) float64 {
negativePolarity := !isLightMode
return DeltaPhiStar(hexFg, hexBg, negativePolarity)
}
func EnsureContrast(hexColor, hexBg string, minRatio float64, isLightMode bool) string {
currentRatio := ContrastRatio(hexColor, hexBg)
if currentRatio >= minRatio {
return hexColor
}
rgb := HexToRGB(hexColor)
hsv := RGBToHSV(rgb)
for step := 1; step < 30; step++ {
delta := float64(step) * 0.02
if isLightMode {
newV := math.Max(0, hsv.V-delta)
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if ContrastRatio(candidate, hexBg) >= minRatio {
return candidate
}
newV = math.Min(1, hsv.V+delta)
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if ContrastRatio(candidate, hexBg) >= minRatio {
return candidate
}
} else {
newV := math.Min(1, hsv.V+delta)
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if ContrastRatio(candidate, hexBg) >= minRatio {
return candidate
}
newV = math.Max(0, hsv.V-delta)
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if ContrastRatio(candidate, hexBg) >= minRatio {
return candidate
}
}
}
return hexColor
}
func EnsureContrastDPS(hexColor, hexBg string, minLc float64, isLightMode bool) string {
currentLc := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
if currentLc >= minLc {
return hexColor
}
rgb := HexToRGB(hexColor)
hsv := RGBToHSV(rgb)
for step := 1; step < 50; step++ {
delta := float64(step) * 0.015
if isLightMode {
newV := math.Max(0, hsv.V-delta)
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
return candidate
}
newV = math.Min(1, hsv.V+delta)
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
return candidate
}
} else {
newV := math.Min(1, hsv.V+delta)
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
return candidate
}
newV = math.Max(0, hsv.V-delta)
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
return candidate
}
}
}
return hexColor
}
// Nudge L* until contrast is good enough. Keeps hue intact unlike HSV fiddling.
func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode bool) string {
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
if current >= minLc {
return hexColor
}
fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
Lf, af, bf := cf.Lab()
dir := 1.0
if isLightMode {
dir = -1.0 // light mode = darker text
}
step := 0.5
for i := 0; i < 120; i++ {
Lf = math.Max(0, math.Min(100, Lf+dir*step))
cand := labToHex(Lf, af, bf)
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
return cand
}
}
return hexColor
}
type PaletteOptions struct {
IsLight bool
Background string
UseDPS bool
}
func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOptions) string {
if opts.UseDPS {
return EnsureContrastDPSLstar(hexColor, hexBg, target, opts.IsLight)
}
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
}
func DeriveContainer(primary string, isLight bool) string {
rgb := HexToRGB(primary)
hsv := RGBToHSV(rgb)
if isLight {
containerV := math.Min(hsv.V*1.77, 1.0)
containerS := hsv.S * 0.32
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
}
containerV := hsv.V * 0.463
containerS := math.Min(hsv.S*1.834, 1.0)
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
}
func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
baseColor := DeriveContainer(primaryColor, opts.IsLight)
rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb)
var palette Palette
var normalTextTarget, secondaryTarget float64
if opts.UseDPS {
normalTextTarget = 40.0
secondaryTarget = 35.0
} else {
normalTextTarget = 4.5
secondaryTarget = 3.0
}
var bgColor string
if opts.Background != "" {
bgColor = opts.Background
} else if opts.IsLight {
bgColor = "#f8f8f8"
} else {
bgColor = "#1a1a1a"
}
palette.Color0 = NewColorInfo(bgColor)
hueShift := (hsv.H - 0.6) * 0.12
satBoost := 1.15
redH := math.Mod(0.0+hueShift+1.0, 1.0)
var redColor string
if opts.IsLight {
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))
} else {
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))
}
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
var greenColor string
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}))
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} else {
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))
}
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
var yellowColor string
if opts.IsLight {
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))
} else {
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))
}
var blueColor string
if opts.IsLight {
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))
} 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)}))
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
}
magH := hsv.H - 0.03
if magH < 0 {
magH += 1.0
}
var magColor string
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
if opts.IsLight {
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))
} else {
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))
}
cyanH := hsv.H + 0.08
if cyanH > 1.0 {
cyanH -= 1.0
}
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
if opts.IsLight {
palette.Color7 = NewColorInfo("#1a1a1a")
palette.Color8 = NewColorInfo("#2e2e2e")
} else {
palette.Color7 = NewColorInfo("#abb2bf")
palette.Color8 = NewColorInfo("#5c6370")
}
if opts.IsLight {
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))
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))
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))
hr := HexToRGB(primaryColor)
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)}))
palette.Color12 = NewColorInfo(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)}))
palette.Color13 = NewColorInfo(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)}))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} else {
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))
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))
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))
brightBlue := retoneToL(primaryColor, 85.0)
palette.Color12 = NewColorInfo(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)}))
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyanH := hsv.H + 0.02
if 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)}))
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
}
if opts.IsLight {
palette.Color15 = NewColorInfo("#1a1a1a")
} else {
palette.Color15 = NewColorInfo("#ffffff")
}
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),
}
}
-668
View File
@@ -1,668 +0,0 @@
package dank16
import (
"math"
"testing"
)
func TestHexToRGB(t *testing.T) {
tests := []struct {
name string
input string
expected RGB
}{
{
name: "black with hash",
input: "#000000",
expected: RGB{R: 0.0, G: 0.0, B: 0.0},
},
{
name: "white with hash",
input: "#ffffff",
expected: RGB{R: 1.0, G: 1.0, B: 1.0},
},
{
name: "red without hash",
input: "ff0000",
expected: RGB{R: 1.0, G: 0.0, B: 0.0},
},
{
name: "purple",
input: "#625690",
expected: RGB{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412},
},
{
name: "mid gray",
input: "#808080",
expected: RGB{R: 0.5019607843137255, G: 0.5019607843137255, B: 0.5019607843137255},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HexToRGB(tt.input)
if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) {
t.Errorf("HexToRGB(%s) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestRGBToHex(t *testing.T) {
tests := []struct {
name string
input RGB
expected string
}{
{
name: "black",
input: RGB{R: 0.0, G: 0.0, B: 0.0},
expected: "#000000",
},
{
name: "white",
input: RGB{R: 1.0, G: 1.0, B: 1.0},
expected: "#ffffff",
},
{
name: "red",
input: RGB{R: 1.0, G: 0.0, B: 0.0},
expected: "#ff0000",
},
{
name: "clamping above 1.0",
input: RGB{R: 1.5, G: 0.5, B: 0.5},
expected: "#ff7f7f",
},
{
name: "clamping below 0.0",
input: RGB{R: -0.5, G: 0.5, B: 0.5},
expected: "#007f7f",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := RGBToHex(tt.input)
if result != tt.expected {
t.Errorf("RGBToHex(%v) = %s, expected %s", tt.input, result, tt.expected)
}
})
}
}
func TestRGBToHSV(t *testing.T) {
tests := []struct {
name string
input RGB
expected HSV
}{
{
name: "black",
input: RGB{R: 0.0, G: 0.0, B: 0.0},
expected: HSV{H: 0.0, S: 0.0, V: 0.0},
},
{
name: "white",
input: RGB{R: 1.0, G: 1.0, B: 1.0},
expected: HSV{H: 0.0, S: 0.0, V: 1.0},
},
{
name: "red",
input: RGB{R: 1.0, G: 0.0, B: 0.0},
expected: HSV{H: 0.0, S: 1.0, V: 1.0},
},
{
name: "green",
input: RGB{R: 0.0, G: 1.0, B: 0.0},
expected: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0},
},
{
name: "blue",
input: RGB{R: 0.0, G: 0.0, B: 1.0},
expected: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := RGBToHSV(tt.input)
if !floatEqual(result.H, tt.expected.H) || !floatEqual(result.S, tt.expected.S) || !floatEqual(result.V, tt.expected.V) {
t.Errorf("RGBToHSV(%v) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestHSVToRGB(t *testing.T) {
tests := []struct {
name string
input HSV
expected RGB
}{
{
name: "black",
input: HSV{H: 0.0, S: 0.0, V: 0.0},
expected: RGB{R: 0.0, G: 0.0, B: 0.0},
},
{
name: "white",
input: HSV{H: 0.0, S: 0.0, V: 1.0},
expected: RGB{R: 1.0, G: 1.0, B: 1.0},
},
{
name: "red",
input: HSV{H: 0.0, S: 1.0, V: 1.0},
expected: RGB{R: 1.0, G: 0.0, B: 0.0},
},
{
name: "green",
input: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0},
expected: RGB{R: 0.0, G: 1.0, B: 0.0},
},
{
name: "blue",
input: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0},
expected: RGB{R: 0.0, G: 0.0, B: 1.0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HSVToRGB(tt.input)
if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) {
t.Errorf("HSVToRGB(%v) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestLuminance(t *testing.T) {
tests := []struct {
name string
input string
expected float64
}{
{
name: "black",
input: "#000000",
expected: 0.0,
},
{
name: "white",
input: "#ffffff",
expected: 1.0,
},
{
name: "red",
input: "#ff0000",
expected: 0.2126,
},
{
name: "green",
input: "#00ff00",
expected: 0.7152,
},
{
name: "blue",
input: "#0000ff",
expected: 0.0722,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Luminance(tt.input)
if !floatEqual(result, tt.expected) {
t.Errorf("Luminance(%s) = %f, expected %f", tt.input, result, tt.expected)
}
})
}
}
func TestContrastRatio(t *testing.T) {
tests := []struct {
name string
fg string
bg string
expected float64
}{
{
name: "black on white",
fg: "#000000",
bg: "#ffffff",
expected: 21.0,
},
{
name: "white on black",
fg: "#ffffff",
bg: "#000000",
expected: 21.0,
},
{
name: "same color",
fg: "#808080",
bg: "#808080",
expected: 1.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ContrastRatio(tt.fg, tt.bg)
if !floatEqual(result, tt.expected) {
t.Errorf("ContrastRatio(%s, %s) = %f, expected %f", tt.fg, tt.bg, result, tt.expected)
}
})
}
}
func TestEnsureContrast(t *testing.T) {
tests := []struct {
name string
color string
bg string
minRatio float64
isLightMode bool
}{
{
name: "already sufficient contrast dark mode",
color: "#ffffff",
bg: "#000000",
minRatio: 4.5,
isLightMode: false,
},
{
name: "already sufficient contrast light mode",
color: "#000000",
bg: "#ffffff",
minRatio: 4.5,
isLightMode: true,
},
{
name: "needs adjustment dark mode",
color: "#404040",
bg: "#1a1a1a",
minRatio: 4.5,
isLightMode: false,
},
{
name: "needs adjustment light mode",
color: "#c0c0c0",
bg: "#f8f8f8",
minRatio: 4.5,
isLightMode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := EnsureContrast(tt.color, tt.bg, tt.minRatio, tt.isLightMode)
actualRatio := ContrastRatio(result, tt.bg)
if actualRatio < tt.minRatio {
t.Errorf("EnsureContrast(%s, %s, %f, %t) = %s with ratio %f, expected ratio >= %f",
tt.color, tt.bg, tt.minRatio, tt.isLightMode, result, actualRatio, tt.minRatio)
}
})
}
}
func TestGeneratePalette(t *testing.T) {
tests := []struct {
name string
base string
opts PaletteOptions
}{
{
name: "dark theme default",
base: "#625690",
opts: PaletteOptions{IsLight: false},
},
{
name: "light theme default",
base: "#625690",
opts: PaletteOptions{IsLight: true},
},
{
name: "light theme with custom background",
base: "#625690",
opts: PaletteOptions{
IsLight: true,
Background: "#fafafa",
},
},
{
name: "dark theme with custom background",
base: "#625690",
opts: PaletteOptions{
IsLight: false,
Background: "#0a0a0a",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts)
colors := []ColorInfo{
result.Color0, result.Color1, result.Color2, result.Color3,
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 {
if len(color.Hex) != 7 || color.Hex[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex)
}
}
if tt.opts.Background != "" && result.Color0.Hex != tt.opts.Background {
t.Errorf("Background color = %s, expected %s", result.Color0.Hex, tt.opts.Background)
} else if !tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result.Color0.Hex)
} else if tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
}
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
}
})
}
}
func TestRoundTripConversion(t *testing.T) {
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
for _, hex := range testColors {
t.Run(hex, func(t *testing.T) {
rgb := HexToRGB(hex)
result := RGBToHex(rgb)
if result != hex {
t.Errorf("Round trip %s -> RGB -> %s failed", hex, result)
}
})
}
}
func TestRGBHSVRoundTrip(t *testing.T) {
testCases := []RGB{
{R: 0.0, G: 0.0, B: 0.0},
{R: 1.0, G: 1.0, B: 1.0},
{R: 1.0, G: 0.0, B: 0.0},
{R: 0.0, G: 1.0, B: 0.0},
{R: 0.0, G: 0.0, B: 1.0},
{R: 0.5, G: 0.5, B: 0.5},
{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412},
}
for _, rgb := range testCases {
t.Run("", func(t *testing.T) {
hsv := RGBToHSV(rgb)
result := HSVToRGB(hsv)
if !floatEqual(result.R, rgb.R) || !floatEqual(result.G, rgb.G) || !floatEqual(result.B, rgb.B) {
t.Errorf("Round trip RGB->HSV->RGB failed: %v -> %v -> %v", rgb, hsv, result)
}
})
}
}
func floatEqual(a, b float64) bool {
return math.Abs(a-b) < 1e-9
}
func TestDeltaPhiStar(t *testing.T) {
tests := []struct {
name string
fg string
bg string
negativePolarity bool
minExpected float64
}{
{
name: "white on black (negative polarity)",
fg: "#ffffff",
bg: "#000000",
negativePolarity: true,
minExpected: 100.0,
},
{
name: "black on white (positive polarity)",
fg: "#000000",
bg: "#ffffff",
negativePolarity: false,
minExpected: 100.0,
},
{
name: "low contrast same color",
fg: "#808080",
bg: "#808080",
negativePolarity: false,
minExpected: -40.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := DeltaPhiStar(tt.fg, tt.bg, tt.negativePolarity)
if result < tt.minExpected {
t.Errorf("DeltaPhiStar(%s, %s, %v) = %f, expected >= %f",
tt.fg, tt.bg, tt.negativePolarity, result, tt.minExpected)
}
})
}
}
func TestDeltaPhiStarContrast(t *testing.T) {
tests := []struct {
name string
fg string
bg string
isLightMode bool
minExpected float64
}{
{
name: "white on black (dark mode)",
fg: "#ffffff",
bg: "#000000",
isLightMode: false,
minExpected: 100.0,
},
{
name: "black on white (light mode)",
fg: "#000000",
bg: "#ffffff",
isLightMode: true,
minExpected: 100.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := DeltaPhiStarContrast(tt.fg, tt.bg, tt.isLightMode)
if result < tt.minExpected {
t.Errorf("DeltaPhiStarContrast(%s, %s, %v) = %f, expected >= %f",
tt.fg, tt.bg, tt.isLightMode, result, tt.minExpected)
}
})
}
}
func TestEnsureContrastDPS(t *testing.T) {
tests := []struct {
name string
color string
bg string
minLc float64
isLightMode bool
}{
{
name: "already sufficient contrast dark mode",
color: "#ffffff",
bg: "#000000",
minLc: 60.0,
isLightMode: false,
},
{
name: "already sufficient contrast light mode",
color: "#000000",
bg: "#ffffff",
minLc: 60.0,
isLightMode: true,
},
{
name: "needs adjustment dark mode",
color: "#404040",
bg: "#1a1a1a",
minLc: 60.0,
isLightMode: false,
},
{
name: "needs adjustment light mode",
color: "#c0c0c0",
bg: "#f8f8f8",
minLc: 60.0,
isLightMode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := EnsureContrastDPS(tt.color, tt.bg, tt.minLc, tt.isLightMode)
actualLc := DeltaPhiStarContrast(result, tt.bg, tt.isLightMode)
if actualLc < tt.minLc {
t.Errorf("EnsureContrastDPS(%s, %s, %f, %t) = %s with Lc %f, expected Lc >= %f",
tt.color, tt.bg, tt.minLc, tt.isLightMode, result, actualLc, tt.minLc)
}
})
}
}
func TestGeneratePaletteWithDPS(t *testing.T) {
tests := []struct {
name string
base string
opts PaletteOptions
}{
{
name: "dark theme with DPS",
base: "#625690",
opts: PaletteOptions{IsLight: false, UseDPS: true},
},
{
name: "light theme with DPS",
base: "#625690",
opts: PaletteOptions{IsLight: true, UseDPS: true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts)
colors := []ColorInfo{
result.Color0, result.Color1, result.Color2, result.Color3,
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 {
if len(color.Hex) != 7 || color.Hex[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex)
}
}
bgColor := result.Color0.Hex
for i := 1; i < 8; i++ {
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
minLc := 30.0
if lc < minLc && lc > 0 {
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
i, colors[i].Hex, lc, bgColor, minLc)
}
}
})
}
}
func TestDeriveContainer(t *testing.T) {
tests := []struct {
name string
primary string
isLight bool
expected string
}{
{
name: "dark mode",
primary: "#ccbdff",
isLight: false,
expected: "#4a3e76",
},
{
name: "light mode",
primary: "#625690",
isLight: true,
expected: "#e7deff",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := DeriveContainer(tt.primary, tt.isLight)
resultRGB := HexToRGB(result)
expectedRGB := HexToRGB(tt.expected)
rDiff := math.Abs(resultRGB.R - expectedRGB.R)
gDiff := math.Abs(resultRGB.G - expectedRGB.G)
bDiff := math.Abs(resultRGB.B - expectedRGB.B)
tolerance := 0.02
if rDiff > tolerance || gDiff > tolerance || bDiff > tolerance {
t.Errorf("DeriveContainer(%s, %v) = %s, expected %s (RGB diff: R:%.4f G:%.4f B:%.4f)",
tt.primary, tt.isLight, result, tt.expected, rDiff, gDiff, bDiff)
}
})
}
}
func TestContrastAlgorithmComparison(t *testing.T) {
base := "#625690"
optsWCAG := PaletteOptions{IsLight: false, UseDPS: false}
optsDPS := PaletteOptions{IsLight: false, UseDPS: true}
paletteWCAG := GeneratePalette(base, optsWCAG)
paletteDPS := GeneratePalette(base, optsDPS)
wcagColors := []ColorInfo{
paletteWCAG.Color0, paletteWCAG.Color1, paletteWCAG.Color2, paletteWCAG.Color3,
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 {
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG.Color0.Hex, paletteDPS.Color0.Hex)
}
differentCount := 0
for i := 0; i < 16; i++ {
if wcagColors[i].Hex != dpsColors[i].Hex {
differentCount++
}
}
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
}
-114
View File
@@ -1,114 +0,0 @@
package dank16
import (
"encoding/json"
"fmt"
"strings"
)
func GenerateJSON(p Palette) string {
marshalled, _ := json.Marshal(p)
return string(marshalled)
}
func GenerateVariantJSON(p VariantPalette) string {
marshalled, _ := json.Marshal(p)
return string(marshalled)
}
func GenerateKittyTheme(p Palette) string {
var result strings.Builder
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
fmt.Fprintf(&result, "color1 %s\n", p.Color1.Hex)
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()
}
func GenerateFootTheme(p Palette) string {
var result strings.Builder
fmt.Fprintf(&result, "regular0=%s\n", p.Color0.HexStripped)
fmt.Fprintf(&result, "regular1=%s\n", p.Color1.HexStripped)
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()
}
func GenerateAlacrittyTheme(p Palette) string {
var result strings.Builder
result.WriteString("[colors.normal]\n")
fmt.Fprintf(&result, "black = '%s'\n", p.Color0.Hex)
fmt.Fprintf(&result, "red = '%s'\n", p.Color1.Hex)
fmt.Fprintf(&result, "green = '%s'\n", p.Color2.Hex)
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color3.Hex)
fmt.Fprintf(&result, "blue = '%s'\n", p.Color4.Hex)
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color5.Hex)
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color6.Hex)
fmt.Fprintf(&result, "white = '%s'\n", p.Color7.Hex)
result.WriteString("\n[colors.bright]\n")
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()
}
func GenerateGhosttyTheme(p Palette) string {
var result strings.Builder
fmt.Fprintf(&result, "palette = 0=%s\n", p.Color0.Hex)
fmt.Fprintf(&result, "palette = 1=%s\n", p.Color1.Hex)
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()
}
func GenerateWeztermTheme(p Palette) string {
var result strings.Builder
fmt.Fprintf(&result, "ansi = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
p.Color0.Hex, p.Color1.Hex, p.Color2.Hex, p.Color3.Hex,
p.Color4.Hex, p.Color5.Hex, p.Color6.Hex, p.Color7.Hex)
fmt.Fprintf(&result, "brights = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
p.Color8.Hex, p.Color9.Hex, p.Color10.Hex, p.Color11.Hex,
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
return result.String()
}
-51
View File
@@ -1,51 +0,0 @@
package deps
import (
"context"
)
type DependencyStatus int
const (
StatusMissing DependencyStatus = iota
StatusInstalled
StatusNeedsUpdate
StatusNeedsReinstall
)
type PackageVariant int
const (
VariantStable PackageVariant = iota
VariantGit
)
type Dependency struct {
Name string
Status DependencyStatus
Version string
Description string
Required bool
Variant PackageVariant
CanToggle bool
}
type WindowManager int
const (
WindowManagerHyprland WindowManager = iota
WindowManagerNiri
)
type Terminal int
const (
TerminalGhostty Terminal = iota
TerminalKitty
TerminalAlacritty
)
type DependencyDetector interface {
DetectDependencies(ctx context.Context, wm WindowManager) ([]Dependency, error)
DetectDependenciesWithTerminal(ctx context.Context, wm WindowManager, terminal Terminal) ([]Dependency, error)
}
-758
View File
@@ -1,758 +0,0 @@
package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func init() {
Register("arch", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("archarm", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("archcraft", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("manjaro", "#35BF5C", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("obarun", "#2494be", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
return NewArchDistribution(config, logChan)
})
}
type ArchDistribution struct {
*BaseDistribution
*ManualPackageInstaller
config DistroConfig
}
func NewArchDistribution(config DistroConfig, logChan chan<- string) *ArchDistribution {
base := NewBaseDistribution(logChan)
return &ArchDistribution{
BaseDistribution: base,
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
config: config,
}
}
func (a *ArchDistribution) GetID() string {
return a.config.ID
}
func (a *ArchDistribution) GetColorHex() string {
return a.config.ColorHex
}
func (a *ArchDistribution) GetFamily() DistroFamily {
return a.config.Family
}
func (a *ArchDistribution) GetPackageManager() PackageManagerType {
return PackageManagerPacman
}
func (a *ArchDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return a.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (a *ArchDistribution) 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, a.detectDMS())
// Terminal with choice support
dependencies = append(dependencies, a.detectSpecificTerminal(terminal))
// Common detections using base methods
dependencies = append(dependencies, a.detectGit())
dependencies = append(dependencies, a.detectWindowManager(wm))
dependencies = append(dependencies, a.detectQuickshell())
dependencies = append(dependencies, a.detectXDGPortal())
dependencies = append(dependencies, a.detectAccountsService())
// Hyprland-specific tools
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, a.detectHyprlandTools()...)
}
// Niri-specific tools
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, a.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, a.detectMatugen())
dependencies = append(dependencies, a.detectDgop())
dependencies = append(dependencies, a.detectClipboardTools()...)
return dependencies, nil
}
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
}
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
}
func (a *ArchDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("pacman", "-Q", pkg)
err := cmd.Run()
return err == nil
}
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
"git": {Name: "git", Repository: RepoTypeSystem},
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
"matugen": a.getMatugenMapping(variants["matugen"]),
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = a.getNiriMapping(variants["niri"])
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
return packages
}
func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
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-git", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
}
func (a *ArchDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
}
func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) PackageMapping {
if runtime.GOARCH == "arm64" {
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
}
if variant == deps.VariantGit {
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
}
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
if forceDMSGit || variant == deps.VariantGit {
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
}
if a.packageInstalled("dms-shell-git") {
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
}
if a.packageInstalled("dms-shell-bin") {
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing
if a.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
}
func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking base-devel...",
IsComplete: false,
LogOutput: "Checking if base-devel is installed",
}
checkCmd := exec.CommandContext(ctx, "pacman", "-Qq", "base-devel")
if err := checkCmd.Run(); err == nil {
a.log("base-devel already installed")
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.10,
Step: "base-devel already installed",
IsComplete: false,
LogOutput: "base-devel is already installed on the system",
}
return nil
}
a.log("Installing base-devel...")
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.08,
Step: "Installing base-devel...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo pacman -S --needed --noconfirm base-devel",
LogOutput: "Installing base-devel development tools",
}
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
return fmt.Errorf("failed to install base-devel: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.12,
Step: "base-devel installation complete",
IsComplete: false,
LogOutput: "base-devel successfully installed",
}
return nil
}
func (a *ArchDistribution) 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 := a.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Phase 3: System Packages
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
}
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install system packages: %w", err)
}
}
// Phase 4: AUR Packages
if len(aurPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d AUR packages...", len(aurPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing AUR packages: %s", strings.Join(aurPkgs, ", ")),
}
if err := a.installAURPackages(ctx, aurPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install AUR packages: %w", err)
}
}
// Phase 5: Manual Builds
if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.85,
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
}
if err := a.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install manual packages: %w", err)
}
}
// Phase 6: Configuration
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
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
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, []string, map[string]deps.PackageVariant) {
systemPkgs := []string{}
aurPkgs := []string{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := a.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
manualPkgs = append(manualPkgs, dep.Name)
continue
}
switch pkgInfo.Repository {
case RepoTypeAUR:
aurPkgs = append(aurPkgs, pkgInfo.Name)
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return systemPkgs, aurPkgs, manualPkgs, variantMap
}
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
hasNiri := false
hasQuickshell := false
for _, pkg := range packages {
if pkg == "niri-git" {
hasNiri = true
}
if pkg == "quickshell" || pkg == "quickshell-git" {
hasQuickshell = true
}
}
// If quickshell is in the list, always reinstall google-breakpad first
if hasQuickshell {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.63,
Step: "Reinstalling google-breakpad for quickshell...",
IsComplete: false,
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
}
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
}
}
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
if hasNiri {
if !a.packageInstalled("makepkg-git-lfs-proto") {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: "Installing makepkg-git-lfs-proto for niri...",
IsComplete: false,
CommandInfo: "Installing prerequisite for niri-git",
}
if err := a.installSingleAURPackage(ctx, "makepkg-git-lfs-proto", sudoPassword, progressChan, 0.65, 0.67); err != nil {
return fmt.Errorf("failed to install makepkg-git-lfs-proto prerequisite for niri: %w", err)
}
}
}
// Reorder packages to ensure dms-shell-git dependencies are installed first
orderedPackages := a.reorderAURPackages(packages)
baseProgress := 0.67
progressStep := 0.13 / float64(len(orderedPackages))
for i, pkg := range orderedPackages {
currentProgress := baseProgress + (float64(i) * progressStep)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: currentProgress,
Step: fmt.Sprintf("Installing AUR package %s (%d/%d)...", pkg, i+1, len(packages)),
IsComplete: false,
CommandInfo: fmt.Sprintf("Building and installing %s", pkg),
}
if err := a.installSingleAURPackage(ctx, pkg, sudoPassword, progressChan, currentProgress, currentProgress+progressStep); err != nil {
return fmt.Errorf("failed to install AUR package %s: %w", pkg, err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.80,
Step: "All AUR packages installed successfully",
IsComplete: false,
LogOutput: fmt.Sprintf("Successfully installed AUR packages: %s", strings.Join(packages, ", ")),
}
return nil
}
func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
dmsDepencies := []string{"quickshell", "quickshell-git", "dgop"}
var deps []string
var others []string
var dmsShell []string
for _, pkg := range packages {
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
dmsShell = append(dmsShell, pkg)
} else {
isDep := false
for _, dep := range dmsDepencies {
if pkg == dep {
deps = append(deps, pkg)
isDep = true
break
}
}
if !isDep {
others = append(others, pkg)
}
}
}
result := append(deps, others...)
result = append(result, dmsShell...)
return result
}
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "aur-builds", pkg)
// Clean up any existing cache first
if err := os.RemoveAll(buildDir); err != nil {
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
}
if err := os.MkdirAll(buildDir, 0755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
defer func() {
if removeErr := os.RemoveAll(buildDir); removeErr != nil {
a.log(fmt.Sprintf("Warning: failed to cleanup build directory %s: %v", buildDir, removeErr))
}
}()
// Clone the AUR package
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.1*(endProgress-startProgress),
Step: fmt.Sprintf("Cloning %s from AUR...", pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("git clone https://aur.archlinux.org/%s.git", pkg),
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", fmt.Sprintf("https://aur.archlinux.org/%s.git", pkg), filepath.Join(buildDir, pkg))
if err := a.runWithProgress(cloneCmd, progressChan, PhaseAURPackages, startProgress+0.1*(endProgress-startProgress), startProgress+0.2*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to clone %s: %w", pkg, err)
}
packageDir := filepath.Join(buildDir, pkg)
if pkg == "niri-git" {
pkgbuildPath := filepath.Join(packageDir, "PKGBUILD")
sedCmd := exec.CommandContext(ctx, "sed", "-i", "s/makepkg-git-lfs-proto//g", pkgbuildPath)
if err := sedCmd.Run(); err != nil {
return fmt.Errorf("failed to patch PKGBUILD for niri-git: %w", err)
}
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
sedCmd2 := exec.CommandContext(ctx, "sed", "-i", "/makedepends = makepkg-git-lfs-proto/d", srcinfoPath)
if err := sedCmd2.Run(); err != nil {
return fmt.Errorf("failed to patch .SRCINFO for niri-git: %w", err)
}
}
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
depsToRemove := []string{
"depends = quickshell",
"depends = dgop",
}
for _, dep := range depsToRemove {
sedCmd := exec.CommandContext(ctx, "sed", "-i", fmt.Sprintf("/%s/d", dep), srcinfoPath)
if err := sedCmd.Run(); err != nil {
return fmt.Errorf("failed to remove dependency %s from .SRCINFO for %s: %w", dep, pkg, err)
}
}
}
// Remove all optdepends from .SRCINFO for all packages
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
optdepsCmd := exec.CommandContext(ctx, "sed", "-i", "/^[[:space:]]*optdepends = /d", srcinfoPath)
if err := optdepsCmd.Run(); err != nil {
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
}
// Skip dependency installation for dms-shell-git and dms-shell-bin
// since we manually manage those dependencies
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
// Pre-install dependencies from .SRCINFO
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
IsComplete: false,
CommandInfo: "Installing package dependencies and makedepends",
}
// Install dependencies and makedepends explicitly
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
if [[ "%s" == *"quickshell"* ]]; then
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
fi
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi
`, srcinfoPath, pkg, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
}
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
if [ ! -z "$makedeps" ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
fi
`, srcinfoPath, sudoPassword))
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
}
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.4*(endProgress-startProgress),
Step: fmt.Sprintf("Building %s...", pkg),
IsComplete: false,
CommandInfo: "makepkg --noconfirm",
}
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
buildCmd.Dir = packageDir
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to build %s: %w", pkg, err)
}
// Find built package file
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.7*(endProgress-startProgress),
Step: fmt.Sprintf("Installing %s...", pkg),
IsComplete: false,
CommandInfo: "sudo pacman -U built-package",
}
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
var files []string
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
// For DMS split packages, install base package
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
matches, err := filepath.Glob(pattern)
if err == nil {
for _, match := range matches {
basename := filepath.Base(match)
// Always include base package
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
files = append(files, match)
}
}
}
// Also update compositor-specific packages if they're installed
if strings.HasSuffix(pkg, "-git") {
if a.packageInstalled("dms-shell-hyprland-git") {
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
files = append(files, hyprlandMatches[0])
}
}
if a.packageInstalled("dms-shell-niri-git") {
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
files = append(files, niriMatches[0])
}
}
}
} else {
// For other packages, install all built packages
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
files = matches
}
if len(files) == 0 {
return fmt.Errorf("no package files found after building %s", pkg)
}
installArgs := []string{"pacman", "-U", "--noconfirm"}
installArgs = append(installArgs, files...)
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
fileNames := make([]string, len(files))
for i, f := range files {
fileNames[i] = filepath.Base(f)
}
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.7*(endProgress-startProgress),
LogOutput: fmt.Sprintf("Installing packages: %s", strings.Join(fileNames, ", ")),
}
if err := a.runWithProgress(installCmd, progressChan, PhaseAURPackages, startProgress+0.7*(endProgress-startProgress), endProgress); err != nil {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress,
LogOutput: fmt.Sprintf("ERROR: pacman -U failed for %s with error: %v", pkg, err),
Error: err,
}
return fmt.Errorf("failed to install built package %s: %w", pkg, err)
}
a.log(fmt.Sprintf("Successfully installed AUR package: %s", pkg))
return nil
}
-748
View File
@@ -1,748 +0,0 @@
package distros
import (
"bufio"
"context"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
)
const (
forceQuickshellGit = false
forceDMSGit = false
)
// BaseDistribution provides common functionality for all distributions
type BaseDistribution struct {
logChan chan<- string
}
// NewBaseDistribution creates a new base distribution
func NewBaseDistribution(logChan chan<- string) *BaseDistribution {
return &BaseDistribution{
logChan: logChan,
}
}
// Common helper methods
func (b *BaseDistribution) commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func (b *BaseDistribution) CommandExists(cmd string) bool {
return b.commandExists(cmd)
}
func (b *BaseDistribution) log(message string) {
if b.logChan != nil {
b.logChan <- message
}
}
func (b *BaseDistribution) logError(message string, err error) {
errorMsg := fmt.Sprintf("ERROR: %s: %v", message, err)
b.log(errorMsg)
}
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
func escapeSingleQuotes(s string) string {
return strings.ReplaceAll(s, "'", "'\\''")
}
// MakeSudoCommand creates a command string that safely passes password to sudo.
// This helper escapes special characters in the password to prevent shell injection
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
func MakeSudoCommand(sudoPassword string, command string) string {
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
}
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
// The password is properly escaped to prevent shell injection and syntax errors.
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
cmdStr := MakeSudoCommand(sudoPassword, command)
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
}
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
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 {
return b.detectCommand("git", "Version control system")
}
func (b *BaseDistribution) detectMatugen() deps.Dependency {
return b.detectCommand("matugen", "Material Design color generation tool")
}
func (b *BaseDistribution) detectDgop() deps.Dependency {
return b.detectCommand("dgop", "Desktop portal management tool")
}
func (b *BaseDistribution) detectDMS() deps.Dependency {
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
status := deps.StatusMissing
currentVersion := ""
if _, err := os.Stat(dmsPath); err == nil {
status = deps.StatusInstalled
// Only get current version, don't check for updates (lazy loading)
current, err := version.GetCurrentDMSVersion()
if err == nil {
currentVersion = current
}
}
dep := deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Description: "Desktop Management System configuration",
Required: true,
CanToggle: true,
}
if currentVersion != "" {
dep.Version = currentVersion
}
return dep
}
func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.Dependency {
switch terminal {
case deps.TerminalGhostty:
status := deps.StatusMissing
if b.commandExists("ghostty") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "ghostty",
Status: status,
Description: "A fast, native terminal emulator built in Zig.",
Required: true,
}
case deps.TerminalKitty:
status := deps.StatusMissing
if b.commandExists("kitty") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "kitty",
Status: status,
Description: "A feature-rich, customizable terminal emulator.",
Required: true,
}
case deps.TerminalAlacritty:
status := deps.StatusMissing
if b.commandExists("alacritty") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "alacritty",
Status: status,
Description: "A simple terminal emulator. (No dynamic theming)",
Required: true,
}
default:
return b.detectSpecificTerminal(deps.TerminalGhostty)
}
}
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
var dependencies []deps.Dependency
cliphist := deps.StatusMissing
if b.commandExists("cliphist") {
cliphist = deps.StatusInstalled
}
wlClipboard := deps.StatusMissing
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
wlClipboard = deps.StatusInstalled
}
dependencies = append(dependencies,
deps.Dependency{
Name: "cliphist",
Status: cliphist,
Description: "Wayland clipboard manager",
Required: true,
},
deps.Dependency{
Name: "wl-clipboard",
Status: wlClipboard,
Description: "Wayland clipboard utilities",
Required: true,
},
)
return dependencies
}
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
var dependencies []deps.Dependency
tools := []struct {
name string
description string
}{
{"hyprctl", "Hyprland control utility"},
{"jq", "JSON processor"},
}
for _, tool := range tools {
status := deps.StatusMissing
if b.commandExists(tool.name) {
status = deps.StatusInstalled
}
dependencies = append(dependencies, deps.Dependency{
Name: tool.name,
Status: status,
Description: tool.description,
Required: true,
})
}
return dependencies
}
func (b *BaseDistribution) detectQuickshell() deps.Dependency {
if !b.commandExists("qs") {
return deps.Dependency{
Name: "quickshell",
Status: deps.StatusMissing,
Description: "QtQuick based desktop shell toolkit",
Required: true,
Variant: deps.VariantStable,
CanToggle: true,
}
}
cmd := exec.Command("qs", "--version")
output, err := cmd.Output()
if err != nil {
return deps.Dependency{
Name: "quickshell",
Status: deps.StatusNeedsReinstall,
Description: "QtQuick based desktop shell toolkit (version check failed)",
Required: true,
Variant: deps.VariantStable,
CanToggle: true,
}
}
versionStr := string(output)
versionRegex := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
matches := versionRegex.FindStringSubmatch(versionStr)
if len(matches) < 2 {
return deps.Dependency{
Name: "quickshell",
Status: deps.StatusNeedsReinstall,
Description: "QtQuick based desktop shell toolkit (unknown version)",
Required: true,
Variant: deps.VariantStable,
CanToggle: true,
}
}
version := matches[1]
variant := deps.VariantStable
if strings.Contains(versionStr, "git") || strings.Contains(versionStr, "+") {
variant = deps.VariantGit
}
if b.versionCompare(version, "0.2.0") >= 0 {
return deps.Dependency{
Name: "quickshell",
Status: deps.StatusInstalled,
Version: version,
Description: "QtQuick based desktop shell toolkit",
Required: true,
Variant: variant,
CanToggle: true,
}
}
return deps.Dependency{
Name: "quickshell",
Status: deps.StatusNeedsUpdate,
Variant: variant,
CanToggle: true,
Version: version,
Description: "QtQuick based desktop shell toolkit (needs 0.2.0+)",
Required: true,
}
}
func (b *BaseDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
switch wm {
case deps.WindowManagerHyprland:
status := deps.StatusMissing
variant := deps.VariantStable
version := ""
if b.commandExists("hyprland") || b.commandExists("Hyprland") {
status = deps.StatusInstalled
cmd := exec.Command("hyprctl", "version")
if output, err := cmd.Output(); err == nil {
outStr := string(output)
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
variant = deps.VariantGit
}
if versionRegex := regexp.MustCompile(`v(\d+\.\d+\.\d+)`); versionRegex.MatchString(outStr) {
matches := versionRegex.FindStringSubmatch(outStr)
if len(matches) > 1 {
version = matches[1]
}
}
}
}
return deps.Dependency{
Name: "hyprland",
Status: status,
Version: version,
Description: "Dynamic tiling Wayland compositor",
Required: true,
Variant: variant,
CanToggle: true,
}
case deps.WindowManagerNiri:
status := deps.StatusMissing
variant := deps.VariantStable
version := ""
if b.commandExists("niri") {
status = deps.StatusInstalled
cmd := exec.Command("niri", "--version")
if output, err := cmd.Output(); err == nil {
outStr := string(output)
if strings.Contains(outStr, "git") || strings.Contains(outStr, "+") {
variant = deps.VariantGit
}
if versionRegex := regexp.MustCompile(`niri (\d+\.\d+)`); versionRegex.MatchString(outStr) {
matches := versionRegex.FindStringSubmatch(outStr)
if len(matches) > 1 {
version = matches[1]
}
}
}
}
return deps.Dependency{
Name: "niri",
Status: status,
Version: version,
Description: "Scrollable-tiling Wayland compositor",
Required: true,
Variant: variant,
CanToggle: true,
}
default:
return deps.Dependency{
Name: "unknown-wm",
Status: deps.StatusMissing,
Description: "Unknown window manager",
Required: true,
}
}
}
// Version comparison helper
func (b *BaseDistribution) versionCompare(v1, v2 string) int {
parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")
for i := 0; i < len(parts1) && i < len(parts2); i++ {
if parts1[i] < parts2[i] {
return -1
}
if parts1[i] > parts2[i] {
return 1
}
}
if len(parts1) < len(parts2) {
return -1
}
if len(parts1) > len(parts2) {
return 1
}
return 0
}
// Common installation helper
func (b *BaseDistribution) runWithProgress(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64) error {
return b.runWithProgressTimeout(cmd, progressChan, phase, startProgress, endProgress, 20*time.Minute)
}
func (b *BaseDistribution) runWithProgressTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, timeout time.Duration) error {
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, "Installing...", timeout)
}
func (b *BaseDistribution) runWithProgressStep(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string) error {
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, stepMessage, 20*time.Minute)
}
func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string, timeoutDuration time.Duration) error {
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to create stdout pipe: %w", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("failed to create stderr pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return err
}
outputChan := make(chan string, 100)
done := make(chan error, 1)
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
b.log(line)
outputChan <- line
}
}()
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
line := scanner.Text()
b.log(line)
outputChan <- line
}
}()
go func() {
done <- cmd.Wait()
close(outputChan)
}()
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
progress := startProgress
progressStep := (endProgress - startProgress) / 50
lastOutput := ""
var timeout *time.Timer
var timeoutChan <-chan time.Time
if timeoutDuration > 0 {
timeout = time.NewTimer(timeoutDuration)
defer timeout.Stop()
timeoutChan = timeout.C
}
for {
select {
case err := <-done:
if err != nil {
b.logError("Command execution failed", err)
b.log(fmt.Sprintf("Last output before failure: %s", lastOutput))
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: startProgress,
Step: "Command failed",
IsComplete: false,
LogOutput: lastOutput,
Error: err,
}
return err
}
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: endProgress,
Step: "Installation step complete",
IsComplete: false,
LogOutput: lastOutput,
}
return nil
case output, ok := <-outputChan:
if ok {
lastOutput = output
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: progress,
Step: stepMessage,
IsComplete: false,
LogOutput: output,
}
if timeout != nil {
timeout.Reset(timeoutDuration)
}
}
case <-timeoutChan:
if cmd.Process != nil {
cmd.Process.Kill()
}
err := fmt.Errorf("installation timed out after %v", timeoutDuration)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: startProgress,
Step: "Installation timed out",
IsComplete: false,
LogOutput: lastOutput,
Error: err,
}
return err
case <-ticker.C:
if progress < endProgress-0.01 {
progress += progressStep
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: progress,
Step: "Installing...",
IsComplete: false,
LogOutput: lastOutput,
}
}
}
}
}
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
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
b.log("Installing/updating DMS binary...")
// Detect architecture
arch := runtime.GOARCH
switch arch {
case "amd64":
case "arm64":
default:
return fmt.Errorf("unsupported architecture for DMS: %s", arch)
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.80,
Step: "Downloading DMS binary...",
IsComplete: false,
CommandInfo: fmt.Sprintf("Downloading dms-%s.gz", arch),
}
// Get latest release version
latestVersionCmd := exec.CommandContext(ctx, "bash", "-c",
`curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'`)
versionOutput, err := latestVersionCmd.Output()
if err != nil {
return fmt.Errorf("failed to get latest DMS version: %w", err)
}
version := strings.TrimSpace(string(versionOutput))
if version == "" {
return fmt.Errorf("could not determine latest DMS version")
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
// Download the gzipped binary
downloadURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch)
gzPath := filepath.Join(tmpDir, "dms.gz")
downloadCmd := exec.CommandContext(ctx, "curl", "-L", downloadURL, "-o", gzPath)
if err := downloadCmd.Run(); err != nil {
return fmt.Errorf("failed to download DMS binary: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.85,
Step: "Extracting DMS binary...",
IsComplete: false,
CommandInfo: "gunzip dms.gz",
}
// Extract the binary
extractCmd := exec.CommandContext(ctx, "gunzip", gzPath)
if err := extractCmd.Run(); err != nil {
return fmt.Errorf("failed to extract DMS binary: %w", err)
}
binaryPath := filepath.Join(tmpDir, "dms")
// Make it executable
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", binaryPath)
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to make DMS binary executable: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.88,
Step: "Installing DMS to /usr/local/bin...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cp dms /usr/local/bin/",
}
// Install to /usr/local/bin
installCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install DMS binary: %w", err)
}
b.log("DMS binary installed successfully")
return nil
}
-216
View File
@@ -1,216 +0,0 @@
package distros
import (
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
tempDir := t.TempDir()
os.Setenv("HOME", tempDir)
logChan := make(chan string, 10)
defer close(logChan)
base := NewBaseDistribution(logChan)
dep := base.detectDMS()
if dep.Status != deps.StatusMissing {
t.Errorf("Expected StatusMissing, got %d", dep.Status)
}
if dep.Name != "dms (DankMaterialShell)" {
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
}
if !dep.Required {
t.Error("Expected Required to be true")
}
}
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
if !utils.CommandExists("git") {
t.Skip("git not available")
}
tempDir := t.TempDir()
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
os.MkdirAll(dmsPath, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", tempDir)
exec.Command("git", "init", dmsPath).Run()
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
testFile := filepath.Join(dmsPath, "test.txt")
os.WriteFile(testFile, []byte("test"), 0644)
exec.Command("git", "-C", dmsPath, "add", ".").Run()
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
logChan := make(chan string, 10)
defer close(logChan)
base := NewBaseDistribution(logChan)
dep := base.detectDMS()
if dep.Status == deps.StatusMissing {
t.Error("Expected DMS to be detected as installed")
}
if dep.Name != "dms (DankMaterialShell)" {
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
}
if !dep.Required {
t.Error("Expected Required to be true")
}
t.Logf("Status: %d, Version: %s", dep.Status, dep.Version)
}
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
if !utils.CommandExists("git") {
t.Skip("git not available")
}
tempDir := t.TempDir()
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
os.MkdirAll(dmsPath, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", tempDir)
exec.Command("git", "init", dmsPath).Run()
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
testFile := filepath.Join(dmsPath, "test.txt")
os.WriteFile(testFile, []byte("test"), 0644)
exec.Command("git", "-C", dmsPath, "add", ".").Run()
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
exec.Command("git", "-C", dmsPath, "checkout", "v0.0.1").Run()
logChan := make(chan string, 10)
defer close(logChan)
base := NewBaseDistribution(logChan)
dep := base.detectDMS()
if dep.Name != "dms (DankMaterialShell)" {
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
}
if !dep.Required {
t.Error("Expected Required to be true")
}
t.Logf("Status: %d, Version: %s", dep.Status, dep.Version)
}
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
tempDir := t.TempDir()
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
os.MkdirAll(dmsPath, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", tempDir)
logChan := make(chan string, 10)
defer close(logChan)
base := NewBaseDistribution(logChan)
dep := base.detectDMS()
if dep.Status == deps.StatusMissing {
t.Error("Expected DMS to be detected as present")
}
if dep.Name != "dms (DankMaterialShell)" {
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
}
if !dep.Required {
t.Error("Expected Required to be true")
}
}
func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
logChan := make(chan string, 10)
defer close(logChan)
base := NewBaseDistribution(logChan)
if base == nil {
t.Fatal("NewBaseDistribution returned nil")
}
if base.logChan == nil {
t.Error("logChan was not set")
}
}
func TestBaseDistribution_versionCompare(t *testing.T) {
logChan := make(chan string, 10)
defer close(logChan)
base := NewBaseDistribution(logChan)
tests := []struct {
v1 string
v2 string
expected int
}{
{"0.1.0", "0.1.0", 0},
{"0.1.0", "0.1.1", -1},
{"0.1.1", "0.1.0", 1},
{"0.2.0", "0.1.9", 1},
{"1.0.0", "0.9.9", 1},
}
for _, tt := range tests {
result := base.versionCompare(tt.v1, tt.v2)
if result != tt.expected {
t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected)
}
}
}
func TestBaseDistribution_versionCompare_WithPrefix(t *testing.T) {
logChan := make(chan string, 10)
defer close(logChan)
base := NewBaseDistribution(logChan)
tests := []struct {
v1 string
v2 string
expected int
}{
{"v0.1.0", "v0.1.0", 0},
{"v0.1.0", "v0.1.1", -1},
{"v0.1.1", "v0.1.0", 1},
}
for _, tt := range tests {
result := base.versionCompare(tt.v1, tt.v2)
if result != tt.expected {
t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected)
}
}
}
-649
View File
@@ -1,649 +0,0 @@
package distros
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func init() {
Register("debian", "#A80030", FamilyDebian, func(config DistroConfig, logChan chan<- string) Distribution {
return NewDebianDistribution(config, logChan)
})
}
type DebianDistribution struct {
*BaseDistribution
*ManualPackageInstaller
config DistroConfig
}
func NewDebianDistribution(config DistroConfig, logChan chan<- string) *DebianDistribution {
base := NewBaseDistribution(logChan)
return &DebianDistribution{
BaseDistribution: base,
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
config: config,
}
}
func (d *DebianDistribution) GetID() string {
return d.config.ID
}
func (d *DebianDistribution) GetColorHex() string {
return d.config.ColorHex
}
func (d *DebianDistribution) GetFamily() DistroFamily {
return d.config.Family
}
func (d *DebianDistribution) GetPackageManager() PackageManagerType {
return PackageManagerAPT
}
func (d *DebianDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return d.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
dependencies = append(dependencies, d.detectDMS())
dependencies = append(dependencies, d.detectSpecificTerminal(terminal))
dependencies = append(dependencies, d.detectGit())
dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectAccountsService())
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, d.detectXwaylandSatellite())
}
dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectClipboardTools()...)
return dependencies, nil
}
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
return d.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", d.packageInstalled("xdg-desktop-portal-gtk"))
}
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
return d.detectCommand("xwayland-satellite", "Xwayland support")
}
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
}
func (d *DebianDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run()
return err == nil
}
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{
// Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
// DMS packages from OBS with variant support
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
if wm == deps.WindowManagerNiri {
niriVariant := variants["niri"]
packages["niri"] = d.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = d.getXwaylandSatelliteMapping(niriVariant)
}
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 {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Updating package lists...",
IsComplete: false,
LogOutput: "Updating APT package lists",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
return fmt.Errorf("failed to update package lists: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.08,
Step: "Installing build-essential...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install -y build-essential",
LogOutput: "Installing build tools",
}
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.10,
Step: "Installing development dependencies...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
LogOutput: "Installing additional development tools",
}
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")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.12,
Step: "Prerequisites installation complete",
IsComplete: false,
LogOutput: "Prerequisites successfully installed",
}
return nil
}
func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if err := d.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, obsPkgs, 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 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
}
if err := d.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install APT packages: %w", err)
}
}
// 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 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.80,
Step: "Installing build dependencies...",
IsComplete: false,
LogOutput: "Installing build tools for manual compilation",
}
if err := d.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install build dependencies: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.85,
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
}
if err := d.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install manual packages: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
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{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
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) {
systemPkgs := []string{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := d.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
d.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return systemPkgs, obsPkgs, 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 {
if len(packages) == 0 {
return nil
}
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
buildDeps := make(map[string]bool)
for _, pkg := range manualPkgs {
switch pkg {
case "niri":
buildDeps["curl"] = true
buildDeps["libxkbcommon-dev"] = true
buildDeps["libwayland-dev"] = true
buildDeps["libudev-dev"] = true
buildDeps["libinput-dev"] = true
buildDeps["libdisplay-info-dev"] = true
buildDeps["libpango1.0-dev"] = true
buildDeps["libcairo-dev"] = true
buildDeps["libpipewire-0.3-dev"] = true
buildDeps["libc6-dev"] = true
buildDeps["clang"] = true
buildDeps["libseat-dev"] = true
buildDeps["libgbm-dev"] = true
buildDeps["alacritty"] = true
buildDeps["fuzzel"] = true
case "quickshell":
buildDeps["qt6-base-dev"] = true
buildDeps["qt6-base-private-dev"] = true
buildDeps["qt6-declarative-dev"] = true
buildDeps["qt6-declarative-private-dev"] = true
buildDeps["qt6-wayland-dev"] = true
buildDeps["qt6-wayland-private-dev"] = true
buildDeps["qt6-tools-dev"] = true
buildDeps["libqt6svg6-dev"] = true
buildDeps["qt6-shadertools-dev"] = true
buildDeps["spirv-tools"] = true
buildDeps["libcli11-dev"] = true
buildDeps["libjemalloc-dev"] = true
buildDeps["libwayland-dev"] = true
buildDeps["wayland-protocols"] = true
buildDeps["libdrm-dev"] = true
buildDeps["libgbm-dev"] = true
buildDeps["libegl-dev"] = true
buildDeps["libgles2-mesa-dev"] = true
buildDeps["libgl1-mesa-dev"] = true
buildDeps["libxcb1-dev"] = true
buildDeps["libpipewire-0.3-dev"] = true
buildDeps["libpam0g-dev"] = true
case "ghostty":
buildDeps["curl"] = true
case "matugen":
buildDeps["curl"] = true
}
}
for _, pkg := range manualPkgs {
switch pkg {
case "niri", "matugen":
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Rust: %w", err)
}
case "cliphist", "dgop":
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err)
}
}
}
if len(buildDeps) == 0 {
return nil
}
depList := make([]string, 0, len(buildDeps))
for dep := range buildDeps {
depList = append(depList, dep)
}
args := []string{"apt-get", "install", "-y"}
args = append(args, depList...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
}
func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if d.commandExists("cargo") {
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.82,
Step: "Installing rustup...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install rustup",
}
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.83,
Step: "Installing stable Rust toolchain...",
IsComplete: false,
CommandInfo: "rustup install stable",
}
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
if err := d.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
return fmt.Errorf("failed to install Rust toolchain: %w", err)
}
if !d.commandExists("cargo") {
d.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
}
return nil
}
func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if d.commandExists("go") {
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.87,
Step: "Installing Go...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install golang-go",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
}
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
d.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
for _, pkg := range packages {
switch pkg {
default:
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err)
}
}
}
return nil
}
-19
View File
@@ -1,19 +0,0 @@
package distros
import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
// NewDependencyDetector creates a DependencyDetector for the specified distribution
func NewDependencyDetector(distribution string, logChan chan<- string) (deps.DependencyDetector, error) {
distro, err := NewDistribution(distribution, logChan)
if err != nil {
return nil, err
}
return distro, nil
}
// NewPackageInstaller creates a Distribution for package installation
func NewPackageInstaller(distribution string, logChan chan<- string) (Distribution, error) {
return NewDistribution(distribution, logChan)
}
-535
View File
@@ -1,535 +0,0 @@
package distros
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func init() {
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
}
type FedoraDistribution struct {
*BaseDistribution
*ManualPackageInstaller
config DistroConfig
}
func NewFedoraDistribution(config DistroConfig, logChan chan<- string) *FedoraDistribution {
base := NewBaseDistribution(logChan)
return &FedoraDistribution{
BaseDistribution: base,
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
config: config,
}
}
func (f *FedoraDistribution) GetID() string {
return f.config.ID
}
func (f *FedoraDistribution) GetColorHex() string {
return f.config.ColorHex
}
func (f *FedoraDistribution) GetFamily() DistroFamily {
return f.config.Family
}
func (f *FedoraDistribution) GetPackageManager() PackageManagerType {
return PackageManagerDNF
}
func (f *FedoraDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return f.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (f *FedoraDistribution) 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, f.detectDMS())
// Terminal with choice support
dependencies = append(dependencies, f.detectSpecificTerminal(terminal))
// Common detections using base methods
dependencies = append(dependencies, f.detectGit())
dependencies = append(dependencies, f.detectWindowManager(wm))
dependencies = append(dependencies, f.detectQuickshell())
dependencies = append(dependencies, f.detectXDGPortal())
dependencies = append(dependencies, f.detectAccountsService())
// Hyprland-specific tools
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, f.detectHyprlandTools()...)
}
// Niri-specific tools
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, f.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, f.detectMatugen())
dependencies = append(dependencies, f.detectDgop())
dependencies = append(dependencies, f.detectClipboardTools()...)
return dependencies, nil
}
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
return f.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", f.packageInstalled("xdg-desktop-portal-gtk"))
}
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("rpm", "-q", pkg)
err := cmd.Run()
return err == nil
}
func (f *FedoraDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return f.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
// Standard DNF packages
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
// COPR packages
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = f.getNiriMapping(variants["niri"])
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
return packages
}
func (f *FedoraDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
}
func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms-git"}
}
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
}
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
}
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
}
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
}
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing
if f.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
}
func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing
if f.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 (f *FedoraDistribution) getPrerequisites() []string {
return []string{
"dnf-plugins-core",
"make",
"unzip",
"libwayland-server",
}
}
func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
prerequisites := f.getPrerequisites()
var missingPkgs []string
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking prerequisites...",
IsComplete: false,
LogOutput: "Checking prerequisite packages",
}
for _, pkg := range prerequisites {
checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg)
if err := checkCmd.Run(); err != nil {
missingPkgs = append(missingPkgs, pkg)
}
}
_, err := exec.LookPath("go")
if err != nil {
f.log("go not found in PATH, will install golang-bin")
missingPkgs = append(missingPkgs, "golang-bin")
} else {
f.log("go already available in PATH")
}
if len(missingPkgs) == 0 {
f.log("All prerequisites already installed")
return nil
}
f.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.08,
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo dnf install -y %s", strings.Join(missingPkgs, " ")),
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
}
args := []string{"dnf", "install", "-y"}
args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput()
if err != nil {
f.logError("failed to install prerequisites", err)
f.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
return fmt.Errorf("failed to install prerequisites: %w", err)
}
f.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
return nil
}
func (f *FedoraDistribution) 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 := f.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
dnfPkgs, coprPkgs, manualPkgs, variantMap := f.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Phase 2: Enable COPR repositories
if len(coprPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling COPR repositories...",
IsComplete: false,
LogOutput: "Setting up COPR repositories for additional packages",
}
if err := f.enableCOPRRepos(ctx, coprPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable COPR repositories: %w", err)
}
}
// Phase 3: System Packages (DNF)
if len(dnfPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d system packages...", len(dnfPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(dnfPkgs, ", ")),
}
if err := f.installDNFPackages(ctx, dnfPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install DNF packages: %w", err)
}
}
// Phase 4: COPR Packages
coprPkgNames := f.extractPackageNames(coprPkgs)
if len(coprPkgNames) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, // Reusing AUR phase for COPR
Progress: 0.65,
Step: fmt.Sprintf("Installing %d COPR packages...", len(coprPkgNames)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing COPR packages: %s", strings.Join(coprPkgNames, ", ")),
}
if err := f.installCOPRPackages(ctx, coprPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install COPR packages: %w", err)
}
}
// Phase 5: Manual Builds
if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.85,
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
}
if err := f.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install manual packages: %w", err)
}
}
// Phase 6: Configuration
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
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
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (f *FedoraDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
dnfPkgs := []string{}
coprPkgs := []PackageMapping{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := f.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
f.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeSystem:
dnfPkgs = append(dnfPkgs, pkgInfo.Name)
case RepoTypeCOPR:
coprPkgs = append(coprPkgs, pkgInfo)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return dnfPkgs, coprPkgs, manualPkgs, variantMap
}
func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
for _, pkg := range coprPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
f.log(fmt.Sprintf("Enabling COPR repository: %s", pkg.RepoURL))
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Enabling COPR repo %s...", pkg.RepoURL),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
}
cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
output, err := cmd.CombinedOutput()
if err != nil {
f.logError(fmt.Sprintf("failed to enable COPR repo %s", pkg.RepoURL), err)
f.log(fmt.Sprintf("COPR enable command output: %s", string(output)))
return fmt.Errorf("failed to enable COPR repo %s: %w", pkg.RepoURL, err)
}
f.log(fmt.Sprintf("COPR repo %s enabled successfully: %s", pkg.RepoURL, string(output)))
enabledRepos[pkg.RepoURL] = true
// Special handling for niri COPR repo - set priority=1
if pkg.RepoURL == "yalter/niri-git" {
f.log("Setting priority=1 for niri-git COPR repo...")
repoFile := "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:yalter:niri-git.repo"
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.22,
Step: "Setting niri COPR repo priority...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
}
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
priorityOutput, err := priorityCmd.CombinedOutput()
if err != nil {
f.logError("failed to set niri COPR repo priority", err)
f.log(fmt.Sprintf("Priority command output: %s", string(priorityOutput)))
return fmt.Errorf("failed to set niri COPR repo priority: %w", err)
}
f.log(fmt.Sprintf("niri COPR repo priority set successfully: %s", string(priorityOutput)))
}
}
}
return nil
}
func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
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...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
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...)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.70,
Step: "Installing COPR packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
}
-703
View File
@@ -1,703 +0,0 @@
package distros
import (
"context"
"fmt"
"os/exec"
"runtime"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
var GentooGlobalUseFlags = []string{
"dbus",
"udev",
"alsa",
"policykit",
"jpeg",
"png",
"webp",
"gif",
"tiff",
"svg",
"brotli",
"gdbm",
"accessibility",
"gtk",
"qt6",
"egl",
"gbm",
}
func init() {
Register("gentoo", "#54487A", FamilyGentoo, func(config DistroConfig, logChan chan<- string) Distribution {
return NewGentooDistribution(config, logChan)
})
}
type GentooDistribution struct {
*BaseDistribution
*ManualPackageInstaller
config DistroConfig
skipGlobalUseFlags bool
}
func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDistribution {
base := NewBaseDistribution(logChan)
return &GentooDistribution{
BaseDistribution: base,
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
config: config,
}
}
func (g *GentooDistribution) getArchKeyword() string {
arch := runtime.GOARCH
switch arch {
case "amd64":
return "~amd64"
case "arm64":
return "~arm64"
default:
return "~amd64"
}
}
func (g *GentooDistribution) GetID() string {
return g.config.ID
}
func (g *GentooDistribution) GetColorHex() string {
return g.config.ColorHex
}
func (g *GentooDistribution) GetFamily() DistroFamily {
return g.config.Family
}
func (g *GentooDistribution) GetPackageManager() PackageManagerType {
return PackageManagerPortage
}
func (g *GentooDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return g.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
dependencies = append(dependencies, g.detectDMS())
dependencies = append(dependencies, g.detectSpecificTerminal(terminal))
dependencies = append(dependencies, g.detectGit())
dependencies = append(dependencies, g.detectWindowManager(wm))
dependencies = append(dependencies, g.detectQuickshell())
dependencies = append(dependencies, g.detectXDGPortal())
dependencies = append(dependencies, g.detectAccountsService())
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, g.detectHyprlandTools()...)
}
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, g.detectXwaylandSatellite())
}
dependencies = append(dependencies, g.detectMatugen())
dependencies = append(dependencies, g.detectDgop())
dependencies = append(dependencies, g.detectClipboardTools()...)
return dependencies, nil
}
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"))
}
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
return g.detectPackage("xwayland-satellite", "Xwayland support", g.packageInstalled("gui-apps/xwayland-satellite"))
}
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
return g.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", g.packageInstalled("sys-apps/accountsservice"))
}
func (g *GentooDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("qlist", "-I", pkg)
err := cmd.Run()
return err == nil
}
func (g *GentooDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return g.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
archKeyword := g.getArchKeyword()
packages := map[string]PackageMapping{
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
"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"},
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
"qtwayland": {Name: "dev-qt/qtwayland", Repository: RepoTypeSystem},
"mesa": {Name: "media-libs/mesa", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = g.getNiriMapping(variants["niri"])
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
}
return packages
}
func (g *GentooDistribution) getQuickshellMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-apps/quickshell", Repository: RepoTypeGURU, UseFlags: "breakpad jemalloc sockets wayland layer-shell session-lock toplevel-management screencopy X pipewire tray mpris pam hyprland hyprland-global-shortcuts hyprland-focus-grab i3 i3-ipc bluetooth", AcceptKeywords: "**"}
}
func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
}
func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()}
}
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
}
func (g *GentooDistribution) getPrerequisites() []string {
return []string{
"app-eselect/eselect-repository",
"dev-vcs/git",
"dev-build/make",
"app-arch/unzip",
"dev-util/pkgconf",
"dev-qt/qtdeclarative",
}
}
func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword string) error {
useFlags := strings.Join(GentooGlobalUseFlags, " ")
checkCmd := exec.CommandContext(ctx, "grep", "-q", "^USE=", "/etc/portage/make.conf")
hasUse := checkCmd.Run() == nil
var cmd *exec.Cmd
if hasUse {
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
} else {
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
}
output, err := cmd.CombinedOutput()
if err != nil {
g.log(fmt.Sprintf("Failed to set global USE flags: %s", string(output)))
return err
}
g.log(fmt.Sprintf("Set global USE flags: %s", useFlags))
return nil
}
func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
prerequisites := g.getPrerequisites()
var missingPkgs []string
if !g.skipGlobalUseFlags {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Setting global USE flags...",
IsComplete: false,
LogOutput: "Configuring global USE flags in /etc/portage/make.conf",
}
if err := g.setGlobalUseFlags(ctx, sudoPassword); err != nil {
g.logError("failed to set global USE flags", err)
return fmt.Errorf("failed to set global USE flags: %w", err)
}
} else {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Skipping global USE flags...",
IsComplete: false,
LogOutput: "Skipping global USE flags configuration (using existing configuration)",
}
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking prerequisites...",
IsComplete: false,
LogOutput: "Checking prerequisite packages",
}
for _, pkg := range prerequisites {
checkCmd := exec.CommandContext(ctx, "qlist", "-I", pkg)
if err := checkCmd.Run(); err != nil {
missingPkgs = append(missingPkgs, pkg)
}
}
_, err := exec.LookPath("go")
if err != nil {
g.log("go not found in PATH, will install dev-lang/go")
missingPkgs = append(missingPkgs, "dev-lang/go")
} else {
g.log("go already available in PATH")
}
if len(missingPkgs) == 0 {
g.log("All prerequisites already installed")
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.07,
Step: "Syncing Portage tree...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo emerge --sync",
LogOutput: "Syncing Portage tree with emerge --sync",
}
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
syncOutput, syncErr := syncCmd.CombinedOutput()
if syncErr != nil {
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
return fmt.Errorf("failed to sync Portage tree: %w\nOutput: %s", syncErr, string(syncOutput))
}
g.log("Portage tree synced successfully")
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.08,
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")),
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
}
args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput()
if err != nil {
g.logError("failed to install prerequisites", err)
g.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
return fmt.Errorf("failed to install prerequisites: %w\nOutput: %s", err, string(output))
}
g.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
return nil
}
func (g *GentooDistribution) 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 {
g.skipGlobalUseFlags = skipGlobalUseFlags
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if err := g.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, guruPkgs, manualPkgs, variantMap := g.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
g.log(fmt.Sprintf("CATEGORIZED PACKAGES: system=%d, guru=%d, manual=%d", len(systemPkgs), len(guruPkgs), len(manualPkgs)))
if len(systemPkgs) > 0 {
systemPkgNames := g.extractPackageNames(systemPkgs)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgNames, ", ")),
}
if err := g.installPortagePackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Portage packages: %w", err)
}
}
if len(guruPkgs) > 0 {
g.log(fmt.Sprintf("FOUND %d GURU PACKAGES - WILL SYNC GURU REPO", len(guruPkgs)))
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.60,
Step: "Syncing GURU repository...",
IsComplete: false,
LogOutput: "Syncing GURU repository to fetch latest ebuilds",
}
g.log("ABOUT TO CALL syncGURURepo")
if err := g.syncGURURepo(ctx, sudoPassword, progressChan); err != nil {
g.log(fmt.Sprintf("syncGURURepo RETURNED ERROR: %v", err))
return fmt.Errorf("failed to sync GURU repository: %w", err)
}
g.log("syncGURURepo COMPLETED SUCCESSFULLY")
guruPkgNames := g.extractPackageNames(guruPkgs)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d GURU packages...", len(guruPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing GURU packages: %s", strings.Join(guruPkgNames, ", ")),
}
if err := g.installGURUPackages(ctx, guruPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install GURU packages: %w", err)
}
}
if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.85,
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
}
if err := g.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install manual packages: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
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{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (g *GentooDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]PackageMapping, []PackageMapping, []string, map[string]deps.PackageVariant) {
systemPkgs := []PackageMapping{}
guruPkgs := []PackageMapping{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := g.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
g.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo)
case RepoTypeGURU:
guruPkgs = append(guruPkgs, pkgInfo)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return systemPkgs, guruPkgs, manualPkgs, variantMap
}
func (g *GentooDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (g *GentooDistribution) installPortagePackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
packageNames := g.extractPackageNames(packages)
g.log(fmt.Sprintf("Installing Portage packages: %s", strings.Join(packageNames, ", ")))
for _, pkg := range packages {
if pkg.AcceptKeywords != "" {
if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil {
return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err)
}
}
if pkg.UseFlags != "" {
if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil {
return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err)
}
}
}
args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, packageNames...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
}
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
packageUseDir := "/etc/portage/package.use"
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("mkdir -p %s", packageUseDir))
if output, err := mkdirCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
return fmt.Errorf("failed to create package.use directory: %w", err)
}
useFlagLine := fmt.Sprintf("%s %s", packageName, useFlags)
checkExistingCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, packageUseDir))
if checkExistingCmd.Run() == nil {
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
if output, err := replaceCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
return fmt.Errorf("failed to remove old USE flags: %w", err)
}
}
appendCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
output, err := appendCmd.CombinedOutput()
if err != nil {
g.log(fmt.Sprintf("append output: %s", string(output)))
return fmt.Errorf("failed to write USE flags to package.use: %w", err)
}
g.log(fmt.Sprintf("Set USE flags for %s: %s", packageName, useFlags))
return nil
}
func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.55,
Step: "Enabling GURU repository...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo eselect repository enable guru",
LogOutput: "Enabling GURU repository with eselect",
}
// Enable GURU repository
enableCmd := ExecSudoCommand(ctx, sudoPassword,
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
output, err := enableCmd.CombinedOutput()
g.log(fmt.Sprintf("eselect repository enable guru output:\n%s", string(output)))
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.55,
LogOutput: "GURU repository enabled",
}
if err != nil {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.55,
LogOutput: fmt.Sprintf("ERROR enabling GURU: %v", err),
Error: err,
}
return fmt.Errorf("failed to enable GURU repository: %w\nOutput: %s", err, string(output))
}
// Sync GURU repository
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.57,
Step: "Syncing GURU repository...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo emaint sync --repo guru",
LogOutput: "Syncing GURU repository",
}
syncCmd := ExecSudoCommand(ctx, sudoPassword,
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
syncOutput, syncErr := syncCmd.CombinedOutput()
g.log(fmt.Sprintf("emaint sync --repo guru output:\n%s", string(syncOutput)))
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.57,
LogOutput: "GURU repository synced",
}
if syncErr != nil {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.57,
LogOutput: fmt.Sprintf("ERROR syncing GURU: %v", syncErr),
Error: syncErr,
}
return fmt.Errorf("failed to sync GURU repository: %w\nOutput: %s", syncErr, string(syncOutput))
}
return nil
}
func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packageName, keywords, sudoPassword string) error {
checkCmd := exec.CommandContext(ctx, "portageq", "match", "/", packageName)
if output, err := checkCmd.CombinedOutput(); err == nil && len(output) > 0 {
g.log(fmt.Sprintf("Package %s is already available (may already be unmasked)", packageName))
return nil
}
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
if output, err := mkdirCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
return fmt.Errorf("failed to create package.accept_keywords directory: %w", err)
}
keywordLine := fmt.Sprintf("%s %s", packageName, keywords)
checkExistingCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, acceptKeywordsDir))
if checkExistingCmd.Run() == nil {
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
if output, err := replaceCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
return fmt.Errorf("failed to remove old accept keywords: %w", err)
}
}
appendCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
output, err := appendCmd.CombinedOutput()
if err != nil {
g.log(fmt.Sprintf("append output: %s", string(output)))
return fmt.Errorf("failed to write accept keywords: %w", err)
}
g.log(fmt.Sprintf("Set accept keywords for %s: %s", packageName, keywords))
return nil
}
func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
packageNames := g.extractPackageNames(packages)
g.log(fmt.Sprintf("Installing GURU packages: %s", strings.Join(packageNames, ", ")))
for _, pkg := range packages {
if pkg.AcceptKeywords != "" {
if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil {
return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err)
}
}
if pkg.UseFlags != "" {
if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil {
return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err)
}
}
}
guruPackages := make([]string, len(packageNames))
for i, pkg := range packageNames {
guruPackages[i] = pkg + "::guru"
}
args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, guruPackages...)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.70,
Step: "Installing GURU packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
}
-157
View File
@@ -1,157 +0,0 @@
package distros
import (
"context"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
// DistroFamily represents a family of related distributions
type DistroFamily string
const (
FamilyArch DistroFamily = "arch"
FamilyFedora DistroFamily = "fedora"
FamilySUSE DistroFamily = "suse"
FamilyUbuntu DistroFamily = "ubuntu"
FamilyDebian DistroFamily = "debian"
FamilyNix DistroFamily = "nix"
FamilyGentoo DistroFamily = "gentoo"
)
// PackageManagerType defines the package manager a distro uses
type PackageManagerType string
const (
PackageManagerPacman PackageManagerType = "pacman"
PackageManagerDNF PackageManagerType = "dnf"
PackageManagerAPT PackageManagerType = "apt"
PackageManagerZypper PackageManagerType = "zypper"
PackageManagerNix PackageManagerType = "nix"
PackageManagerPortage PackageManagerType = "portage"
)
// RepositoryType defines the type of repository for a package
type RepositoryType string
const (
RepoTypeSystem RepositoryType = "system" // Standard system repo (pacman, dnf, apt)
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
RepoTypeFlake RepositoryType = "flake" // Nix flake
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
RepoTypeManual RepositoryType = "manual" // Manual build from source
)
// InstallPhase represents the current phase of installation
type InstallPhase int
const (
PhasePrerequisites InstallPhase = iota
PhaseAURHelper
PhaseSystemPackages
PhaseAURPackages
PhaseCursorTheme
PhaseConfiguration
PhaseComplete
)
// InstallProgressMsg represents progress during package installation
type InstallProgressMsg struct {
Phase InstallPhase
Progress float64
Step string
IsComplete bool
NeedsSudo bool
CommandInfo string
LogOutput string
Error error
}
// PackageMapping defines how to install a package on a specific distro
type PackageMapping struct {
Name string // Package name to install
Repository RepositoryType // Repository type
RepoURL string // Repository URL if needed (e.g., COPR repo, PPA)
BuildFunc string // Name of manual build function if RepoTypeManual
UseFlags string // USE flags for Gentoo packages
AcceptKeywords string // Accept keywords for Gentoo packages (e.g., "~amd64")
}
// Distribution defines a Linux distribution with all its specific configurations
type Distribution interface {
// Metadata
GetID() string
GetColorHex() string
GetFamily() DistroFamily
GetPackageManager() PackageManagerType
// Dependency Detection
DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error)
DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error)
// Package Installation
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
// Package Mapping
GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping
// Prerequisites
InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error
}
// DistroConfig holds configuration for a distribution
type DistroConfig struct {
ID string
ColorHex string
Family DistroFamily
Constructor func(config DistroConfig, logChan chan<- string) Distribution
}
// Registry holds all supported distributions
var Registry = make(map[string]DistroConfig)
// Register adds a distribution to the registry
func Register(id, colorHex string, family DistroFamily, constructor func(config DistroConfig, logChan chan<- string) Distribution) {
Registry[id] = DistroConfig{
ID: id,
ColorHex: colorHex,
Family: family,
Constructor: constructor,
}
}
// GetSupportedDistros returns a list of all supported distribution IDs
func GetSupportedDistros() []string {
ids := make([]string, 0, len(Registry))
for id := range Registry {
ids = append(ids, id)
}
return ids
}
// IsDistroSupported checks if a distribution ID is supported
func IsDistroSupported(id string) bool {
_, exists := Registry[id]
return exists
}
// NewDistribution creates a distribution instance by ID
func NewDistribution(id string, logChan chan<- string) (Distribution, error) {
config, exists := Registry[id]
if !exists {
return nil, &UnsupportedDistributionError{ID: id}
}
return config.Constructor(config, logChan), nil
}
// UnsupportedDistributionError is returned when a distribution is not supported
type UnsupportedDistributionError struct {
ID string
}
func (e *UnsupportedDistributionError) Error() string {
return "unsupported distribution: " + e.ID
}
-895
View File
@@ -1,895 +0,0 @@
package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
// ManualPackageInstaller provides methods for installing packages from source
type ManualPackageInstaller struct {
*BaseDistribution
}
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
parts := strings.Split(line, "refs/tags/")
if len(parts) > 1 {
latestTag := strings.TrimSpace(parts[1])
return latestTag
}
}
}
return ""
}
// getLatestQuickshellTag fetches the latest tag from the quickshell repository
func (m *ManualPackageInstaller) getLatestQuickshellTag(ctx context.Context) string {
tagCmd := exec.CommandContext(ctx, "git", "ls-remote", "--tags", "--sort=-v:refname",
"https://github.com/quickshell-mirror/quickshell.git")
tagOutput, err := tagCmd.Output()
if err != nil {
m.log(fmt.Sprintf("Warning: failed to fetch quickshell tags: %v", err))
return ""
}
return m.parseLatestTagFromGitOutput(string(tagOutput))
}
func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
m.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
for _, pkg := range packages {
variant := variantMap[pkg]
switch pkg {
case "dms (DankMaterialShell)", "dms":
if err := m.installDankMaterialShell(ctx, variant, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install DankMaterialShell: %w", err)
}
case "dgop":
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install dgop: %w", err)
}
case "niri":
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install niri: %w", err)
}
case "quickshell":
if err := m.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install quickshell: %w", err)
}
case "hyprland":
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install hyprland: %w", err)
}
case "hyprpicker":
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install hyprpicker: %w", err)
}
case "ghostty":
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
}
case "matugen":
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install matugen: %w", err)
}
case "cliphist":
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install cliphist: %w", err)
}
case "xwayland-satellite":
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
}
default:
m.log(fmt.Sprintf("Warning: No manual build method for %s", pkg))
}
}
return nil
}
func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing dgop from source...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "dgop-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Cloning dgop repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/AvengeMedia/dgop.git",
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/AvengeMedia/dgop.git", tmpDir)
if err := cloneCmd.Run(); err != nil {
m.logError("failed to clone dgop repository", err)
return fmt.Errorf("failed to clone dgop repository: %w", err)
}
buildCmd := exec.CommandContext(ctx, "make")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.7, "Building dgop..."); err != nil {
return fmt.Errorf("failed to build dgop: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.7,
Step: "Installing dgop...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo make install",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
m.logError("failed to install dgop", err)
return fmt.Errorf("failed to install dgop: %w", err)
}
m.log("dgop installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing niri from source...")
homeDir, _ := os.UserHomeDir()
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
if err := os.MkdirAll(buildDir, 0755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer func() {
os.RemoveAll(buildDir)
os.RemoveAll(tmpDir)
}()
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.2,
Step: "Cloning niri repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/YaLTeR/niri.git",
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/YaLTeR/niri.git", buildDir)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone niri: %w", err)
}
checkoutCmd := exec.CommandContext(ctx, "git", "-C", buildDir, "checkout", "v25.08")
if err := checkoutCmd.Run(); err != nil {
m.log(fmt.Sprintf("Warning: failed to checkout v25.08, using main: %v", err))
}
if !m.commandExists("cargo-deb") {
cargoDebInstallCmd := exec.CommandContext(ctx, "cargo", "install", "cargo-deb")
cargoDebInstallCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir)
if err := m.runWithProgressStep(cargoDebInstallCmd, progressChan, PhaseSystemPackages, 0.3, 0.35, "Installing cargo-deb..."); err != nil {
return fmt.Errorf("failed to install cargo-deb: %w", err)
}
}
buildDebCmd := exec.CommandContext(ctx, "cargo", "deb")
buildDebCmd.Dir = buildDir
buildDebCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir)
if err := m.runWithProgressStep(buildDebCmd, progressChan, PhaseSystemPackages, 0.35, 0.95, "Building niri deb package..."); err != nil {
return fmt.Errorf("failed to build niri deb: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.95,
Step: "Installing niri deb package...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "dpkg -i niri.deb",
}
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
output, err := installDebCmd.CombinedOutput()
if err != nil {
m.log(fmt.Sprintf("dpkg install failed. Output:\n%s", string(output)))
return fmt.Errorf("failed to install niri deb package: %w\nOutput:\n%s", err, string(output))
}
m.log(fmt.Sprintf("dpkg install successful. Output:\n%s", string(output)))
m.log("niri installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing quickshell from source...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "quickshell-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Cloning quickshell repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git",
}
var cloneCmd *exec.Cmd
if forceQuickshellGit || variant == deps.VariantGit {
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
} else {
latestTag := m.getLatestQuickshellTag(ctx)
if latestTag != "" {
m.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
} else {
m.log("Warning: failed to fetch latest tag, using default branch")
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
}
}
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone quickshell: %w", err)
}
buildDir := tmpDir + "/build"
if err := os.MkdirAll(buildDir, 0755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.3,
Step: "Configuring quickshell build...",
IsComplete: false,
CommandInfo: "cmake -B build -S . -G Ninja",
}
configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build",
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
"-DCRASH_REPORTER=off",
"-DCMAKE_CXX_STANDARD=20")
configureCmd.Dir = tmpDir
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
output, err := configureCmd.CombinedOutput()
if err != nil {
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output))
}
m.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output)))
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.4,
Step: "Building quickshell (this may take a while)...",
IsComplete: false,
CommandInfo: "cmake --build build",
}
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil {
return fmt.Errorf("failed to build quickshell: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing quickshell...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cmake --install build",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install quickshell: %w", err)
}
m.log("quickshell installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing Hyprland from source...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "hyprland-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Cloning Hyprland repository...",
IsComplete: false,
CommandInfo: "git clone --recursive https://github.com/hyprwm/Hyprland.git",
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recursive", "https://github.com/hyprwm/Hyprland.git", tmpDir)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone Hyprland: %w", err)
}
checkoutCmd := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "v0.50.1")
if err := checkoutCmd.Run(); err != nil {
m.log(fmt.Sprintf("Warning: failed to checkout v0.50.1, using main: %v", err))
}
buildCmd := exec.CommandContext(ctx, "make", "all")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.2, 0.8, "Building Hyprland..."); err != nil {
return fmt.Errorf("failed to build Hyprland: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing Hyprland...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo make install",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install Hyprland: %w", err)
}
m.log("Hyprland installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing hyprpicker from source...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
// Install hyprutils first
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.05,
Step: "Building hyprutils dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
}
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
return fmt.Errorf("failed to create hyprutils directory: %w", err)
}
defer os.RemoveAll(hyprutilsDir)
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
if err := cloneUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprutils: %w", err)
}
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-DBUILD_TESTING=off",
"-S", ".",
"-B", "./build")
configureUtilsCmd.Dir = hyprutilsDir
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
return fmt.Errorf("failed to configure hyprutils: %w", err)
}
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
buildUtilsCmd.Dir = hyprutilsDir
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
return fmt.Errorf("failed to build hyprutils: %w", err)
}
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installUtilsCmd.Dir = hyprutilsDir
if err := installUtilsCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprutils: %w", err)
}
// Install hyprwayland-scanner
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.2,
Step: "Building hyprwayland-scanner dependency...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
}
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
if err := os.MkdirAll(scannerDir, 0755); err != nil {
return fmt.Errorf("failed to create scanner directory: %w", err)
}
defer os.RemoveAll(scannerDir)
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
if err := cloneScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
}
configureScannerCmd := exec.CommandContext(ctx, "cmake",
"-DCMAKE_INSTALL_PREFIX=/usr",
"-B", "build")
configureScannerCmd.Dir = scannerDir
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
}
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
buildScannerCmd.Dir = scannerDir
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
}
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installScannerCmd.Dir = scannerDir
if err := installScannerCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
}
// Now build hyprpicker
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: "Cloning hyprpicker repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone hyprpicker: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.45,
Step: "Configuring hyprpicker build...",
IsComplete: false,
CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
}
configureCmd := exec.CommandContext(ctx, "cmake",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE:STRING=Release",
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
"-S", ".",
"-B", "./build")
configureCmd.Dir = tmpDir
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
output, err := configureCmd.CombinedOutput()
if err != nil {
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
return fmt.Errorf("failed to configure hyprpicker: %w\nCMake output:\n%s", err, string(output))
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.55,
Step: "Building hyprpicker...",
IsComplete: false,
CommandInfo: "cmake --build build --target hyprpicker",
}
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "hyprpicker")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
return fmt.Errorf("failed to build hyprpicker: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing hyprpicker...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cmake --install build",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install hyprpicker: %w", err)
}
m.log("hyprpicker installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing Ghostty from source...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "ghostty-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Cloning Ghostty repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/ghostty-org/ghostty.git",
}
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/ghostty-org/ghostty.git", tmpDir)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone Ghostty: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.2,
Step: "Building Ghostty (this may take a while)...",
IsComplete: false,
CommandInfo: "zig build -Doptimize=ReleaseFast",
}
buildCmd := exec.CommandContext(ctx, "zig", "build", "-Doptimize=ReleaseFast")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
if err := buildCmd.Run(); err != nil {
return fmt.Errorf("failed to build Ghostty: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing Ghostty...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install Ghostty: %w", err)
}
m.log("Ghostty installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing matugen from source...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Installing matugen via cargo...",
IsComplete: false,
CommandInfo: "cargo install matugen",
}
installCmd := exec.CommandContext(ctx, "cargo", "install", "matugen")
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building matugen..."); err != nil {
return fmt.Errorf("failed to install matugen: %w", err)
}
homeDir := os.Getenv("HOME")
sourcePath := filepath.Join(homeDir, ".cargo", "bin", "matugen")
targetPath := "/usr/local/bin/matugen"
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.7,
Step: "Installing matugen binary to system...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
}
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
}
// Make it executable
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to make matugen executable: %w", err)
}
m.log("matugen installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing DankMaterialShell (DMS)...")
if err := m.installDMSBinary(ctx, sudoPassword, progressChan); err != nil {
m.logError("Failed to install DMS binary", err)
}
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.90,
Step: "Cloning DankMaterialShell...",
IsComplete: false,
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
}
configDir := filepath.Dir(dmsPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create quickshell config directory: %w", err)
}
cloneCmd := exec.CommandContext(ctx, "git", "clone",
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
}
if forceDMSGit || variant == deps.VariantGit {
m.log("Using git variant (master branch)")
return nil
}
tagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master")
tagOutput, err := tagCmd.Output()
if err != nil {
m.log("Using default branch (no tags found)")
return nil
}
latestTag := strings.TrimSpace(string(tagOutput))
checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag)
if err := checkoutCmd.Run(); err != nil {
m.logError(fmt.Sprintf("Failed to checkout tag %s", latestTag), err)
return nil
}
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
m.log("DankMaterialShell cloned successfully")
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.90,
Step: "Updating DankMaterialShell...",
IsComplete: false,
CommandInfo: "Updating ~/.config/quickshell/dms",
}
fetchCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "fetch", "origin", "--tags", "--force")
if err := fetchCmd.Run(); err != nil {
m.logError("Failed to fetch updates", err)
return nil
}
if forceDMSGit || variant == deps.VariantGit {
branchCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "rev-parse", "--abbrev-ref", "HEAD")
branchOutput, err := branchCmd.Output()
if err != nil {
m.logError("Failed to get current branch", err)
return nil
}
branch := strings.TrimSpace(string(branchOutput))
if branch == "" {
branch = "master"
}
pullCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "pull", "origin", branch)
if err := pullCmd.Run(); err != nil {
m.logError("Failed to pull updates", err)
return nil
}
m.log("DankMaterialShell updated successfully (git variant)")
return nil
}
latestTagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master")
tagOutput, err := latestTagCmd.Output()
if err != nil {
m.logError("Failed to get latest tag", err)
return nil
}
latestTag := strings.TrimSpace(string(tagOutput))
checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag)
if err := checkoutCmd.Run(); err != nil {
m.logError(fmt.Sprintf("Failed to checkout tag %s", latestTag), err)
return nil
}
m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
return nil
}
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing cliphist from source...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Installing cliphist via go install...",
IsComplete: false,
CommandInfo: "go install go.senan.xyz/cliphist@latest",
}
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
return fmt.Errorf("failed to install cliphist: %w", err)
}
homeDir := os.Getenv("HOME")
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
targetPath := "/usr/local/bin/cliphist"
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.7,
Step: "Installing cliphist binary to system...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
}
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
}
// Make it executable
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to make cliphist executable: %w", err)
}
m.log("cliphist installed successfully from source")
return nil
}
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
m.log("Installing xwayland-satellite from source...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Installing xwayland-satellite via cargo...",
IsComplete: false,
CommandInfo: "cargo install --git https://github.com/Supreeeme/xwayland-satellite --tag v0.7",
}
installCmd := exec.CommandContext(ctx, "cargo", "install", "--git", "https://github.com/Supreeeme/xwayland-satellite", "--tag", "v0.7")
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building xwayland-satellite..."); err != nil {
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
}
homeDir := os.Getenv("HOME")
sourcePath := filepath.Join(homeDir, ".cargo", "bin", "xwayland-satellite")
targetPath := "/usr/local/bin/xwayland-satellite"
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.7,
Step: "Installing xwayland-satellite binary to system...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
}
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
}
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
}
m.log("xwayland-satellite installed successfully from source")
return nil
}
@@ -1,122 +0,0 @@
package distros
import (
"testing"
)
func TestManualPackageInstaller_parseLatestTagFromGitOutput(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "normal tag output",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0
703a3789083d2f990c4e99cd25c97c2a4cccbd81 refs/tags/v0.1.0`,
expected: "v0.2.1",
},
{
name: "annotated tags with ^{}",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.2.1^{}
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
expected: "v0.2.1",
},
{
name: "mixed tags",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0
b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.3.0^{}
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0
c1c150fab00a93ea983aaca5df55304bc837f51d refs/tags/beta-1`,
expected: "v0.3.0",
},
{
name: "empty output",
input: "",
expected: "",
},
{
name: "no tags",
input: "some other output\nwithout tags",
expected: "",
},
{
name: "only annotated tags",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1^{}
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0^{}`,
expected: "",
},
{
name: "single tag",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.0.0`,
expected: "v1.0.0",
},
{
name: "tag with extra whitespace",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
expected: "v0.2.1",
},
{
name: "beta and rc tags",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0-beta.1
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
expected: "v0.3.0-beta.1",
},
{
name: "tags without v prefix",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/0.2.1
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/0.2.0`,
expected: "0.2.1",
},
{
name: "multiple lines with spaces",
input: `
a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.2.3
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v1.2.2
`,
expected: "v1.2.3",
},
{
name: "tag at end of line",
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1`,
expected: "v0.2.1",
},
}
logChan := make(chan string, 100)
defer close(logChan)
base := NewBaseDistribution(logChan)
installer := &ManualPackageInstaller{BaseDistribution: base}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := installer.parseLatestTagFromGitOutput(tt.input)
if result != tt.expected {
t.Errorf("parseLatestTagFromGitOutput() = %q, expected %q", result, tt.expected)
}
})
}
}
func TestManualPackageInstaller_parseLatestTagFromGitOutput_EmptyInstaller(t *testing.T) {
// Test that parsing works even with a minimal installer setup
logChan := make(chan string, 10)
defer close(logChan)
base := NewBaseDistribution(logChan)
installer := &ManualPackageInstaller{BaseDistribution: base}
input := `abc123 refs/tags/v1.0.0
def456 refs/tags/v0.9.0`
result := installer.parseLatestTagFromGitOutput(input)
if result != "v1.0.0" {
t.Errorf("Expected v1.0.0, got %s", result)
}
}
-714
View File
@@ -1,714 +0,0 @@
package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func init() {
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
return NewOpenSUSEDistribution(config, logChan)
})
}
type OpenSUSEDistribution struct {
*BaseDistribution
*ManualPackageInstaller
config DistroConfig
}
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
base := NewBaseDistribution(logChan)
return &OpenSUSEDistribution{
BaseDistribution: base,
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
config: config,
}
}
func (o *OpenSUSEDistribution) GetID() string {
return o.config.ID
}
func (o *OpenSUSEDistribution) GetColorHex() string {
return o.config.ColorHex
}
func (o *OpenSUSEDistribution) GetFamily() DistroFamily {
return o.config.Family
}
func (o *OpenSUSEDistribution) GetPackageManager() PackageManagerType {
return PackageManagerZypper
}
func (o *OpenSUSEDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return o.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (o *OpenSUSEDistribution) 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, o.detectDMS())
// Terminal with choice support
dependencies = append(dependencies, o.detectSpecificTerminal(terminal))
// Common detections using base methods
dependencies = append(dependencies, o.detectGit())
dependencies = append(dependencies, o.detectWindowManager(wm))
dependencies = append(dependencies, o.detectQuickshell())
dependencies = append(dependencies, o.detectXDGPortal())
dependencies = append(dependencies, o.detectAccountsService())
// Hyprland-specific tools
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, o.detectHyprlandTools()...)
}
// Niri-specific tools
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, o.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, o.detectMatugen())
dependencies = append(dependencies, o.detectDgop())
dependencies = append(dependencies, o.detectClipboardTools()...)
return dependencies, nil
}
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
return o.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", o.packageInstalled("xdg-desktop-portal-gtk"))
}
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("rpm", "-q", pkg)
err := cmd.Run()
return err == nil
}
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
// Standard zypper packages
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
// DMS packages from OBS
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
// Niri stable has native package support on openSUSE
niriVariant := variants["niri"]
packages["niri"] = o.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = o.getXwaylandSatelliteMapping(niriVariant)
}
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 {
status := deps.StatusMissing
if o.commandExists("xwayland-satellite") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "xwayland-satellite",
Status: status,
Description: "Xwayland support",
Required: true,
}
}
func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
status := deps.StatusMissing
if o.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 (o *OpenSUSEDistribution) getPrerequisites() []string {
return []string{
"make",
"unzip",
"gcc",
"gcc-c++",
"cmake",
"ninja",
"pkgconf-pkg-config",
"git",
"qt6-base-devel",
"qt6-declarative-devel",
"qt6-declarative-private-devel",
"qt6-shadertools",
"qt6-shadertools-devel",
"qt6-wayland-devel",
"qt6-waylandclient-private-devel",
"spirv-tools-devel",
"cli11-devel",
"wayland-protocols-devel",
"libgbm-devel",
"libdrm-devel",
"pipewire-devel",
"jemalloc-devel",
"wayland-utils",
"Mesa-libGLESv3-devel",
"pam-devel",
"glib2-devel",
"polkit-devel",
}
}
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
prerequisites := o.getPrerequisites()
var missingPkgs []string
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking prerequisites...",
IsComplete: false,
LogOutput: "Checking prerequisite packages",
}
for _, pkg := range prerequisites {
checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg)
if err := checkCmd.Run(); err != nil {
missingPkgs = append(missingPkgs, pkg)
}
}
_, err := exec.LookPath("go")
if err != nil {
o.log("go not found in PATH, will install go")
missingPkgs = append(missingPkgs, "go")
} else {
o.log("go already available in PATH")
}
if len(missingPkgs) == 0 {
o.log("All prerequisites already installed")
return nil
}
o.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.08,
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper install -y %s", strings.Join(missingPkgs, " ")),
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
}
args := []string{"zypper", "install", "-y"}
args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput()
if err != nil {
o.logError("failed to install prerequisites", err)
o.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
return fmt.Errorf("failed to install prerequisites: %w", err)
}
o.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
return nil
}
func (o *OpenSUSEDistribution) 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 := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, obsPkgs, manualPkgs, variantMap := o.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 := 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 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
}
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install zypper packages: %w", err)
}
}
// OBS Packages
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 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.85,
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
}
if err := o.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install manual packages: %w", err)
}
}
// Configuration
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
terminal := o.DetectTerminalFromDeps(dependencies)
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{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
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) {
systemPkgs := []string{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := o.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
o.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return systemPkgs, obsPkgs, 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 {
if len(packages) == 0 {
return nil
}
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
args := []string{"zypper", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
o.log("Installing quickshell from source (with openSUSE-specific build flags)...")
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
tmpDir := filepath.Join(cacheDir, "quickshell-build")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Cloning quickshell repository...",
IsComplete: false,
CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git",
}
var cloneCmd *exec.Cmd
if forceQuickshellGit || variant == deps.VariantGit {
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
} else {
latestTag := o.getLatestQuickshellTag(ctx)
if latestTag != "" {
o.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
} else {
o.log("Warning: failed to fetch latest tag, using default branch")
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
}
}
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone quickshell: %w", err)
}
buildDir := tmpDir + "/build"
if err := os.MkdirAll(buildDir, 0755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.3,
Step: "Configuring quickshell build (with openSUSE flags)...",
IsComplete: false,
CommandInfo: "cmake -B build -S . -G Ninja",
}
// Get optflags from rpm
optflagsCmd := exec.CommandContext(ctx, "rpm", "--eval", "%{optflags}")
optflagsOutput, err := optflagsCmd.Output()
optflags := strings.TrimSpace(string(optflagsOutput))
if err != nil || optflags == "" {
o.log("Warning: Could not get optflags from rpm, using default -O2 -g")
optflags = "-O2 -g"
}
// Set openSUSE-specific CFLAGS
customCFLAGS := fmt.Sprintf("%s -I/usr/include/wayland", optflags)
configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build",
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
"-DCRASH_REPORTER=off",
"-DCMAKE_CXX_STANDARD=20")
configureCmd.Dir = tmpDir
configureCmd.Env = append(os.Environ(),
"TMPDIR="+cacheDir,
"CFLAGS="+customCFLAGS,
"CXXFLAGS="+customCFLAGS)
o.log(fmt.Sprintf("Using CFLAGS: %s", customCFLAGS))
output, err := configureCmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output))
}
o.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output)))
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.4,
Step: "Building quickshell (this may take a while)...",
IsComplete: false,
CommandInfo: "cmake --build build",
}
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build")
buildCmd.Dir = tmpDir
buildCmd.Env = append(os.Environ(),
"TMPDIR="+cacheDir,
"CFLAGS="+customCFLAGS,
"CXXFLAGS="+customCFLAGS)
if err := o.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil {
return fmt.Errorf("failed to build quickshell: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.8,
Step: "Installing quickshell...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo cmake --install build",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install quickshell: %w", err)
}
o.log("quickshell installed successfully from source")
return nil
}
func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if o.commandExists("cargo") {
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.82,
Step: "Installing rustup...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo zypper install rustup",
}
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.83,
Step: "Installing stable Rust toolchain...",
IsComplete: false,
CommandInfo: "rustup install stable",
}
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
if err := o.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
return fmt.Errorf("failed to install Rust toolchain: %w", err)
}
if !o.commandExists("cargo") {
o.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
}
return nil
}
func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
o.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
for _, pkg := range packages {
if pkg == "matugen" {
if err := o.installRust(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Rust: %w", err)
}
break
}
}
for _, pkg := range packages {
variant := variantMap[pkg]
if pkg == "quickshell" {
if err := o.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install quickshell: %w", err)
}
} else {
if err := o.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err)
}
}
}
return nil
}
-115
View File
@@ -1,115 +0,0 @@
package distros
import (
"bufio"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
)
// DistroInfo contains basic information about a distribution
type DistroInfo struct {
ID string
HexColorCode string
}
// OSInfo contains complete OS information
type OSInfo struct {
Distribution DistroInfo
Version string
VersionID string
PrettyName string
Architecture string
}
// GetOSInfo detects the current OS and returns information about it
func GetOSInfo() (*OSInfo, error) {
if runtime.GOOS != "linux" {
return nil, errdefs.NewCustomError(errdefs.ErrTypeNotLinux, fmt.Sprintf("Only linux is supported, but I found %s", runtime.GOOS))
}
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
return nil, errdefs.NewCustomError(errdefs.ErrTypeInvalidArchitecture, fmt.Sprintf("Only amd64 and arm64 are supported, but I found %s", runtime.GOARCH))
}
info := &OSInfo{
Architecture: runtime.GOARCH,
}
file, err := os.Open("/etc/os-release")
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := parts[0]
value := strings.Trim(parts[1], "\"")
switch key {
case "ID":
config, exists := Registry[value]
if !exists {
return nil, errdefs.NewCustomError(errdefs.ErrTypeUnsupportedDistribution, fmt.Sprintf("Unsupported distribution: %s", value))
}
info.Distribution = DistroInfo{
ID: value, // Use the actual ID from os-release
HexColorCode: config.ColorHex,
}
case "VERSION_ID", "BUILD_ID":
info.VersionID = value
case "VERSION":
info.Version = value
case "PRETTY_NAME":
info.PrettyName = value
}
}
return info, scanner.Err()
}
// IsUnsupportedDistro checks if a distribution/version combination is supported
func IsUnsupportedDistro(distroID, versionID string) bool {
if !IsDistroSupported(distroID) {
return true
}
if distroID == "ubuntu" {
parts := strings.Split(versionID, ".")
if len(parts) >= 2 {
major, err1 := strconv.Atoi(parts[0])
minor, err2 := strconv.Atoi(parts[1])
if err1 == nil && err2 == nil {
return major < 25 || (major == 25 && minor < 4)
}
}
return true
}
if distroID == "debian" {
if versionID == "" {
// debian testing/sid have no version ID
return false
}
versionNum, err := strconv.Atoi(versionID)
if err == nil {
return versionNum < 12
}
return true
}
return false
}
-680
View File
@@ -1,680 +0,0 @@
package distros
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func init() {
Register("ubuntu", "#E95420", FamilyUbuntu, func(config DistroConfig, logChan chan<- string) Distribution {
return NewUbuntuDistribution(config, logChan)
})
}
type UbuntuDistribution struct {
*BaseDistribution
*ManualPackageInstaller
config DistroConfig
}
func NewUbuntuDistribution(config DistroConfig, logChan chan<- string) *UbuntuDistribution {
base := NewBaseDistribution(logChan)
return &UbuntuDistribution{
BaseDistribution: base,
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
config: config,
}
}
func (u *UbuntuDistribution) GetID() string {
return u.config.ID
}
func (u *UbuntuDistribution) GetColorHex() string {
return u.config.ColorHex
}
func (u *UbuntuDistribution) GetFamily() DistroFamily {
return u.config.Family
}
func (u *UbuntuDistribution) GetPackageManager() PackageManagerType {
return PackageManagerAPT
}
func (u *UbuntuDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return u.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (u *UbuntuDistribution) 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, u.detectDMS())
// Terminal with choice support
dependencies = append(dependencies, u.detectSpecificTerminal(terminal))
// Common detections using base methods
dependencies = append(dependencies, u.detectGit())
dependencies = append(dependencies, u.detectWindowManager(wm))
dependencies = append(dependencies, u.detectQuickshell())
dependencies = append(dependencies, u.detectXDGPortal())
dependencies = append(dependencies, u.detectAccountsService())
// Hyprland-specific tools
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, u.detectHyprlandTools()...)
}
// Niri-specific tools
if wm == deps.WindowManagerNiri {
dependencies = append(dependencies, u.detectXwaylandSatellite())
}
// Base detections (common across distros)
dependencies = append(dependencies, u.detectMatugen())
dependencies = append(dependencies, u.detectDgop())
dependencies = append(dependencies, u.detectClipboardTools()...)
return dependencies, nil
}
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
return u.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", u.packageInstalled("xdg-desktop-portal-gtk"))
}
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
return u.detectCommand("xwayland-satellite", "Xwayland support")
}
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
}
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run()
return err == nil
}
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{
// Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
// DMS packages from PPAs
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
}
switch wm {
case deps.WindowManagerHyprland:
// Use the cppiber PPA for Hyprland
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
niriVariant := variants["niri"]
packages["niri"] = u.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = u.getXwaylandSatelliteMapping(niriVariant)
}
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 {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Updating package lists...",
IsComplete: false,
LogOutput: "Updating APT package lists",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
return fmt.Errorf("failed to update package lists: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.08,
Step: "Installing build-essential...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install -y build-essential",
LogOutput: "Installing build tools",
}
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil {
// Not installed, install it
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.10,
Step: "Installing development dependencies...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev",
LogOutput: "Installing additional development tools",
}
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.12,
Step: "Prerequisites installation complete",
IsComplete: false,
LogOutput: "Prerequisites successfully installed",
}
return nil
}
func (u *UbuntuDistribution) 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 := u.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, ppaPkgs, manualPkgs, variantMap := u.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Phase 2: Enable PPA repositories
if len(ppaPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling PPA repositories...",
IsComplete: false,
LogOutput: "Setting up PPA repositories for additional packages",
}
if err := u.enablePPARepos(ctx, ppaPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable PPA repositories: %w", err)
}
}
// Phase 3: System Packages (APT)
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
}
if err := u.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install APT packages: %w", err)
}
}
// Phase 4: PPA Packages
ppaPkgNames := u.extractPackageNames(ppaPkgs)
if len(ppaPkgNames) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, // Reusing AUR phase for PPA
Progress: 0.65,
Step: fmt.Sprintf("Installing %d PPA packages...", len(ppaPkgNames)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing PPA packages: %s", strings.Join(ppaPkgNames, ", ")),
}
if err := u.installPPAPackages(ctx, ppaPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install PPA packages: %w", err)
}
}
// Phase 5: Manual Builds
if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.80,
Step: "Installing build dependencies...",
IsComplete: false,
LogOutput: "Installing build tools for manual compilation",
}
if err := u.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install build dependencies: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.85,
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
IsComplete: false,
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
}
if err := u.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install manual packages: %w", err)
}
}
// Phase 6: Configuration
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
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
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
systemPkgs := []string{}
ppaPkgs := []PackageMapping{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := u.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
u.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypePPA:
ppaPkgs = append(ppaPkgs, pkgInfo)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return systemPkgs, ppaPkgs, manualPkgs, variantMap
}
func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
installPPACmd := ExecSudoCommand(ctx, sudoPassword,
"apt-get install -y software-properties-common")
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
return fmt.Errorf("failed to install software-properties-common: %w", err)
}
for _, pkg := range ppaPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
u.log(fmt.Sprintf("Enabling PPA repository: %s", pkg.RepoURL))
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Enabling PPA repo %s...", pkg.RepoURL),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
}
cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
return fmt.Errorf("failed to enable PPA repo %s: %w", pkg.RepoURL, err)
}
u.log(fmt.Sprintf("PPA repo %s enabled successfully", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
}
}
if len(enabledRepos) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Updating package lists...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get update",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
}
}
return nil
}
func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"apt-get", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
}
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
args := []string{"apt-get", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.70,
Step: "Installing PPA packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
}
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
buildDeps := make(map[string]bool)
for _, pkg := range manualPkgs {
switch pkg {
case "niri":
buildDeps["curl"] = true
buildDeps["libxkbcommon-dev"] = true
buildDeps["libwayland-dev"] = true
buildDeps["libudev-dev"] = true
buildDeps["libinput-dev"] = true
buildDeps["libdisplay-info-dev"] = true
buildDeps["libpango1.0-dev"] = true
buildDeps["libcairo-dev"] = true
buildDeps["libpipewire-0.3-dev"] = true
buildDeps["libc6-dev"] = true
buildDeps["clang"] = true
buildDeps["libseat-dev"] = true
buildDeps["libgbm-dev"] = true
buildDeps["alacritty"] = true
buildDeps["fuzzel"] = true
buildDeps["libxcb-cursor-dev"] = true
case "quickshell":
buildDeps["qt6-base-dev"] = true
buildDeps["qt6-base-private-dev"] = true
buildDeps["qt6-declarative-dev"] = true
buildDeps["qt6-declarative-private-dev"] = true
buildDeps["qt6-wayland-dev"] = true
buildDeps["qt6-wayland-private-dev"] = true
buildDeps["qt6-tools-dev"] = true
buildDeps["libqt6svg6-dev"] = true
buildDeps["qt6-shadertools-dev"] = true
buildDeps["spirv-tools"] = true
buildDeps["libcli11-dev"] = true
buildDeps["libjemalloc-dev"] = true
buildDeps["libwayland-dev"] = true
buildDeps["wayland-protocols"] = true
buildDeps["libdrm-dev"] = true
buildDeps["libgbm-dev"] = true
buildDeps["libegl-dev"] = true
buildDeps["libgles2-mesa-dev"] = true
buildDeps["libgl1-mesa-dev"] = true
buildDeps["libxcb1-dev"] = true
buildDeps["libpipewire-0.3-dev"] = true
buildDeps["libpam0g-dev"] = true
case "matugen":
buildDeps["curl"] = true
case "cliphist":
// Go will be installed separately with PPA
}
}
for _, pkg := range manualPkgs {
switch pkg {
case "niri", "matugen":
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Rust: %w", err)
}
case "cliphist", "dgop":
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err)
}
}
}
if len(buildDeps) == 0 {
return nil
}
depList := make([]string, 0, len(buildDeps))
for dep := range buildDeps {
depList = append(depList, dep)
}
args := []string{"apt-get", "install", "-y"}
args = append(args, depList...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
}
func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if u.commandExists("cargo") {
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.82,
Step: "Installing rustup...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install rustup",
}
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.83,
Step: "Installing stable Rust toolchain...",
IsComplete: false,
CommandInfo: "rustup install stable",
}
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
if err := u.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
return fmt.Errorf("failed to install Rust toolchain: %w", err)
}
// Verify cargo is now available
if !u.commandExists("cargo") {
u.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
}
return nil
}
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if u.commandExists("go") {
return nil
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.87,
Step: "Adding Go PPA repository...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
}
addPPACmd := ExecSudoCommand(ctx, sudoPassword,
"add-apt-repository -y ppa:longsleep/golang-backports")
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
return fmt.Errorf("failed to add Go PPA: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.88,
Step: "Updating package lists...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get update",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.89,
Step: "Installing Go...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo apt-get install golang-go",
}
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
}
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
u.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
for _, pkg := range packages {
switch pkg {
default:
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err)
}
}
}
return nil
}
-450
View File
@@ -1,450 +0,0 @@
//go:build !distro_binary
package dms
import (
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
tea "github.com/charmbracelet/bubbletea"
)
type AppState int
const (
StateMainMenu AppState = iota
StateUpdate
StateUpdatePassword
StateUpdateProgress
StateShell
StatePluginsMenu
StatePluginsBrowse
StatePluginDetail
StatePluginSearch
StatePluginsInstalled
StatePluginInstalledDetail
StateGreeterMenu
StateGreeterCompositorSelect
StateGreeterPassword
StateGreeterInstalling
StateAbout
)
type Model struct {
version string
detector *Detector
dependencies []DependencyInfo
state AppState
selectedItem int
width int
height int
// Menu items
menuItems []MenuItem
updateDeps []DependencyInfo
selectedUpdateDep int
updateToggles map[string]bool
updateProgressChan chan updateProgressMsg
updateProgress updateProgressMsg
updateLogs []string
sudoPassword string
passwordInput string
passwordError string
// Window manager states
hyprlandInstalled bool
niriInstalled bool
selectedGreeterItem int
greeterInstallChan chan greeterProgressMsg
greeterProgress greeterProgressMsg
greeterLogs []string
greeterPasswordInput string
greeterPasswordError string
greeterSudoPassword string
greeterCompositors []string
greeterSelectedComp int
greeterChosenCompositor string
pluginsMenuItems []MenuItem
selectedPluginsMenuItem int
pluginsList []pluginInfo
filteredPluginsList []pluginInfo
selectedPluginIndex int
pluginsLoading bool
pluginsError string
pluginSearchQuery string
installedPluginsList []pluginInfo
selectedInstalledIndex int
installedPluginsLoading bool
installedPluginsError string
pluginInstallStatus map[string]bool
}
type pluginInfo struct {
ID string
Name string
Category string
Author string
Description string
Repo string
Path string
Capabilities []string
Compositors []string
Dependencies []string
FirstParty bool
}
type MenuItem struct {
Label string
Action AppState
}
func NewModel(version string) Model {
detector, _ := NewDetector()
var dependencies []DependencyInfo
var hyprlandInstalled, niriInstalled bool
var err error
if detector != nil {
dependencies = detector.GetInstalledComponents()
// Use the proper detection method for both window managers
hyprlandInstalled, niriInstalled, err = detector.GetWindowManagerStatus()
if err != nil {
// Fallback to false if detection fails
hyprlandInstalled = false
niriInstalled = false
}
}
updateToggles := make(map[string]bool)
for _, dep := range dependencies {
if dep.Name == "dms (DankMaterialShell)" && dep.Status == deps.StatusNeedsUpdate {
updateToggles[dep.Name] = true
break
}
}
m := Model{
version: version,
detector: detector,
dependencies: dependencies,
state: StateMainMenu,
selectedItem: 0,
updateToggles: updateToggles,
updateDeps: dependencies,
updateProgressChan: make(chan updateProgressMsg, 100),
hyprlandInstalled: hyprlandInstalled,
niriInstalled: niriInstalled,
greeterInstallChan: make(chan greeterProgressMsg, 100),
pluginInstallStatus: make(map[string]bool),
}
m.menuItems = m.buildMenuItems()
return m
}
func (m *Model) buildMenuItems() []MenuItem {
items := []MenuItem{
{Label: "Update", Action: StateUpdate},
}
// Shell management
if m.isShellRunning() {
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
} else {
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
}
// Plugins management
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
// Greeter management
items = append(items, MenuItem{Label: "Greeter", Action: StateGreeterMenu})
items = append(items, MenuItem{Label: "About", Action: StateAbout})
return items
}
func (m *Model) buildPluginsMenuItems() []MenuItem {
return []MenuItem{
{Label: "Browse Plugins", Action: StatePluginsBrowse},
{Label: "View Installed", Action: StatePluginsInstalled},
}
}
func (m *Model) isShellRunning() bool {
// Check for both -c and -p flag patterns since quickshell can be started either way
// -c dms: config name mode
// -p <path>/dms: path mode (used when installed via system packages)
cmd := exec.Command("pgrep", "-f", "qs.*dms")
err := cmd.Run()
return err == nil
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case shellStartedMsg:
m.menuItems = m.buildMenuItems()
if m.selectedItem >= len(m.menuItems) {
m.selectedItem = len(m.menuItems) - 1
}
return m, nil
case updateProgressMsg:
m.updateProgress = msg
if msg.logOutput != "" {
m.updateLogs = append(m.updateLogs, msg.logOutput)
}
return m, m.waitForProgress()
case updateCompleteMsg:
m.updateProgress.complete = true
m.updateProgress.err = msg.err
m.dependencies = m.detector.GetInstalledComponents()
m.updateDeps = m.dependencies
m.menuItems = m.buildMenuItems()
// Restart shell if update was successful and shell is running
if msg.err == nil && m.isShellRunning() {
restartShell()
}
return m, nil
case greeterProgressMsg:
m.greeterProgress = msg
if msg.logOutput != "" {
m.greeterLogs = append(m.greeterLogs, msg.logOutput)
}
return m, m.waitForGreeterProgress()
case pluginsLoadedMsg:
m.pluginsLoading = false
if msg.err != nil {
m.pluginsError = msg.err.Error()
} else {
m.pluginsList = make([]pluginInfo, len(msg.plugins))
for i, p := range msg.plugins {
m.pluginsList[i] = pluginInfo{
ID: p.ID,
Name: p.Name,
Category: p.Category,
Author: p.Author,
Description: p.Description,
Repo: p.Repo,
Path: p.Path,
Capabilities: p.Capabilities,
Compositors: p.Compositors,
Dependencies: p.Dependencies,
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
}
}
m.filteredPluginsList = m.pluginsList
m.selectedPluginIndex = 0
m.updatePluginInstallStatus()
}
return m, nil
case installedPluginsLoadedMsg:
m.installedPluginsLoading = false
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
for i, p := range msg.plugins {
m.installedPluginsList[i] = pluginInfo{
ID: p.ID,
Name: p.Name,
Category: p.Category,
Author: p.Author,
Description: p.Description,
Repo: p.Repo,
Path: p.Path,
Capabilities: p.Capabilities,
Compositors: p.Compositors,
Dependencies: p.Dependencies,
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
}
}
m.selectedInstalledIndex = 0
}
return m, nil
case pluginUninstalledMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
m.state = StatePluginInstalledDetail
} else {
m.state = StatePluginsInstalled
m.installedPluginsLoading = true
m.installedPluginsError = ""
return m, loadInstalledPlugins
}
return m, nil
case pluginUpdatedMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsError = ""
}
return m, nil
case pluginInstalledMsg:
if msg.err != nil {
m.pluginsError = msg.err.Error()
} else {
m.pluginInstallStatus[msg.pluginName] = true
m.pluginsError = ""
}
return m, nil
case greeterPasswordValidMsg:
if msg.valid {
m.greeterSudoPassword = msg.password
m.greeterPasswordInput = ""
m.greeterPasswordError = ""
m.state = StateGreeterInstalling
m.greeterProgress = greeterProgressMsg{step: "Starting greeter installation..."}
m.greeterLogs = []string{}
return m, tea.Batch(m.performGreeterInstall(), m.waitForGreeterProgress())
} else {
m.greeterPasswordError = "Incorrect password. Please try again."
m.greeterPasswordInput = ""
}
return m, nil
case passwordValidMsg:
if msg.valid {
m.sudoPassword = msg.password
m.passwordInput = ""
m.passwordError = ""
m.state = StateUpdateProgress
m.updateProgress = updateProgressMsg{progress: 0.0, step: "Starting update..."}
m.updateLogs = []string{}
return m, tea.Batch(m.performUpdate(), m.waitForProgress())
} else {
m.passwordError = "Incorrect password. Please try again."
m.passwordInput = ""
}
return m, nil
case tea.KeyMsg:
switch m.state {
case StateMainMenu:
return m.updateMainMenu(msg)
case StateUpdate:
return m.updateUpdateView(msg)
case StateUpdatePassword:
return m.updatePasswordView(msg)
case StateUpdateProgress:
return m.updateProgressView(msg)
case StateShell:
return m.updateShellView(msg)
case StatePluginsMenu:
return m.updatePluginsMenu(msg)
case StatePluginsBrowse:
return m.updatePluginsBrowse(msg)
case StatePluginDetail:
return m.updatePluginDetail(msg)
case StatePluginSearch:
return m.updatePluginSearch(msg)
case StatePluginsInstalled:
return m.updatePluginsInstalled(msg)
case StatePluginInstalledDetail:
return m.updatePluginInstalledDetail(msg)
case StateGreeterMenu:
return m.updateGreeterMenu(msg)
case StateGreeterCompositorSelect:
return m.updateGreeterCompositorSelect(msg)
case StateGreeterPassword:
return m.updateGreeterPasswordView(msg)
case StateGreeterInstalling:
return m.updateGreeterInstalling(msg)
case StateAbout:
return m.updateAboutView(msg)
}
}
return m, nil
}
type updateProgressMsg struct {
progress float64
step string
complete bool
err error
logOutput string
}
type updateCompleteMsg struct {
err error
}
type passwordValidMsg struct {
password string
valid bool
}
type greeterProgressMsg struct {
step string
complete bool
err error
logOutput string
}
type greeterPasswordValidMsg struct {
password string
valid bool
}
func (m Model) waitForProgress() tea.Cmd {
return func() tea.Msg {
return <-m.updateProgressChan
}
}
func (m Model) waitForGreeterProgress() tea.Cmd {
return func() tea.Msg {
return <-m.greeterInstallChan
}
}
func (m Model) View() string {
switch m.state {
case StateMainMenu:
return m.renderMainMenu()
case StateUpdate:
return m.renderUpdateView()
case StateUpdatePassword:
return m.renderPasswordView()
case StateUpdateProgress:
return m.renderProgressView()
case StateShell:
return m.renderShellView()
case StatePluginsMenu:
return m.renderPluginsMenu()
case StatePluginsBrowse:
return m.renderPluginsBrowse()
case StatePluginDetail:
return m.renderPluginDetail()
case StatePluginSearch:
return m.renderPluginSearch()
case StatePluginsInstalled:
return m.renderPluginsInstalled()
case StatePluginInstalledDetail:
return m.renderPluginInstalledDetail()
case StateGreeterMenu:
return m.renderGreeterMenu()
case StateGreeterCompositorSelect:
return m.renderGreeterCompositorSelect()
case StateGreeterPassword:
return m.renderGreeterPasswordView()
case StateGreeterInstalling:
return m.renderGreeterInstalling()
case StateAbout:
return m.renderAboutView()
default:
return m.renderMainMenu()
}
}
-267
View File
@@ -1,267 +0,0 @@
//go:build distro_binary
package dms
import (
"os/exec"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
type AppState int
const (
StateMainMenu AppState = iota
StateShell
StatePluginsMenu
StatePluginsBrowse
StatePluginDetail
StatePluginSearch
StatePluginsInstalled
StatePluginInstalledDetail
StateAbout
)
type Model struct {
version string
detector *Detector
dependencies []DependencyInfo
state AppState
selectedItem int
width int
height int
// Menu items
menuItems []MenuItem
// Window manager states
hyprlandInstalled bool
niriInstalled bool
pluginsMenuItems []MenuItem
selectedPluginsMenuItem int
pluginsList []pluginInfo
filteredPluginsList []pluginInfo
selectedPluginIndex int
pluginsLoading bool
pluginsError string
pluginSearchQuery string
installedPluginsList []pluginInfo
selectedInstalledIndex int
installedPluginsLoading bool
installedPluginsError string
pluginInstallStatus map[string]bool
}
type pluginInfo struct {
ID string
Name string
Category string
Author string
Description string
Repo string
Path string
Capabilities []string
Compositors []string
Dependencies []string
FirstParty bool
}
type MenuItem struct {
Label string
Action AppState
}
func NewModel(version string) Model {
detector, _ := NewDetector()
var dependencies []DependencyInfo
var hyprlandInstalled, niriInstalled bool
if detector != nil {
dependencies = detector.GetInstalledComponents()
hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus()
}
m := Model{
version: version,
detector: detector,
dependencies: dependencies,
state: StateMainMenu,
selectedItem: 0,
hyprlandInstalled: hyprlandInstalled,
niriInstalled: niriInstalled,
pluginInstallStatus: make(map[string]bool),
}
m.menuItems = m.buildMenuItems()
return m
}
func (m *Model) buildMenuItems() []MenuItem {
items := []MenuItem{}
// Shell management
if m.isShellRunning() {
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
} else {
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
}
// Plugins management
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
items = append(items, MenuItem{Label: "About", Action: StateAbout})
return items
}
func (m *Model) buildPluginsMenuItems() []MenuItem {
return []MenuItem{
{Label: "Browse Plugins", Action: StatePluginsBrowse},
{Label: "View Installed", Action: StatePluginsInstalled},
}
}
func (m *Model) isShellRunning() bool {
cmd := exec.Command("pgrep", "-f", "qs -c dms")
err := cmd.Run()
return err == nil
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case pluginsLoadedMsg:
m.pluginsLoading = false
if msg.err != nil {
m.pluginsError = msg.err.Error()
} else {
m.pluginsList = make([]pluginInfo, len(msg.plugins))
for i, p := range msg.plugins {
m.pluginsList[i] = pluginInfo{
ID: p.ID,
Name: p.Name,
Category: p.Category,
Author: p.Author,
Description: p.Description,
Repo: p.Repo,
Path: p.Path,
Capabilities: p.Capabilities,
Compositors: p.Compositors,
Dependencies: p.Dependencies,
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
}
}
m.filteredPluginsList = m.pluginsList
m.selectedPluginIndex = 0
m.updatePluginInstallStatus()
}
return m, nil
case installedPluginsLoadedMsg:
m.installedPluginsLoading = false
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
for i, p := range msg.plugins {
m.installedPluginsList[i] = pluginInfo{
ID: p.ID,
Name: p.Name,
Category: p.Category,
Author: p.Author,
Description: p.Description,
Repo: p.Repo,
Path: p.Path,
Capabilities: p.Capabilities,
Compositors: p.Compositors,
Dependencies: p.Dependencies,
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
}
}
m.selectedInstalledIndex = 0
}
return m, nil
case pluginUninstalledMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
m.state = StatePluginInstalledDetail
} else {
m.state = StatePluginsInstalled
m.installedPluginsLoading = true
m.installedPluginsError = ""
return m, loadInstalledPlugins
}
return m, nil
case pluginUpdatedMsg:
if msg.err != nil {
m.installedPluginsError = msg.err.Error()
} else {
m.installedPluginsError = ""
}
return m, nil
case pluginInstalledMsg:
if msg.err != nil {
m.pluginsError = msg.err.Error()
} else {
m.pluginInstallStatus[msg.pluginName] = true
m.pluginsError = ""
}
return m, nil
case tea.KeyMsg:
switch m.state {
case StateMainMenu:
return m.updateMainMenu(msg)
case StateShell:
return m.updateShellView(msg)
case StatePluginsMenu:
return m.updatePluginsMenu(msg)
case StatePluginsBrowse:
return m.updatePluginsBrowse(msg)
case StatePluginDetail:
return m.updatePluginDetail(msg)
case StatePluginSearch:
return m.updatePluginSearch(msg)
case StatePluginsInstalled:
return m.updatePluginsInstalled(msg)
case StatePluginInstalledDetail:
return m.updatePluginInstalledDetail(msg)
case StateAbout:
return m.updateAboutView(msg)
}
}
return m, nil
}
func (m Model) View() string {
switch m.state {
case StateMainMenu:
return m.renderMainMenu()
case StateShell:
return m.renderShellView()
case StatePluginsMenu:
return m.renderPluginsMenu()
case StatePluginsBrowse:
return m.renderPluginsBrowse()
case StatePluginDetail:
return m.renderPluginDetail()
case StatePluginSearch:
return m.renderPluginSearch()
case StatePluginsInstalled:
return m.renderPluginsInstalled()
case StatePluginInstalledDetail:
return m.renderPluginInstalledDetail()
case StateAbout:
return m.renderAboutView()
default:
return m.renderMainMenu()
}
}
-143
View File
@@ -1,143 +0,0 @@
package dms
import (
"context"
"os"
"os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
)
type Detector struct {
homeDir string
distribution distros.Distribution
}
func (d *Detector) GetDistribution() distros.Distribution {
return d.distribution
}
func NewDetector() (*Detector, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
logChan := make(chan string, 100)
go func() {
for range logChan {
}
}()
osInfo, err := distros.GetOSInfo()
if err != nil {
return nil, err
}
dist, err := distros.NewDistribution(osInfo.Distribution.ID, logChan)
if err != nil {
return nil, err
}
return &Detector{
homeDir: homeDir,
distribution: dist,
}, nil
}
func (d *Detector) IsDMSInstalled() bool {
_, err := config.LocateDMSConfig()
return err == nil
}
func (d *Detector) GetDependencyStatus() ([]deps.Dependency, error) {
hyprlandDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerHyprland)
if err != nil {
return nil, err
}
niriDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerNiri)
if err != nil {
return nil, err
}
// Combine dependencies and deduplicate
depMap := make(map[string]deps.Dependency)
for _, dep := range hyprlandDeps {
depMap[dep.Name] = dep
}
for _, dep := range niriDeps {
// If dependency already exists, keep the one that's installed or needs update
if existing, exists := depMap[dep.Name]; exists {
if dep.Status > existing.Status {
depMap[dep.Name] = dep
}
} else {
depMap[dep.Name] = dep
}
}
// Convert map back to slice
var allDeps []deps.Dependency
for _, dep := range depMap {
allDeps = append(allDeps, dep)
}
return allDeps, nil
}
func (d *Detector) GetWindowManagerStatus() (bool, bool, error) {
// Reuse the existing command detection logic from BaseDistribution
// Since all distros embed BaseDistribution, we can access it via interface
type CommandChecker interface {
CommandExists(string) bool
}
checker, ok := d.distribution.(CommandChecker)
if !ok {
// Fallback to direct command check if interface not available
hyprlandInstalled := d.commandExists("hyprland") || d.commandExists("Hyprland")
niriInstalled := d.commandExists("niri")
return hyprlandInstalled, niriInstalled, nil
}
hyprlandInstalled := checker.CommandExists("hyprland") || checker.CommandExists("Hyprland")
niriInstalled := checker.CommandExists("niri")
return hyprlandInstalled, niriInstalled, nil
}
func (d *Detector) commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func (d *Detector) GetInstalledComponents() []DependencyInfo {
dependencies, err := d.GetDependencyStatus()
if err != nil {
return []DependencyInfo{}
}
var components []DependencyInfo
for _, dep := range dependencies {
components = append(components, DependencyInfo{
Name: dep.Name,
Status: dep.Status,
Description: dep.Description,
Required: dep.Required,
})
}
return components
}
type DependencyInfo struct {
Name string
Status deps.DependencyStatus
Description string
Required bool
}
-54
View File
@@ -1,54 +0,0 @@
package dms
import (
"os/exec"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
tea "github.com/charmbracelet/bubbletea"
)
func (m Model) updateShellView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc":
m.state = StateMainMenu
default:
return m, tea.Quit
}
return m, nil
}
func (m Model) updateAboutView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c", "q", "esc":
if msg.String() == "esc" {
m.state = StateMainMenu
} else {
return m, tea.Quit
}
}
return m, nil
}
func terminateShell() {
patterns := []string{"dms run", "qs -c dms"}
for _, pattern := range patterns {
cmd := exec.Command("pkill", "-f", pattern)
cmd.Run()
}
}
func startShellDaemon() {
cmd := exec.Command("dms", "run", "-d")
if err := cmd.Start(); err != nil {
log.Errorf("Error starting daemon: %v", err)
}
}
func restartShell() {
terminateShell()
time.Sleep(500 * time.Millisecond)
startShellDaemon()
}
-392
View File
@@ -1,392 +0,0 @@
//go:build !distro_binary
package dms
import (
"context"
"fmt"
"os/exec"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
tea "github.com/charmbracelet/bubbletea"
)
func (m Model) updateUpdateView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
filteredDeps := m.getFilteredDeps()
maxIndex := len(filteredDeps) - 1
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc":
m.state = StateMainMenu
case "up", "k":
if m.selectedUpdateDep > 0 {
m.selectedUpdateDep--
}
case "down", "j":
if m.selectedUpdateDep < maxIndex {
m.selectedUpdateDep++
}
case " ":
if dep := m.getDepAtVisualIndex(m.selectedUpdateDep); dep != nil {
m.updateToggles[dep.Name] = !m.updateToggles[dep.Name]
}
case "enter":
hasSelected := false
for _, toggle := range m.updateToggles {
if toggle {
hasSelected = true
break
}
}
if !hasSelected {
m.state = StateMainMenu
return m, nil
}
m.state = StateUpdatePassword
m.passwordInput = ""
m.passwordError = ""
return m, nil
}
return m, nil
}
func (m Model) updatePasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
case "esc":
m.state = StateUpdate
m.passwordInput = ""
m.passwordError = ""
return m, nil
case "enter":
if m.passwordInput == "" {
return m, nil
}
return m, m.validatePassword(m.passwordInput)
case "backspace":
if len(m.passwordInput) > 0 {
m.passwordInput = m.passwordInput[:len(m.passwordInput)-1]
}
default:
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
m.passwordInput += msg.String()
}
}
return m, nil
}
func (m Model) updateProgressView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc":
if m.updateProgress.complete {
m.state = StateMainMenu
m.updateProgress = updateProgressMsg{}
m.updateLogs = []string{}
}
}
return m, nil
}
func (m Model) validatePassword(password string) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
stdin, err := cmd.StdinPipe()
if err != nil {
return passwordValidMsg{password: "", valid: false}
}
go func() {
defer stdin.Close()
fmt.Fprintf(stdin, "%s\n", password)
}()
output, err := cmd.CombinedOutput()
outputStr := string(output)
if err != nil {
if strings.Contains(outputStr, "Sorry, try again") ||
strings.Contains(outputStr, "incorrect password") ||
strings.Contains(outputStr, "authentication failure") {
return passwordValidMsg{password: "", valid: false}
}
return passwordValidMsg{password: "", valid: false}
}
return passwordValidMsg{password: password, valid: true}
}
}
func (m Model) performUpdate() tea.Cmd {
var depsToUpdate []deps.Dependency
for _, depInfo := range m.updateDeps {
if m.updateToggles[depInfo.Name] {
depsToUpdate = append(depsToUpdate, deps.Dependency{
Name: depInfo.Name,
Status: depInfo.Status,
Description: depInfo.Description,
Required: depInfo.Required,
})
}
}
if len(depsToUpdate) == 0 {
return func() tea.Msg {
return updateCompleteMsg{err: nil}
}
}
wm := deps.WindowManagerHyprland
if m.niriInstalled {
wm = deps.WindowManagerNiri
}
sudoPassword := m.sudoPassword
reinstallFlags := make(map[string]bool)
for name, toggled := range m.updateToggles {
if toggled {
reinstallFlags[name] = true
}
}
distribution := m.detector.GetDistribution()
progressChan := m.updateProgressChan
return func() tea.Msg {
installerChan := make(chan distros.InstallProgressMsg, 100)
go func() {
ctx := context.Background()
disabledFlags := make(map[string]bool)
err := distribution.InstallPackages(ctx, depsToUpdate, wm, sudoPassword, reinstallFlags, disabledFlags, false, installerChan)
close(installerChan)
if err != nil {
progressChan <- updateProgressMsg{complete: true, err: err}
} else {
progressChan <- updateProgressMsg{complete: true}
}
}()
go func() {
for msg := range installerChan {
progressChan <- updateProgressMsg{
progress: msg.Progress,
step: msg.Step,
complete: msg.IsComplete,
err: msg.Error,
logOutput: msg.LogOutput,
}
}
}()
return nil
}
}
func (m Model) updateGreeterMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
greeterMenuItems := []string{"Install Greeter"}
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc":
m.state = StateMainMenu
case "up", "k":
if m.selectedGreeterItem > 0 {
m.selectedGreeterItem--
}
case "down", "j":
if m.selectedGreeterItem < len(greeterMenuItems)-1 {
m.selectedGreeterItem++
}
case "enter", " ":
if m.selectedGreeterItem == 0 {
compositors := greeter.DetectCompositors()
if len(compositors) == 0 {
return m, nil
}
m.greeterCompositors = compositors
if len(compositors) > 1 {
m.state = StateGreeterCompositorSelect
m.greeterSelectedComp = 0
return m, nil
} else {
m.greeterChosenCompositor = compositors[0]
m.state = StateGreeterPassword
m.greeterPasswordInput = ""
m.greeterPasswordError = ""
return m, nil
}
}
}
return m, nil
}
func (m Model) updateGreeterCompositorSelect(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc":
m.state = StateGreeterMenu
return m, nil
case "up", "k":
if m.greeterSelectedComp > 0 {
m.greeterSelectedComp--
}
case "down", "j":
if m.greeterSelectedComp < len(m.greeterCompositors)-1 {
m.greeterSelectedComp++
}
case "enter", " ":
m.greeterChosenCompositor = m.greeterCompositors[m.greeterSelectedComp]
m.state = StateGreeterPassword
m.greeterPasswordInput = ""
m.greeterPasswordError = ""
return m, nil
}
return m, nil
}
func (m Model) updateGreeterPasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
case "esc":
m.state = StateGreeterMenu
m.greeterPasswordInput = ""
m.greeterPasswordError = ""
return m, nil
case "enter":
if m.greeterPasswordInput == "" {
return m, nil
}
return m, m.validateGreeterPassword(m.greeterPasswordInput)
case "backspace":
if len(m.greeterPasswordInput) > 0 {
m.greeterPasswordInput = m.greeterPasswordInput[:len(m.greeterPasswordInput)-1]
}
default:
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
m.greeterPasswordInput += msg.String()
}
}
return m, nil
}
func (m Model) updateGreeterInstalling(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "esc":
if m.greeterProgress.complete {
m.state = StateMainMenu
m.greeterProgress = greeterProgressMsg{}
m.greeterLogs = []string{}
}
}
return m, nil
}
func (m Model) performGreeterInstall() tea.Cmd {
progressChan := m.greeterInstallChan
sudoPassword := m.greeterSudoPassword
compositor := m.greeterChosenCompositor
return func() tea.Msg {
go func() {
logFunc := func(msg string) {
progressChan <- greeterProgressMsg{step: msg, logOutput: msg}
}
progressChan <- greeterProgressMsg{step: "Checking greetd installation..."}
if err := performGreeterInstallSteps(progressChan, logFunc, sudoPassword, compositor); err != nil {
progressChan <- greeterProgressMsg{step: "Installation failed", complete: true, err: err}
return
}
progressChan <- greeterProgressMsg{step: "Installation complete", complete: true}
}()
return nil
}
}
func (m Model) validateGreeterPassword(password string) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
stdin, err := cmd.StdinPipe()
if err != nil {
return greeterPasswordValidMsg{password: "", valid: false}
}
go func() {
defer stdin.Close()
fmt.Fprintf(stdin, "%s\n", password)
}()
output, err := cmd.CombinedOutput()
outputStr := string(output)
if err != nil {
if strings.Contains(outputStr, "Sorry, try again") ||
strings.Contains(outputStr, "incorrect password") ||
strings.Contains(outputStr, "authentication failure") {
return greeterPasswordValidMsg{password: "", valid: false}
}
return greeterPasswordValidMsg{password: "", valid: false}
}
return greeterPasswordValidMsg{password: password, valid: true}
}
}
func performGreeterInstallSteps(progressChan chan greeterProgressMsg, logFunc func(string), sudoPassword string, compositor string) error {
if err := greeter.EnsureGreetdInstalled(logFunc, sudoPassword); err != nil {
return err
}
progressChan <- greeterProgressMsg{step: "Detecting DMS installation..."}
dmsPath, err := greeter.DetectDMSPath()
if err != nil {
return err
}
logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath))
logFunc(fmt.Sprintf("✓ Selected compositor: %s", compositor))
progressChan <- greeterProgressMsg{step: "Copying greeter files..."}
if err := greeter.CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil {
return err
}
progressChan <- greeterProgressMsg{step: "Configuring greetd..."}
if err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, sudoPassword); err != nil {
return err
}
progressChan <- greeterProgressMsg{step: "Synchronizing DMS configurations..."}
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, sudoPassword); err != nil {
return err
}
return nil
}
-61
View File
@@ -1,61 +0,0 @@
//go:build !distro_binary
package dms
import (
"time"
tea "github.com/charmbracelet/bubbletea"
)
type shellStartedMsg struct{}
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c", "q", "esc":
return m, tea.Quit
case "up", "k":
if m.selectedItem > 0 {
m.selectedItem--
}
case "down", "j":
if m.selectedItem < len(m.menuItems)-1 {
m.selectedItem++
}
case "enter", " ":
if m.selectedItem < len(m.menuItems) {
selectedAction := m.menuItems[m.selectedItem].Action
selectedLabel := m.menuItems[m.selectedItem].Label
switch selectedAction {
case StateUpdate:
m.state = StateUpdate
m.selectedUpdateDep = 0
case StateShell:
if selectedLabel == "Terminate Shell" {
terminateShell()
m.menuItems = m.buildMenuItems()
if m.selectedItem >= len(m.menuItems) {
m.selectedItem = len(m.menuItems) - 1
}
} else {
startShellDaemon()
// Wait a moment for the daemon to actually start before checking status
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
return shellStartedMsg{}
})
}
case StatePluginsMenu:
m.state = StatePluginsMenu
m.selectedPluginsMenuItem = 0
m.pluginsMenuItems = m.buildPluginsMenuItems()
case StateGreeterMenu:
m.state = StateGreeterMenu
m.selectedGreeterItem = 0
case StateAbout:
m.state = StateAbout
}
}
}
return m, nil
}

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