mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b40fbeef56 |
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cat:*)",
|
||||
"Bash(git -C /home/purian23/dms diff --stat .github/workflows/)",
|
||||
"Bash(git -C /home/purian23/projects/danklinux diff --stat .github/workflows/)",
|
||||
"Bash(git -C /home/purian23/dms diff .github/workflows/)",
|
||||
"Bash(git -C /home/purian23/dms diff .github/workflows/run-ppa.yml)",
|
||||
"Bash(osc cat:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(git show-ref:*)",
|
||||
"Bash(git tag:*)",
|
||||
"Bash(bash -c 'ALL_PATHS=$(grep -A 5 \"\"<service name=\\\"\"download_url\\\"\">\"\" distro/debian/dms/_service | grep \"\"<param name=\\\"\"path\\\"\">\"\" | sed \"\"s/.*<param name=\\\"\"path\\\"\">\\(.*\\)<\\/param>.*/\\1/\"\"); SOURCE_PATH=\"\"\"\"; for path in $ALL_PATHS; do if echo \"\"$path\"\" | grep -qE \"\"(source|archive|\\.tar\\.(gz|xz|bz2))\"\" && ! echo \"\"$path\"\" | grep -qE \"\"(distropkg|binary)\"\"; then SOURCE_PATH=\"\"$path\"\"; break; fi; done; echo \"\"Selected path: $SOURCE_PATH\"\"')",
|
||||
"Bash(curl:*)",
|
||||
"Bash(tar:*)",
|
||||
"Bash(git -C /home/purian23/dms log:*)",
|
||||
"Bash(osc status:*)",
|
||||
"Bash(osc commit:*)",
|
||||
"Bash(osc up:*)",
|
||||
"Bash(osc results:*)",
|
||||
"Bash(osc api:*)",
|
||||
"Bash(systemctl:*)",
|
||||
"Bash(dms version:*)",
|
||||
"Bash(git describe:*)",
|
||||
"Bash(qmlsc:*)",
|
||||
"Bash(qmllint-qt6:*)",
|
||||
"Bash(make fmt:*)",
|
||||
"Bash(make test:*)",
|
||||
"Bash(dms chroma list-styles:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(time dms chroma:*)",
|
||||
"Bash(dms chroma:*)",
|
||||
"Bash(make build:*)",
|
||||
"Bash(pgrep:*)",
|
||||
"Bash(go build:*)",
|
||||
"Bash(/tmp/dms-test chroma:*)",
|
||||
"Bash(1)",
|
||||
"Bash(go install:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(journalctl:*)",
|
||||
"Bash(qdbus:*)",
|
||||
"Bash(TZ='Asia/Tokyo' date:*)",
|
||||
"Bash(dms --help:*)",
|
||||
"Bash(dms run:*)",
|
||||
"Bash(dms status:*)",
|
||||
"Bash(dms kill:*)",
|
||||
"Bash(tee:*)",
|
||||
"Bash(qmlscene:*)",
|
||||
"Bash(quickshell --version:*)",
|
||||
"WebFetch(domain:forum.qt.io)",
|
||||
"Bash(gh api:*)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
[*.sh]
|
||||
# like -i=4
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
[*.nix]
|
||||
# like -i=4
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -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']
|
||||
@@ -1,129 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Crashes or unexpected behaviors
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## DankMaterialShell Bug Report
|
||||
Limit your report to one issue per submission unless similarly related
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor
|
||||
options:
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: distribution
|
||||
attributes:
|
||||
label: Distribution
|
||||
options:
|
||||
- Arch Linux
|
||||
- CachyOS
|
||||
- Fedora
|
||||
- NixOS
|
||||
- Debian
|
||||
- Ubuntu
|
||||
- Gentoo
|
||||
- OpenSUSE
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: distribution_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: installation_method
|
||||
attributes:
|
||||
label: Select your Installation Method
|
||||
options:
|
||||
- DankInstaller
|
||||
- Distro Packaging
|
||||
- Source
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: original_installation_method
|
||||
attributes:
|
||||
label: Was this your original Installation method?
|
||||
options:
|
||||
- "Yes"
|
||||
- No (specify below)
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: original_installation_method_specify
|
||||
attributes:
|
||||
label: If no, specify
|
||||
placeholder: e.g., Distro Packaging, then Source
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
attributes:
|
||||
label: dms doctor -vC
|
||||
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
|
||||
placeholder: Paste the output of `dms doctor -vC` here
|
||||
value: |
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Brief description of the issue
|
||||
placeholder: What happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected_behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What did you expect to happen?
|
||||
placeholder: Describe the expected behavior
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Please provide detailed steps to reproduce the issue
|
||||
placeholder: |
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Error Messages/Logs
|
||||
description: Please include any error messages, stack traces, or relevant logs
|
||||
placeholder: |
|
||||
Paste error messages or logs here
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots/Recordings
|
||||
description: If applicable, add screenshots or screen recordings
|
||||
placeholder: Attach images or videos here
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,62 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement for DMS. Keep features focused on a single topic with clear benefits, examples, etc. Avoid vague or broad requests, they will be closed.
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## DankMaterialShell Feature Request
|
||||
- type: textarea
|
||||
id: feature_description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: Brief description of the feature requested
|
||||
placeholder: What feature would you like to see?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: use_case
|
||||
attributes:
|
||||
label: Use Case
|
||||
description: Explain the purpose of this feature/why it'd be useful to you
|
||||
placeholder: Why is this feature important?
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor(s)
|
||||
description: Is this feature specific to one or more compositors?
|
||||
options:
|
||||
- All compositors
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: compositor_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., Wayfire, Mutter, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: proposed_solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: If you have any ideas for how to implement this, please share!
|
||||
placeholder: Suggest a solution or approach
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives/Existing Solutions
|
||||
description: Include any similar/pre-existing products that solve this problem
|
||||
placeholder: List alternatives or existing solutions
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,124 +0,0 @@
|
||||
name: Support Request
|
||||
description: Help with installation, usage, or general questions about DankMaterialShell
|
||||
labels:
|
||||
- support
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## DankMaterialShell Support Request
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor
|
||||
options:
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: compositor_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., Wayfire, Mutter, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: distribution
|
||||
attributes:
|
||||
label: Distribution
|
||||
options:
|
||||
- Arch Linux
|
||||
- CachyOS
|
||||
- Fedora
|
||||
- NixOS
|
||||
- Debian
|
||||
- Ubuntu
|
||||
- Gentoo
|
||||
- OpenSUSE
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: distribution_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: installation_method
|
||||
attributes:
|
||||
label: Select your Installation Method
|
||||
options:
|
||||
- DankInstaller
|
||||
- Distro Packaging
|
||||
- Source
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: original_installation_method_different
|
||||
attributes:
|
||||
label: Was your original Installation method different?
|
||||
options:
|
||||
- "Yes"
|
||||
- No (specify below)
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: original_installation_method_specify
|
||||
attributes:
|
||||
label: If no, specify
|
||||
placeholder: e.g., Distro Packaging, then Source
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
attributes:
|
||||
label: dms doctor -vC
|
||||
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
|
||||
placeholder: Paste the output of `dms doctor -vC` here
|
||||
value: |
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Brief description of the support needed
|
||||
placeholder: What do you need help with?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solutions_tried
|
||||
attributes:
|
||||
label: Solutions Tried
|
||||
description: Describe what you've tried so far (commands, documentation, etc.)
|
||||
placeholder: List steps or resources you've already tried
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: configuration
|
||||
attributes:
|
||||
label: Configuration Details
|
||||
description: Include any relevant configuration if relevant
|
||||
placeholder: Add configuration or environment info
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots/Recordings
|
||||
description: If applicable, add screenshots or screen recordings
|
||||
placeholder: Attach images or videos here
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Update stable branch
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-stable:
|
||||
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: Push to stable branch
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force
|
||||
@@ -1,55 +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: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
|
||||
- name: Add flathub
|
||||
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Add a flatpak that mutagen could support
|
||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- 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
|
||||
@@ -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
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Pre-commit Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
jobs:
|
||||
pre-commit-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
|
||||
- name: Add flathub
|
||||
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Add a flatpak that mutagen could support
|
||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: core/go.mod
|
||||
|
||||
- name: run pre-commit hooks
|
||||
uses: j178/prek-action@v1
|
||||
@@ -1,419 +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: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- 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-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
||||
- **`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/
|
||||
|
||||
# Copy root assets directory to quickshell for systemd service and desktop file
|
||||
cp -r assets 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: Generate vendored source tarball
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
VERSION_NUM=${TAG#v}
|
||||
cd core
|
||||
go mod vendor
|
||||
cd ..
|
||||
tar czf "_release_assets/dms-cli-${VERSION_NUM}.tar.gz" \
|
||||
--transform "s,^core/,dms-cli-${VERSION_NUM}/," \
|
||||
--exclude='core/.git' \
|
||||
core/
|
||||
(cd _release_assets && sha256sum "dms-cli-${VERSION_NUM}.tar.gz" > "dms-cli-${VERSION_NUM}.tar.gz.sha256")
|
||||
|
||||
- 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 }}
|
||||
@@ -1,210 +0,0 @@
|
||||
name: DMS Copr Stable Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package to build (dms, dms-greeter, or both)'
|
||||
required: false
|
||||
default: 'dms'
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- both
|
||||
version:
|
||||
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
||||
required: false
|
||||
default: ''
|
||||
release:
|
||||
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
|
||||
required: false
|
||||
default: '1'
|
||||
|
||||
jobs:
|
||||
determine-packages:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
packages: ${{ steps.set-packages.outputs.packages }}
|
||||
steps:
|
||||
- name: Set package list
|
||||
id: set-packages
|
||||
run: |
|
||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||
if [ "$PACKAGE_INPUT" = "both" ]; then
|
||||
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-and-upload:
|
||||
needs: determine-packages
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJSON(needs.determine-packages.outputs.packages) }}
|
||||
|
||||
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 ${{ matrix.package }} 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')"
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
# Copy spec file from repository
|
||||
cp distro/fedora/${PACKAGE}.spec ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
|
||||
# Replace placeholders with actual values
|
||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
|
||||
echo "✅ Spec file generated for ${PACKAGE} v${VERSION}-${RELEASE}"
|
||||
echo ""
|
||||
echo "=== Spec file preview ==="
|
||||
head -40 ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
|
||||
- name: Build SRPM
|
||||
id: build
|
||||
run: |
|
||||
cd ~/rpmbuild/SPECS
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
echo "🔨 Building SRPM for ${PACKAGE}..."
|
||||
rpmbuild -bs ${PACKAGE}.spec
|
||||
|
||||
SRPM=$(ls ~/rpmbuild/SRPMS/${PACKAGE}-*.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: ${{ matrix.package }}-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: Determine Copr project
|
||||
id: copr_project
|
||||
run: |
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
if [ "$PACKAGE" = "dms" ]; then
|
||||
COPR_PROJECT="avengemedia/dms"
|
||||
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
||||
COPR_PROJECT="avengemedia/danklinux"
|
||||
else
|
||||
echo "❌ Unknown package: $PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
echo "copr_project=$COPR_PROJECT" >> $GITHUB_OUTPUT
|
||||
echo "✅ Copr project: $COPR_PROJECT"
|
||||
|
||||
- name: Upload to Copr
|
||||
run: |
|
||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
echo "🚀 Uploading ${PACKAGE} SRPM to ${COPR_PROJECT}..."
|
||||
echo " SRPM: $(basename $SRPM)"
|
||||
echo " Version: $VERSION"
|
||||
|
||||
BUILD_OUTPUT=$(copr-cli build "$COPR_PROJECT" "$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/${COPR_PROJECT}/build/$BUILD_ID/"
|
||||
else
|
||||
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
||||
fi
|
||||
|
||||
- name: Build summary
|
||||
if: always()
|
||||
run: |
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||
echo "### 🎉 ${PACKAGE} Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Package:** ${PACKAGE}" >> $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/${COPR_PROJECT}/" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1,513 +0,0 @@
|
||||
name: Update OBS Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to update"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-git
|
||||
- dms-greeter
|
||||
- all
|
||||
default: "dms"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
default: ""
|
||||
schedule:
|
||||
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||
|
||||
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: Check for updates
|
||||
id: check
|
||||
env:
|
||||
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
|
||||
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
|
||||
run: |
|
||||
# Helper function to check dms-git commit
|
||||
check_dms_git() {
|
||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
|
||||
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-git: New commit $CURRENT_COMMIT (OBS has ${OBS_COMMIT:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check dms stable tag
|
||||
# Sets LATEST_TAG variable in parent scope if update needed
|
||||
check_dms_stable() {
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
|
||||
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check dms-greeter stable tag
|
||||
check_dms_greeter_stable() {
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
||||
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||
# Run from tag with no package specified - update both stable packages
|
||||
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION (always update)"
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - check dms-git only
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
if check_dms_git; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow trigger
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
|
||||
if [[ -n "$REBUILD" ]]; then
|
||||
# Rebuild requested - always proceed
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
if check_dms_greeter_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
if check_dms_git; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms" ]]; then
|
||||
if check_dms_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||
if check_dms_greeter_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: $PKG"
|
||||
fi
|
||||
else
|
||||
# Fallback - proceed
|
||||
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: 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: |
|
||||
# Use check-updates outputs when available
|
||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
# Use version from check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - dms-git only
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by schedule: updating git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow dispatch
|
||||
|
||||
# Determine version for dms stable and dms-greeter using the API
|
||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Using latest release from API: $LATEST_TAG"
|
||||
else
|
||||
echo "ERROR: Could not fetch latest release from API"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||
else
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
fi
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
run: |
|
||||
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 "1.0.2")
|
||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||
|
||||
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||
|
||||
# Single changelog entry (git snapshots don't need history)
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms-git.spec)
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1"
|
||||
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||
} > 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: |
|
||||
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 "1.0.2")
|
||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||
|
||||
# Single changelog entry (git snapshots don't need history)
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms-git (${NEW_VERSION}db1) nightly; urgency=medium"
|
||||
echo ""
|
||||
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms-git/debian/changelog"
|
||||
|
||||
- name: Update stable version (dms + dms-greeter)
|
||||
if: steps.packages.outputs.version != ''
|
||||
run: |
|
||||
VERSION="${{ steps.packages.outputs.version }}"
|
||||
VERSION_NO_V="${VERSION#v}"
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||
|
||||
# Update dms spec and changelog when dms is in the upload list
|
||||
if [[ "$PACKAGES" == *"dms"* ]]; then
|
||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
|
||||
|
||||
DATE_STR=$(date "+%a %b %d %Y")
|
||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||
{
|
||||
echo "$LOCAL_SPEC_HEAD"
|
||||
echo "%changelog"
|
||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
||||
echo "- Update to stable $VERSION release"
|
||||
} > distro/opensuse/dms.spec
|
||||
|
||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Update to $VERSION stable release"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms/debian/changelog"
|
||||
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update dms-greeter changelog when dms-greeter is in the upload list
|
||||
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
|
||||
CHANGELOG_DATE=$(date -R)
|
||||
{
|
||||
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
|
||||
echo ""
|
||||
echo " * Update to $VERSION stable release"
|
||||
echo ""
|
||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||
} > "distro/debian/dms-greeter/debian/changelog"
|
||||
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
|
||||
fi
|
||||
|
||||
# Update Debian _service files for packages in upload list (download_url paths)
|
||||
for service in distro/debian/*/_service; do
|
||||
if [[ -f "$service" ]]; then
|
||||
# Update tar_scm revision parameter (for dms-git)
|
||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||
# Update download_url paths (for dms, dms-greeter stable)
|
||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- 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
|
||||
id: upload
|
||||
env:
|
||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "✓ No packages need uploading. All up to date!"
|
||||
echo "uploaded_packages=" >> $GITHUB_OUTPUT
|
||||
echo "skipped_packages=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MESSAGE="Automated update from GitHub Actions"
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||
fi
|
||||
|
||||
UPLOADED_PACKAGES=()
|
||||
SKIPPED_PACKAGES=()
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading $PKG to OBS..."
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
echo "🔄 Using rebuild release number: db$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
LOG_FILE=$(mktemp)
|
||||
set +e
|
||||
if [[ "$PKG" == "dms-git" ]]; then
|
||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
|
||||
else
|
||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
|
||||
fi
|
||||
STATUS=$?
|
||||
set -e
|
||||
|
||||
cat "$LOG_FILE"
|
||||
|
||||
if [[ $STATUS -ne 0 ]]; then
|
||||
rm -f "$LOG_FILE"
|
||||
echo "❌ Upload failed for $PKG"
|
||||
exit $STATUS
|
||||
fi
|
||||
|
||||
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
|
||||
echo "ℹ️ $PKG is already up to date. Skipped."
|
||||
SKIPPED_PACKAGES+=("$PKG")
|
||||
else
|
||||
UPLOADED_PACKAGES+=("$PKG")
|
||||
fi
|
||||
|
||||
rm -f "$LOG_FILE"
|
||||
done
|
||||
|
||||
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### OBS Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
|
||||
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
|
||||
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
|
||||
UPLOADED_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
if [[ -n "$UPLOADED_PACKAGES" ]]; then
|
||||
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
|
||||
fi
|
||||
if [[ -n "$SKIPPED_PACKAGES" ]]; then
|
||||
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
|
||||
fi
|
||||
in_list() {
|
||||
local item="$1"
|
||||
local list="$2"
|
||||
[[ " $list " == *" $item "* ]]
|
||||
}
|
||||
|
||||
if [[ "${{ job.status }}" == "success" ]]; then
|
||||
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for PKG in $PACKAGES; do
|
||||
STATUS_ICON="✅"
|
||||
STATUS_TEXT="uploaded"
|
||||
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
|
||||
STATUS_ICON="ℹ️"
|
||||
STATUS_TEXT="up to date (skipped)"
|
||||
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
|
||||
STATUS_ICON="❌"
|
||||
STATUS_TEXT="failed"
|
||||
fi
|
||||
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-git)
|
||||
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-greeter)
|
||||
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
echo "**Rebuild Number:** db${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||
echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -1,280 +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 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||
|
||||
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: |
|
||||
# Helper function to check dms-git commit
|
||||
check_dms_git() {
|
||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
|
||||
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check stable package tag
|
||||
check_stable_package() {
|
||||
local PKG="$1"
|
||||
local PPA_NAME="$2"
|
||||
# Use git ls-remote to find the latest tag, sorted by version (descending)
|
||||
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
|
||||
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
|
||||
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
|
||||
return 0 # Update needed
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - check dms-git only
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
if check_dms_git; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow trigger
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
|
||||
if [[ -n "$REBUILD" ]]; then
|
||||
# Rebuild requested - always proceed
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
|
||||
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
if check_dms_git; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms" ]]; then
|
||||
if check_stable_package "dms" "dms"; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||
if check_stable_package "dms-greeter" "danklinux"; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: $PKG"
|
||||
fi
|
||||
else
|
||||
# Fallback
|
||||
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: 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-file: ./core/go.mod
|
||||
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: |
|
||||
# Use packages determined by check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "Triggered by schedule: uploading git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
|
||||
fi
|
||||
|
||||
- name: Upload to PPA
|
||||
run: |
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "✓ No packages need uploading. All up to date!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Export REBUILD_RELEASE so ppa-build.sh can use it
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
export REBUILD_RELEASE
|
||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
# Map package to PPA name
|
||||
case "$PKG" in
|
||||
dms)
|
||||
PPA_NAME="dms"
|
||||
;;
|
||||
dms-git)
|
||||
PPA_NAME="dms-git"
|
||||
;;
|
||||
dms-greeter)
|
||||
PPA_NAME="danklinux"
|
||||
;;
|
||||
*)
|
||||
echo "⚠️ Unknown package: $PKG, skipping"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Uploading $PKG to PPA $PPA_NAME..."
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
for PKG in $PACKAGES; do
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-git)
|
||||
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-greeter)
|
||||
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -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 .#dms-shell 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 || true)
|
||||
[ -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 .#dms-shell
|
||||
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 ${{ github.ref_name }}
|
||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||
else
|
||||
echo "No changes to flake.nix"
|
||||
fi
|
||||
-114
@@ -1,114 +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
|
||||
AGENTS-activeContext.md
|
||||
AGENTS-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/
|
||||
vim/
|
||||
|
||||
bin/
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv/
|
||||
quickshell/dms-plugins
|
||||
__pycache__
|
||||
@@ -1,22 +0,0 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
name: shellcheck
|
||||
entry: shellcheck -e SC2164 -e SC2001 -e SC2012 -e SC2317
|
||||
language: system
|
||||
types: [shell]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: go-mod-tidy
|
||||
name: go mod tidy
|
||||
entry: bash -c 'cd core && go mod tidy'
|
||||
language: system
|
||||
files: ^core/.*\.(go|mod|sum)$
|
||||
pass_filenames: false
|
||||
@@ -1,33 +0,0 @@
|
||||
This file is more of a quick reference so I know what to account for before next releases.
|
||||
|
||||
# 1.4.0
|
||||
|
||||
- Overhauled system monitor, graphs, styling
|
||||
- dbus API for plugins, KDEConnect
|
||||
- new dank16 algorithm
|
||||
- launcher actions, customize env, args, name, icon
|
||||
- launcher v2 - omega stuff, GIF search, supa powerful
|
||||
- dock on bar
|
||||
- window rule manager, with IPC - #TODO verify RTL layout (niri only)
|
||||
|
||||
# 1.2.0
|
||||
|
||||
- Added clipboard and clipboard history integration
|
||||
- Added swipe to dismiss notification popups and from center
|
||||
- Added paste from clipboard history view - requires wtype
|
||||
- Optimize surface damage of OSD & Toast
|
||||
- Add monitor configurator (niri, Hyprland, MangoWC)
|
||||
- **BREAKING** ghostty theme changed to ~/.config/ghostty/themes/danktheme
|
||||
- requires intervention and doc update
|
||||
- Added desktop widget plugins
|
||||
- dev guidance available
|
||||
- builtin clock & dgop widgets
|
||||
- new IPC targets
|
||||
- Initial RTL support/i18n
|
||||
- Theme registry
|
||||
- Notification persistence & history
|
||||
- **BREAKING** vscode theme needs re-installed
|
||||
- dms doctor cmd
|
||||
- niri/hypr/mango gaps/window/border overrides
|
||||
- settings search
|
||||
- notification display ops on lock screen
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
Contributions are welcome and encouraged.
|
||||
|
||||
To contribute fork this repository, make your changes, and open a pull request.
|
||||
|
||||
## Setup
|
||||
|
||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
||||
|
||||
```bash
|
||||
prek install
|
||||
```
|
||||
|
||||
### 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.25+ 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
|
||||
|
||||
**Note:** Paths may vary by distribution. Below are examples for Arch Linux and Fedora.
|
||||
|
||||
**Arch Linux:**
|
||||
|
||||
```json
|
||||
{
|
||||
"[qml]": {
|
||||
"editor.defaultFormatter": "qt-project.qmlls",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"qt-qml.doNotAskForQmllsDownload": true,
|
||||
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls",
|
||||
"qt-core.additionalQtPaths": [
|
||||
{
|
||||
"name": "Qt-6.x-linux-g++",
|
||||
"path": "/usr/bin/qmake"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Fedora:**
|
||||
|
||||
```json
|
||||
{
|
||||
"[qml]": {
|
||||
"editor.defaultFormatter": "qt-project.qmlls",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"qt-qml.doNotAskForQmllsDownload": true,
|
||||
"qt-qml.qmlls.customExePath": "/usr/bin/qmlls",
|
||||
"qt-core.additionalQtPaths": [
|
||||
{
|
||||
"name": "Qt-6.x-Fedora-linux-g++",
|
||||
"path": "/usr/bin/qmake6"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### I18n/Localization
|
||||
|
||||
When adding user-facing strings, ensure they are wrapped in `I18n.tr()` with context, for example.
|
||||
|
||||
```qml
|
||||
import qs.Common
|
||||
|
||||
Text {
|
||||
text: I18n.tr("Hello World", "<This is context for the translators, example> Hello world greeting that appears on the lock screen")
|
||||
}
|
||||
```
|
||||
|
||||
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
||||
|
||||
### 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.
|
||||
@@ -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.
|
||||
@@ -1,155 +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
|
||||
@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):"$(id -gn $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):"$(id -gn $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"
|
||||
@@ -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/)
|
||||
|
||||
[](https://danklinux.com/docs)
|
||||
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
||||
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||
[](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), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), 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/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) 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.dank-material-shell ];
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
[](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See [LICENSE](LICENSE) for details.
|
||||
@@ -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 |
@@ -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 |
@@ -1,10 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=DMS
|
||||
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;x-scheme-handler/dms;text/html;application/xhtml+xml;
|
||||
Categories=Utility;
|
||||
@@ -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=on-failure
|
||||
RestartSec=1.23
|
||||
TimeoutStopSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
@@ -1,112 +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/Read to bytes.Buffer can't fail
|
||||
- linters:
|
||||
- errcheck
|
||||
text: "Error return value of `binary\\.(Write|Read)` is not checked"
|
||||
# bytes.Reader.Read can't fail (reads from memory)
|
||||
- linters:
|
||||
- errcheck
|
||||
text: "Error return value of `buf\\.Read` is not checked"
|
||||
@@ -1,76 +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:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
|
||||
config:
|
||||
dir: "internal/mocks/wlcontext"
|
||||
outpkg: mocks_wlcontext
|
||||
interfaces:
|
||||
WaylandContext:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
|
||||
config:
|
||||
dir: "internal/mocks/wlclient"
|
||||
outpkg: mocks_wlclient
|
||||
interfaces:
|
||||
WaylandDisplay:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
||||
config:
|
||||
dir: "internal/mocks/utils"
|
||||
outpkg: mocks_utils
|
||||
interfaces:
|
||||
AppChecker:
|
||||
@@ -1,16 +0,0 @@
|
||||
repos:
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.9.0
|
||||
hooks:
|
||||
- id: golangci-lint-fmt
|
||||
require_serial: true
|
||||
- id: golangci-lint-full
|
||||
- id: golangci-lint-config-verify
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: go-test
|
||||
name: go test
|
||||
entry: go test ./...
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [go]
|
||||
-163
@@ -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"
|
||||
-177
@@ -1,177 +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 (Client)
|
||||
|
||||
All Wayland protocols are consumed as a client - connecting to the compositor.
|
||||
|
||||
| Protocol | Purpose |
|
||||
| ----------------------------------------- | ----------------------------------------------------------- |
|
||||
| `wlr-gamma-control-unstable-v1` | Night mode color temperature control |
|
||||
| `wlr-screencopy-unstable-v1` | Screen capture for color picker/screenshot |
|
||||
| `wlr-layer-shell-unstable-v1` | Overlay surfaces for color picker UI/screenshot |
|
||||
| `wlr-output-management-unstable-v1` | Display configuration |
|
||||
| `wlr-output-power-management-unstable-v1` | DPMS on/off CLI |
|
||||
| `wp-viewporter` | Fractional scaling support (color picker/screenshot UIs) |
|
||||
| `keyboard-shortcuts-inhibit-unstable-v1` | Inhibit compositor shortcuts during color picker/screenshot |
|
||||
| `ext-data-control-v1` | Clipboard history and persistence |
|
||||
| `ext-workspace-v1` | Workspace integration |
|
||||
| `dwl-ipc-unstable-v2` | dwl/MangoWC IPC for tags, outputs, etc. |
|
||||
|
||||
### DBus Interfaces
|
||||
|
||||
**Client (consuming external services):**
|
||||
|
||||
| Interface | Purpose |
|
||||
| -------------------------------- | --------------------------------------------- |
|
||||
| `org.bluez` | Bluetooth management with pairing agent |
|
||||
| `org.freedesktop.NetworkManager` | Network management |
|
||||
| `net.connman.iwd` | iwd Wi-Fi backend |
|
||||
| `org.freedesktop.network1` | systemd-networkd integration |
|
||||
| `org.freedesktop.login1` | Session control, sleep inhibitors, brightness |
|
||||
| `org.freedesktop.Accounts` | User account information |
|
||||
| `org.freedesktop.portal.Desktop` | Desktop appearance settings (color scheme) |
|
||||
| CUPS via IPP + D-Bus | Printer management with job notifications |
|
||||
|
||||
**Server (implementing interfaces):**
|
||||
|
||||
| Interface | Purpose |
|
||||
| ----------------------------- | -------------------------------------- |
|
||||
| `org.freedesktop.ScreenSaver` | Screensaver inhibit for video playback |
|
||||
|
||||
Custom IPC via unix socket (JSON API) for shell communication.
|
||||
|
||||
### Hardware Control
|
||||
|
||||
| Subsystem | Method | Purpose |
|
||||
| --------- | ------------------- | ---------------------------------- |
|
||||
| DDC/CI | I2C direct | External monitor brightness |
|
||||
| Backlight | logind or sysfs | Internal display brightness |
|
||||
| evdev | `/dev/input/event*` | Keyboard state (caps lock LED) |
|
||||
| udev | netlink monitor | Backlight device updates (for OSD) |
|
||||
|
||||
### 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.25+
|
||||
|
||||
**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.
|
||||
@@ -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 |
@@ -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 |
@@ -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-*
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"policy_version": 1,
|
||||
"blocked_commands": [
|
||||
"greeter install",
|
||||
"greeter enable",
|
||||
"greeter uninstall",
|
||||
"setup"
|
||||
],
|
||||
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var blurCmd = &cobra.Command{
|
||||
Use: "blur",
|
||||
Short: "Background blur utilities",
|
||||
}
|
||||
|
||||
var blurCheckCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
||||
Args: cobra.NoArgs,
|
||||
Run: runBlurCheck,
|
||||
}
|
||||
|
||||
func init() {
|
||||
blurCmd.AddCommand(blurCheckCmd)
|
||||
}
|
||||
|
||||
func runBlurCheck(cmd *cobra.Command, args []string) {
|
||||
supported, err := blur.ProbeSupport()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch supported {
|
||||
case true:
|
||||
fmt.Println("supported")
|
||||
default:
|
||||
fmt.Println("unsupported")
|
||||
}
|
||||
}
|
||||
@@ -1,311 +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 range sepLen {
|
||||
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 {
|
||||
ddc.WaitPending()
|
||||
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)
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yuin/goldmark"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
ghtml "github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
var (
|
||||
chromaLanguage string
|
||||
chromaStyle string
|
||||
chromaInline bool
|
||||
chromaMarkdown bool
|
||||
chromaLineNumbers bool
|
||||
|
||||
// Caching layer for performance
|
||||
lexerCache = make(map[string]chroma.Lexer)
|
||||
styleCache = make(map[string]*chroma.Style)
|
||||
formatterCache = make(map[string]*html.Formatter)
|
||||
cacheMutex sync.RWMutex
|
||||
maxFileSize = int64(5 * 1024 * 1024) // 5MB default
|
||||
)
|
||||
|
||||
var chromaCmd = &cobra.Command{
|
||||
Use: "chroma [file]",
|
||||
Short: "Syntax highlight source code",
|
||||
Long: `Generate syntax-highlighted HTML from source code.
|
||||
|
||||
Reads from file or stdin, outputs HTML with syntax highlighting.
|
||||
Language is auto-detected from filename or can be specified with --language.
|
||||
|
||||
Examples:
|
||||
dms chroma main.go
|
||||
dms chroma --language python script.py
|
||||
echo "def foo(): pass" | dms chroma -l python
|
||||
cat code.rs | dms chroma -l rust --style dracula
|
||||
dms chroma --markdown README.md
|
||||
dms chroma --markdown --style github-dark notes.md
|
||||
dms chroma list-languages
|
||||
dms chroma list-styles`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: runChroma,
|
||||
}
|
||||
|
||||
var chromaListLanguagesCmd = &cobra.Command{
|
||||
Use: "list-languages",
|
||||
Short: "List all supported languages",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for _, name := range lexers.Names(true) {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var chromaListStylesCmd = &cobra.Command{
|
||||
Use: "list-styles",
|
||||
Short: "List all available color styles",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for _, name := range styles.Names() {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chromaCmd.Flags().StringVarP(&chromaLanguage, "language", "l", "", "Language for highlighting (auto-detect if not specified)")
|
||||
chromaCmd.Flags().StringVarP(&chromaStyle, "style", "s", "monokai", "Color style (monokai, dracula, github, etc.)")
|
||||
chromaCmd.Flags().BoolVar(&chromaInline, "inline", false, "Output inline styles instead of CSS classes")
|
||||
chromaCmd.Flags().BoolVar(&chromaLineNumbers, "line-numbers", false, "Show line numbers in output")
|
||||
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
|
||||
chromaCmd.Flags().Int64Var(&maxFileSize, "max-size", 5*1024*1024, "Maximum file size to process without warning (bytes)")
|
||||
|
||||
chromaCmd.AddCommand(chromaListLanguagesCmd)
|
||||
chromaCmd.AddCommand(chromaListStylesCmd)
|
||||
}
|
||||
|
||||
func getCachedLexer(key string, fallbackFunc func() chroma.Lexer) chroma.Lexer {
|
||||
cacheMutex.RLock()
|
||||
if lexer, ok := lexerCache[key]; ok {
|
||||
cacheMutex.RUnlock()
|
||||
return lexer
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
lexer := fallbackFunc()
|
||||
if lexer != nil {
|
||||
cacheMutex.Lock()
|
||||
lexerCache[key] = lexer
|
||||
cacheMutex.Unlock()
|
||||
}
|
||||
return lexer
|
||||
}
|
||||
|
||||
func getCachedStyle(name string) *chroma.Style {
|
||||
cacheMutex.RLock()
|
||||
if style, ok := styleCache[name]; ok {
|
||||
cacheMutex.RUnlock()
|
||||
return style
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
style := styles.Get(name)
|
||||
if style == nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
|
||||
style = styles.Fallback
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
styleCache[name] = style
|
||||
cacheMutex.Unlock()
|
||||
return style
|
||||
}
|
||||
|
||||
func getCachedFormatter(inline bool, lineNumbers bool) *html.Formatter {
|
||||
key := fmt.Sprintf("inline=%t,lineNumbers=%t", inline, lineNumbers)
|
||||
|
||||
cacheMutex.RLock()
|
||||
if formatter, ok := formatterCache[key]; ok {
|
||||
cacheMutex.RUnlock()
|
||||
return formatter
|
||||
}
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
var opts []html.Option
|
||||
if inline {
|
||||
opts = append(opts, html.WithClasses(false))
|
||||
} else {
|
||||
opts = append(opts, html.WithClasses(true))
|
||||
}
|
||||
opts = append(opts, html.TabWidth(4))
|
||||
|
||||
if lineNumbers {
|
||||
opts = append(opts, html.WithLineNumbers(true))
|
||||
opts = append(opts, html.LineNumbersInTable(false))
|
||||
opts = append(opts, html.WithLinkableLineNumbers(false, ""))
|
||||
}
|
||||
|
||||
formatter := html.New(opts...)
|
||||
|
||||
cacheMutex.Lock()
|
||||
formatterCache[key] = formatter
|
||||
cacheMutex.Unlock()
|
||||
return formatter
|
||||
}
|
||||
|
||||
func runChroma(cmd *cobra.Command, args []string) {
|
||||
var source string
|
||||
var filename string
|
||||
|
||||
// Read from file or stdin
|
||||
if len(args) > 0 {
|
||||
filename = args[0]
|
||||
|
||||
// Check file size before reading
|
||||
fileInfo, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file info: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if fileInfo.Size() > maxFileSize {
|
||||
fmt.Fprintf(os.Stderr, "Warning: File size (%d bytes) exceeds recommended limit (%d bytes)\n",
|
||||
fileInfo.Size(), maxFileSize)
|
||||
fmt.Fprintf(os.Stderr, "Processing may be slow. Consider using smaller files.\n")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
source = string(content)
|
||||
} else {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||
_ = cmd.Help()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
source = string(content)
|
||||
}
|
||||
|
||||
// Handle empty input
|
||||
if strings.TrimSpace(source) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Markdown rendering
|
||||
if chromaMarkdown {
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle(chromaStyle),
|
||||
highlighting.WithFormatOptions(
|
||||
html.WithClasses(!chromaInline),
|
||||
),
|
||||
),
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
ghtml.WithHardWraps(),
|
||||
ghtml.WithXHTML(),
|
||||
),
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert([]byte(source), &buf); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Markdown rendering error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Detect or use specified lexer
|
||||
var lexer chroma.Lexer
|
||||
if chromaLanguage != "" {
|
||||
lexer = getCachedLexer(chromaLanguage, func() chroma.Lexer {
|
||||
l := lexers.Get(chromaLanguage)
|
||||
if l == nil {
|
||||
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
|
||||
os.Exit(1)
|
||||
}
|
||||
return l
|
||||
})
|
||||
} else if filename != "" {
|
||||
lexer = getCachedLexer("file:"+filename, func() chroma.Lexer {
|
||||
return lexers.Match(filename)
|
||||
})
|
||||
}
|
||||
|
||||
// Try content analysis if no lexer found (limit to first 1KB for performance)
|
||||
if lexer == nil {
|
||||
analyzeContent := source
|
||||
if len(source) > 1024 {
|
||||
analyzeContent = source[:1024]
|
||||
}
|
||||
lexer = lexers.Analyse(analyzeContent)
|
||||
}
|
||||
|
||||
// Fallback to plaintext
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
|
||||
// Get cached style
|
||||
style := getCachedStyle(chromaStyle)
|
||||
|
||||
// Get cached formatter
|
||||
formatter := getCachedFormatter(chromaInline, chromaLineNumbers)
|
||||
|
||||
// Tokenize
|
||||
iterator, err := lexer.Tokenise(nil, source)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Tokenization error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Format and output
|
||||
if chromaLineNumbers {
|
||||
var buf bytes.Buffer
|
||||
if err := formatter.Format(&buf, style, iterator); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Add spacing between line numbers
|
||||
output := buf.String()
|
||||
output = strings.ReplaceAll(output, "</span><span>", "</span>\u00A0\u00A0<span>")
|
||||
fmt.Print(output)
|
||||
} else {
|
||||
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,127 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"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) {
|
||||
if err := clipboard.CopyText(text); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "clipboard copy failed:", err)
|
||||
}
|
||||
}
|
||||
@@ -1,530 +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 [target] [function] [args...]",
|
||||
Short: "Send IPC commands to running DMS shell",
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||
_ = findConfig(cmd, args)
|
||||
printIPCHelp()
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
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 "1.0.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,
|
||||
notifyCmd,
|
||||
genericNotifyActionCmd,
|
||||
matugenCmd,
|
||||
clipboardCmd,
|
||||
chromaCmd,
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
dlCmd,
|
||||
randrCmd,
|
||||
blurCmd,
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Configuration utilities",
|
||||
}
|
||||
|
||||
var resolveIncludeCmd = &cobra.Command{
|
||||
Use: "resolve-include <compositor> <filename>",
|
||||
Short: "Check if a file is included in compositor config",
|
||||
Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
||||
case 1:
|
||||
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runResolveInclude,
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(resolveIncludeCmd)
|
||||
}
|
||||
|
||||
type IncludeResult struct {
|
||||
Exists bool `json:"exists"`
|
||||
Included bool `json:"included"`
|
||||
}
|
||||
|
||||
func runResolveInclude(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
filename := args[1]
|
||||
|
||||
var result IncludeResult
|
||||
var err error
|
||||
|
||||
switch compositor {
|
||||
case "hyprland":
|
||||
result, err = checkHyprlandInclude(filename)
|
||||
case "niri":
|
||||
result, err = checkNiriInclude(filename)
|
||||
case "mangowc", "dwl", "mango":
|
||||
result, err = checkMangoWCInclude(filename)
|
||||
default:
|
||||
log.Fatalf("Unknown compositor: %s", compositor)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error checking include: %v", err)
|
||||
}
|
||||
|
||||
output, _ := json.Marshal(result)
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
}
|
||||
|
||||
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||
if err != nil {
|
||||
return IncludeResult{}, err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(configDir, "dms", filename)
|
||||
result := IncludeResult{}
|
||||
|
||||
if _, err := os.Stat(targetPath); err == nil {
|
||||
result.Exists = true
|
||||
}
|
||||
|
||||
mainConfig := filepath.Join(configDir, "hyprland.conf")
|
||||
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
processed := make(map[string]bool)
|
||||
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if processed[absPath] {
|
||||
return false
|
||||
}
|
||||
processed[absPath] = true
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
baseDir := filepath.Dir(absPath)
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(trimmed, "source") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
sourcePath := strings.TrimSpace(parts[1])
|
||||
if matchesTarget(sourcePath, target) {
|
||||
return true
|
||||
}
|
||||
|
||||
fullPath := sourcePath
|
||||
if !filepath.IsAbs(sourcePath) {
|
||||
fullPath = filepath.Join(baseDir, sourcePath)
|
||||
}
|
||||
|
||||
expanded, err := utils.ExpandPath(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if hyprlandFindInclude(expanded, target, processed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkNiriInclude(filename string) (IncludeResult, error) {
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||
if err != nil {
|
||||
return IncludeResult{}, err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(configDir, "dms", filename)
|
||||
result := IncludeResult{}
|
||||
|
||||
if _, err := os.Stat(targetPath); err == nil {
|
||||
result.Exists = true
|
||||
}
|
||||
|
||||
mainConfig := filepath.Join(configDir, "config.kdl")
|
||||
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
processed := make(map[string]bool)
|
||||
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func niriFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if processed[absPath] {
|
||||
return false
|
||||
}
|
||||
processed[absPath] = true
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
baseDir := filepath.Dir(absPath)
|
||||
content := string(data)
|
||||
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(trimmed, "include") {
|
||||
continue
|
||||
}
|
||||
|
||||
startQuote := strings.Index(trimmed, "\"")
|
||||
if startQuote == -1 {
|
||||
continue
|
||||
}
|
||||
endQuote := strings.LastIndex(trimmed, "\"")
|
||||
if endQuote <= startQuote {
|
||||
continue
|
||||
}
|
||||
|
||||
includePath := trimmed[startQuote+1 : endQuote]
|
||||
if matchesTarget(includePath, target) {
|
||||
return true
|
||||
}
|
||||
|
||||
fullPath := includePath
|
||||
if !filepath.IsAbs(includePath) {
|
||||
fullPath = filepath.Join(baseDir, includePath)
|
||||
}
|
||||
|
||||
if niriFindInclude(fullPath, target, processed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/mango")
|
||||
if err != nil {
|
||||
return IncludeResult{}, err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(configDir, "dms", filename)
|
||||
result := IncludeResult{}
|
||||
|
||||
if _, err := os.Stat(targetPath); err == nil {
|
||||
result.Exists = true
|
||||
}
|
||||
|
||||
mainConfig := filepath.Join(configDir, "config.conf")
|
||||
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||
mainConfig = filepath.Join(configDir, "mango.conf")
|
||||
}
|
||||
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
processed := make(map[string]bool)
|
||||
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func mangowcFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if processed[absPath] {
|
||||
return false
|
||||
}
|
||||
processed[absPath] = true
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
baseDir := filepath.Dir(absPath)
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(trimmed, "source") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
sourcePath := strings.TrimSpace(parts[1])
|
||||
if matchesTarget(sourcePath, target) {
|
||||
return true
|
||||
}
|
||||
|
||||
fullPath := sourcePath
|
||||
if !filepath.IsAbs(sourcePath) {
|
||||
fullPath = filepath.Join(baseDir, sourcePath)
|
||||
}
|
||||
|
||||
expanded, err := utils.ExpandPath(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if mangowcFindInclude(expanded, target, processed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func matchesTarget(path, target string) bool {
|
||||
path = strings.TrimPrefix(path, "./")
|
||||
target = strings.TrimPrefix(target, "./")
|
||||
return path == target || strings.HasSuffix(path, "/"+target)
|
||||
}
|
||||
@@ -1,126 +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("neovim", false, "Output in Neovim plugin 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")
|
||||
isNeovim, _ := cmd.Flags().GetBool("neovim")
|
||||
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 if isNeovim {
|
||||
fmt.Print(dank16.GenerateNeovimTheme(colors))
|
||||
} else {
|
||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,99 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var dlOutput string
|
||||
var dlUserAgent string
|
||||
var dlTimeout int
|
||||
var dlIPv4Only bool
|
||||
|
||||
var dlCmd = &cobra.Command{
|
||||
Use: "dl <url>",
|
||||
Short: "Download a URL to stdout or file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runDownload(args[0]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
|
||||
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
|
||||
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
|
||||
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
|
||||
}
|
||||
|
||||
func runDownload(url string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case dlUserAgent != "":
|
||||
req.Header.Set("User-Agent", dlUserAgent)
|
||||
default:
|
||||
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
transport := &http.Transport{DialContext: dialer.DialContext}
|
||||
if dlIPv4Only {
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "tcp4", addr)
|
||||
}
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if dlOutput == "" {
|
||||
_, err = io.Copy(os.Stdout, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
if dir := filepath.Dir(dlOutput); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(dlOutput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create failed: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
os.Remove(dlOutput)
|
||||
return fmt.Errorf("write failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(dlOutput)
|
||||
return nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"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/privesc"
|
||||
"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
|
||||
var isAUR bool
|
||||
if isArchPackageInstalled("dms-shell") {
|
||||
packageName = "dms-shell"
|
||||
} else if isArchPackageInstalled("dms-shell-git") {
|
||||
packageName = "dms-shell-git"
|
||||
isAUR = true
|
||||
} else if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
isAUR = true
|
||||
} else {
|
||||
fmt.Println("Info: No dms-shell package found.")
|
||||
fmt.Println("Info: Falling back to git-based update method...")
|
||||
return updateOtherDistros()
|
||||
}
|
||||
|
||||
if !isAUR {
|
||||
fmt.Printf("This will update %s using pacman.\n", packageName)
|
||||
if !confirmUpdate() {
|
||||
return errdefs.ErrUpdateCancelled
|
||||
}
|
||||
|
||||
fmt.Printf("\nRunning: pacman -S %s\n", packageName)
|
||||
if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
|
||||
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("dms successfully updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
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.SplitSeq(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, 0o755); 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)
|
||||
|
||||
if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
|
||||
return fmt.Errorf("failed to replace binary: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,265 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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() {
|
||||
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
||||
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().Bool("no-inhibiting", false, "Keep bind active when shortcuts are inhibited (allow-inhibiting=false)")
|
||||
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
||||
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
|
||||
|
||||
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("")
|
||||
if err := registry.Register(hyprlandProvider); err != nil {
|
||||
log.Warnf("Failed to register Hyprland provider: %v", err)
|
||||
}
|
||||
|
||||
mangowcProvider := providers.NewMangoWCProvider("")
|
||||
if err := registry.Register(mangowcProvider); err != nil {
|
||||
log.Warnf("Failed to register MangoWC provider: %v", err)
|
||||
}
|
||||
|
||||
configDir, _ := os.UserConfigDir()
|
||||
|
||||
if configDir != "" {
|
||||
scrollProvider := providers.NewSwayProvider(filepath.Join(configDir, "scroll"))
|
||||
if err := registry.Register(scrollProvider); err != nil {
|
||||
log.Warnf("Failed to register Scroll provider: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
miracleProvider := providers.NewMiracleProvider("")
|
||||
if err := registry.Register(miracleProvider); err != nil {
|
||||
log.Warnf("Failed to register Miracle WM provider: %v", err)
|
||||
}
|
||||
|
||||
if configDir != "" {
|
||||
swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "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(cmd *cobra.Command, _ []string) {
|
||||
providerList := keybinds.GetDefaultRegistry().List()
|
||||
asJSON, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
if asJSON {
|
||||
output, _ := json.Marshal(providerList)
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
return
|
||||
}
|
||||
|
||||
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 "miracle":
|
||||
return providers.NewMiracleProvider(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
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
||||
options["allow-inhibiting"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||
options["flags"] = v
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"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,
|
||||
}
|
||||
|
||||
var matugenCheckCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check which template apps are detected",
|
||||
Run: runMatugenCheck,
|
||||
}
|
||||
|
||||
func init() {
|
||||
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||
matugenCmd.AddCommand(matugenQueueCmd)
|
||||
matugenCmd.AddCommand(matugenCheckCmd)
|
||||
|
||||
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")
|
||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
||||
}
|
||||
|
||||
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")
|
||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||
|
||||
return matugen.Options{
|
||||
StateDir: stateDir,
|
||||
ShellDir: shellDir,
|
||||
ConfigDir: configDir,
|
||||
Kind: kind,
|
||||
Value: value,
|
||||
Mode: matugen.ColorMode(mode),
|
||||
IconTheme: iconTheme,
|
||||
MatugenType: matugenType,
|
||||
RunUserTemplates: runUserTemplates,
|
||||
StockColors: stockColors,
|
||||
SyncModeWithPortal: syncModeWithPortal,
|
||||
TerminalsAlwaysDark: terminalsAlwaysDark,
|
||||
SkipTemplates: skipTemplates,
|
||||
}
|
||||
}
|
||||
|
||||
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||
opts := buildMatugenOptions(cmd)
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case 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")
|
||||
|
||||
request := models.Request{
|
||||
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,
|
||||
"skipTemplates": opts.SkipTemplates,
|
||||
"wait": wait,
|
||||
},
|
||||
}
|
||||
|
||||
if !wait {
|
||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
||||
log.Info("Server unavailable, running synchronously")
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
fmt.Println("Theme generation queued")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
resultCh := make(chan error, 1)
|
||||
go func() {
|
||||
resp, ok := tryServerRequest(request)
|
||||
if !ok {
|
||||
log.Info("Server unavailable, running synchronously")
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
resultCh <- matugen.ErrNoChanges
|
||||
case err != nil:
|
||||
resultCh <- err
|
||||
default:
|
||||
resultCh <- nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if resp.Error != "" {
|
||||
resultCh <- fmt.Errorf("server error: %s", resp.Error)
|
||||
return
|
||||
}
|
||||
resultCh <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
fmt.Println("Theme generation completed")
|
||||
case <-ctx.Done():
|
||||
log.Fatalf("Timeout waiting for theme generation")
|
||||
}
|
||||
}
|
||||
|
||||
func runMatugenCheck(cmd *cobra.Command, args []string) {
|
||||
checks := matugen.CheckTemplates(nil)
|
||||
data, err := json.Marshal(checks)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal check results: %v", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
notifyAppName string
|
||||
notifyIcon string
|
||||
notifyFile string
|
||||
notifyTimeout int
|
||||
)
|
||||
|
||||
var notifyCmd = &cobra.Command{
|
||||
Use: "notify <summary> [body]",
|
||||
Short: "Send a desktop notification",
|
||||
Long: `Send a desktop notification with optional actions.
|
||||
|
||||
If --file is provided, the notification will have "Open" and "Open Folder" actions.
|
||||
|
||||
Examples:
|
||||
dms notify "Hello" "World"
|
||||
dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg --icon smartphone
|
||||
dms notify "Download complete" --file ~/Downloads/file.zip --app "My App"`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: runNotify,
|
||||
}
|
||||
|
||||
var genericNotifyActionCmd = &cobra.Command{
|
||||
Use: "notify-action-generic",
|
||||
Hidden: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
notify.RunActionListener(args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
notifyCmd.Flags().StringVar(¬ifyAppName, "app", "DMS", "Application name")
|
||||
notifyCmd.Flags().StringVar(¬ifyIcon, "icon", "", "Icon name or path")
|
||||
notifyCmd.Flags().StringVar(¬ifyFile, "file", "", "File path (enables Open/Open Folder actions)")
|
||||
notifyCmd.Flags().IntVar(¬ifyTimeout, "timeout", 5000, "Timeout in milliseconds")
|
||||
}
|
||||
|
||||
func runNotify(cmd *cobra.Command, args []string) {
|
||||
summary := args[0]
|
||||
body := ""
|
||||
if len(args) > 1 {
|
||||
body = args[1]
|
||||
}
|
||||
|
||||
n := notify.Notification{
|
||||
AppName: notifyAppName,
|
||||
Icon: notifyIcon,
|
||||
Summary: summary,
|
||||
Body: body,
|
||||
FilePath: notifyFile,
|
||||
Timeout: int32(notifyTimeout),
|
||||
}
|
||||
|
||||
if err := notify.Send(n); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"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) {
|
||||
// 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 strings.HasPrefix(target, "dms://") {
|
||||
// Handle DMS internal URLs (theme/plugin install, etc.)
|
||||
if detectedRequestType == "" {
|
||||
detectedRequestType = "url"
|
||||
}
|
||||
log.Infof("Detected DMS internal 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://") || strings.HasPrefix(target, "dms://")) {
|
||||
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 := sendServerRequestFireAndForget(req); err != nil {
|
||||
fmt.Println("DMS is not running. Please start DMS first.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Infof("Request sent successfully")
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var randrCmd = &cobra.Command{
|
||||
Use: "randr",
|
||||
Short: "Query output display information",
|
||||
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
|
||||
Run: runRandr,
|
||||
}
|
||||
|
||||
func init() {
|
||||
randrCmd.Flags().Bool("json", false, "Output in JSON format")
|
||||
}
|
||||
|
||||
type randrJSON struct {
|
||||
Outputs []randrOutput `json:"outputs"`
|
||||
}
|
||||
|
||||
func runRandr(cmd *cobra.Command, args []string) {
|
||||
outputs, err := queryRandr()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
jsonFlag, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
if jsonFlag {
|
||||
data, err := json.Marshal(randrJSON{Outputs: outputs})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to marshal JSON: %v", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
return
|
||||
}
|
||||
|
||||
for i, out := range outputs {
|
||||
if i > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
status := "enabled"
|
||||
if !out.Enabled {
|
||||
status = "disabled"
|
||||
}
|
||||
fmt.Printf("%s (%s)\n", out.Name, status)
|
||||
fmt.Printf(" Scale: %.4g\n", out.Scale)
|
||||
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
|
||||
if out.Refresh > 0 {
|
||||
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var customConfigPath string
|
||||
var configPath string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "dms",
|
||||
Short: "dms CLI",
|
||||
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||
}
|
||||
|
||||
func findConfig(cmd *cobra.Command, args []string) error {
|
||||
if customConfigPath != "" {
|
||||
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
|
||||
shellPath := filepath.Join(customConfigPath, "shell.qml")
|
||||
|
||||
info, statErr := os.Stat(shellPath)
|
||||
|
||||
if statErr == nil && !info.IsDir() {
|
||||
configPath = customConfigPath
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if statErr != nil {
|
||||
return fmt.Errorf("custom config path error: %w", statErr)
|
||||
}
|
||||
|
||||
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
|
||||
}
|
||||
|
||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
||||
if len(getAllDMSPIDs()) == 0 {
|
||||
os.Remove(configStateFile)
|
||||
} else {
|
||||
statePath := strings.TrimSpace(string(data))
|
||||
shellPath := filepath.Join(statePath, "shell.qml")
|
||||
|
||||
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
||||
log.Debug("Using config from active session state file: %s", statePath)
|
||||
configPath = statePath
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil
|
||||
}
|
||||
os.Remove(configStateFile)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("No custom path or active session, searching default XDG locations...")
|
||||
var err error
|
||||
configPath, err = config.LocateDMSConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Using config from: %s", configPath)
|
||||
return nil
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
ssOutputName string
|
||||
ssCursor string
|
||||
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=on # 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().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
|
||||
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
|
||||
if strings.EqualFold(ssCursor, "on") {
|
||||
config.Cursor = screenshot.CursorOn
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return clipboard.Copy(data.Bytes(), mimeType)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var setupCmd = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
PersistentPreRunE: preRunPrivileged,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetup(); err != nil {
|
||||
log.Fatalf("Error during setup: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var setupBindsCmd = &cobra.Command{
|
||||
Use: "binds",
|
||||
Short: "Deploy default keybinds config",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetupDmsConfig("binds"); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var setupLayoutCmd = &cobra.Command{
|
||||
Use: "layout",
|
||||
Short: "Deploy default layout config",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetupDmsConfig("layout"); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var setupColorsCmd = &cobra.Command{
|
||||
Use: "colors",
|
||||
Short: "Deploy default colors config",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetupDmsConfig("colors"); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var setupAlttabCmd = &cobra.Command{
|
||||
Use: "alttab",
|
||||
Short: "Deploy default alt-tab config (niri only)",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetupDmsConfig("alttab"); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var setupOutputsCmd = &cobra.Command{
|
||||
Use: "outputs",
|
||||
Short: "Deploy default outputs config",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetupDmsConfig("outputs"); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var setupCursorCmd = &cobra.Command{
|
||||
Use: "cursor",
|
||||
Short: "Deploy default cursor config",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetupDmsConfig("cursor"); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var setupWindowrulesCmd = &cobra.Command{
|
||||
Use: "windowrules",
|
||||
Short: "Deploy default window rules config",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetupDmsConfig("windowrules"); err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
type dmsConfigSpec struct {
|
||||
niriFile string
|
||||
hyprFile string
|
||||
niriContent func(terminal string) string
|
||||
hyprContent func(terminal string) string
|
||||
}
|
||||
|
||||
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
||||
"binds": {
|
||||
niriFile: "binds.kdl",
|
||||
hyprFile: "binds.conf",
|
||||
niriContent: func(t string) string {
|
||||
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||
},
|
||||
hyprContent: func(t string) string {
|
||||
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||
},
|
||||
},
|
||||
"layout": {
|
||||
niriFile: "layout.kdl",
|
||||
hyprFile: "layout.conf",
|
||||
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
|
||||
},
|
||||
"colors": {
|
||||
niriFile: "colors.kdl",
|
||||
hyprFile: "colors.conf",
|
||||
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||
hyprContent: func(_ string) string { return config.HyprColorsConfig },
|
||||
},
|
||||
"alttab": {
|
||||
niriFile: "alttab.kdl",
|
||||
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
||||
},
|
||||
"outputs": {
|
||||
niriFile: "outputs.kdl",
|
||||
hyprFile: "outputs.conf",
|
||||
niriContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return "" },
|
||||
},
|
||||
"cursor": {
|
||||
niriFile: "cursor.kdl",
|
||||
hyprFile: "cursor.conf",
|
||||
niriContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return "" },
|
||||
},
|
||||
"windowrules": {
|
||||
niriFile: "windowrules.kdl",
|
||||
hyprFile: "windowrules.conf",
|
||||
niriContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return "" },
|
||||
},
|
||||
}
|
||||
|
||||
func detectTerminal() (string, error) {
|
||||
terminals := []string{"ghostty", "foot", "kitty", "alacritty"}
|
||||
var found []string
|
||||
for _, t := range terminals {
|
||||
if utils.CommandExists(t) {
|
||||
found = append(found, t)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(found) {
|
||||
case 0:
|
||||
return "ghostty", nil
|
||||
case 1:
|
||||
return found[0], nil
|
||||
}
|
||||
|
||||
fmt.Println("Multiple terminals detected:")
|
||||
for i, t := range found {
|
||||
fmt.Printf("%d) %s\n", i+1, t)
|
||||
}
|
||||
fmt.Printf("\nChoice (1-%d): ", len(found))
|
||||
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
choice := 0
|
||||
fmt.Sscanf(response, "%d", &choice)
|
||||
if choice < 1 || choice > len(found) {
|
||||
return "", fmt.Errorf("invalid choice")
|
||||
}
|
||||
return found[choice-1], nil
|
||||
}
|
||||
|
||||
func detectCompositorForSetup() (string, error) {
|
||||
compositors := greeter.DetectCompositors()
|
||||
|
||||
switch len(compositors) {
|
||||
case 0:
|
||||
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
|
||||
case 1:
|
||||
return strings.ToLower(compositors[0]), nil
|
||||
}
|
||||
|
||||
selected, err := greeter.PromptCompositorChoice(compositors)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.ToLower(selected), nil
|
||||
}
|
||||
|
||||
func runSetupDmsConfig(name string) error {
|
||||
spec, ok := dmsConfigSpecs[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown config: %s", name)
|
||||
}
|
||||
|
||||
compositor, err := detectCompositorForSetup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var filename string
|
||||
var contentFn func(string) string
|
||||
switch compositor {
|
||||
case "niri":
|
||||
filename = spec.niriFile
|
||||
contentFn = spec.niriContent
|
||||
case "hyprland":
|
||||
filename = spec.hyprFile
|
||||
contentFn = spec.hyprContent
|
||||
default:
|
||||
return fmt.Errorf("unsupported compositor: %s", compositor)
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
return fmt.Errorf("%s is not supported for %s", name, compositor)
|
||||
}
|
||||
|
||||
var dmsDir string
|
||||
switch compositor {
|
||||
case "niri":
|
||||
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "niri", "dms")
|
||||
case "hyprland":
|
||||
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "hypr", "dms")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(dmsDir, filename)
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
return fmt.Errorf("%s already exists and is not empty: %s", name, path)
|
||||
}
|
||||
|
||||
terminal := "ghostty"
|
||||
if contentFn != nil && name == "binds" {
|
||||
terminal, err = detectTerminal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
content := contentFn(terminal)
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", filename, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Deployed %s to %s\n", name, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSetup() error {
|
||||
fmt.Println("=== DMS Configuration Setup ===")
|
||||
|
||||
ensureInputGroup()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Add user to the input group for the evdev manager for inut state tracking.
|
||||
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||
func ensureInputGroup() {
|
||||
if !utils.HasGroup("input") {
|
||||
return
|
||||
}
|
||||
currentUser := os.Getenv("USER")
|
||||
if currentUser == "" {
|
||||
currentUser = os.Getenv("LOGNAME")
|
||||
}
|
||||
if currentUser == "" {
|
||||
return
|
||||
}
|
||||
out, err := execGroups(currentUser)
|
||||
if err == nil && strings.Contains(out, "input") {
|
||||
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
|
||||
return
|
||||
}
|
||||
fmt.Println("Adding user to input group for Caps Lock OSD support...")
|
||||
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
|
||||
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
|
||||
} else {
|
||||
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
func execGroups(user string) (string, error) {
|
||||
out, err := exec.Command("groups", user).Output()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules/providers"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var windowrulesCmd = &cobra.Command{
|
||||
Use: "windowrules",
|
||||
Short: "Manage window rules",
|
||||
}
|
||||
|
||||
var windowrulesListCmd = &cobra.Command{
|
||||
Use: "list [compositor]",
|
||||
Short: "List all window rules",
|
||||
Long: "List all window rules from compositor config file. Returns JSON with rules and DMS status.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesList,
|
||||
}
|
||||
|
||||
var windowrulesAddCmd = &cobra.Command{
|
||||
Use: "add <compositor> '<json>'",
|
||||
Short: "Add a window rule to DMS file",
|
||||
Long: "Add a new window rule to the DMS-managed rules file.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
// ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesAdd,
|
||||
}
|
||||
|
||||
var windowrulesUpdateCmd = &cobra.Command{
|
||||
Use: "update <compositor> <id> '<json>'",
|
||||
Short: "Update a window rule in DMS file",
|
||||
Long: "Update an existing window rule in the DMS-managed rules file.",
|
||||
Args: cobra.ExactArgs(3),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesUpdate,
|
||||
}
|
||||
|
||||
var windowrulesRemoveCmd = &cobra.Command{
|
||||
Use: "remove <compositor> <id>",
|
||||
Short: "Remove a window rule from DMS file",
|
||||
Long: "Remove a window rule from the DMS-managed rules file.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesRemove,
|
||||
}
|
||||
|
||||
var windowrulesReorderCmd = &cobra.Command{
|
||||
Use: "reorder <compositor> '<json-array-of-ids>'",
|
||||
Short: "Reorder window rules in DMS file",
|
||||
Long: "Reorder window rules by providing a JSON array of rule IDs in the desired order.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesReorder,
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(windowrulesCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesListCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesAddCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesUpdateCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesRemoveCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesReorderCmd)
|
||||
}
|
||||
|
||||
type WindowRulesListResult struct {
|
||||
Rules []windowrules.WindowRule `json:"rules"`
|
||||
DMSStatus *windowrules.DMSRulesStatus `json:"dmsStatus,omitempty"`
|
||||
}
|
||||
|
||||
type WindowRuleWriteResult struct {
|
||||
Success bool `json:"success"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func getCompositor(args []string) string {
|
||||
if len(args) > 0 {
|
||||
return strings.ToLower(args[0])
|
||||
}
|
||||
if os.Getenv("NIRI_SOCKET") != "" {
|
||||
return "niri"
|
||||
}
|
||||
// if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||
// return "hyprland"
|
||||
// }
|
||||
return ""
|
||||
}
|
||||
|
||||
func writeRuleError(errMsg string) {
|
||||
result := WindowRuleWriteResult{Success: false, Error: errMsg}
|
||||
output, _ := json.Marshal(result)
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func writeRuleSuccess(id, path string) {
|
||||
result := WindowRuleWriteResult{Success: true, ID: id, Path: path}
|
||||
output, _ := json.Marshal(result)
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
}
|
||||
|
||||
func runWindowrulesList(cmd *cobra.Command, args []string) {
|
||||
compositor := getCompositor(args)
|
||||
if compositor == "" {
|
||||
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
|
||||
}
|
||||
|
||||
var result WindowRulesListResult
|
||||
|
||||
switch compositor {
|
||||
case "niri":
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to expand niri config path: %v", err)
|
||||
}
|
||||
|
||||
parseResult, err := providers.ParseNiriWindowRules(configDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse niri window rules: %v", err)
|
||||
}
|
||||
|
||||
allRules := providers.ConvertNiriRulesToWindowRules(parseResult.Rules)
|
||||
|
||||
provider := providers.NewNiriWritableProvider(configDir)
|
||||
dmsRulesPath := provider.GetOverridePath()
|
||||
dmsRules, _ := provider.LoadDMSRules()
|
||||
|
||||
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||
for i, dr := range dmsRules {
|
||||
dmsRuleMap[i] = dr
|
||||
}
|
||||
|
||||
dmsIdx := 0
|
||||
for i, r := range allRules {
|
||||
if r.Source == dmsRulesPath {
|
||||
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||
allRules[i].ID = dmr.ID
|
||||
allRules[i].Name = dmr.Name
|
||||
}
|
||||
dmsIdx++
|
||||
}
|
||||
}
|
||||
|
||||
result.Rules = allRules
|
||||
result.DMSStatus = parseResult.DMSStatus
|
||||
|
||||
case "hyprland":
|
||||
log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
||||
}
|
||||
|
||||
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse hyprland window rules: %v", err)
|
||||
}
|
||||
|
||||
allRules := providers.ConvertHyprlandRulesToWindowRules(parseResult.Rules)
|
||||
|
||||
provider := providers.NewHyprlandWritableProvider(configDir)
|
||||
dmsRulesPath := provider.GetOverridePath()
|
||||
dmsRules, _ := provider.LoadDMSRules()
|
||||
|
||||
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||
for i, dr := range dmsRules {
|
||||
dmsRuleMap[i] = dr
|
||||
}
|
||||
|
||||
dmsIdx := 0
|
||||
for i, r := range allRules {
|
||||
if r.Source == dmsRulesPath {
|
||||
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||
allRules[i].ID = dmr.ID
|
||||
allRules[i].Name = dmr.Name
|
||||
}
|
||||
dmsIdx++
|
||||
}
|
||||
}
|
||||
|
||||
result.Rules = allRules
|
||||
result.DMSStatus = parseResult.DMSStatus
|
||||
|
||||
default:
|
||||
log.Fatalf("Unknown compositor: %s", compositor)
|
||||
}
|
||||
|
||||
output, _ := json.Marshal(result)
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
}
|
||||
|
||||
func runWindowrulesAdd(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
ruleJSON := args[1]
|
||||
|
||||
var rule windowrules.WindowRule
|
||||
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
|
||||
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
|
||||
}
|
||||
|
||||
if rule.ID == "" {
|
||||
rule.ID = generateRuleID()
|
||||
}
|
||||
rule.Enabled = true
|
||||
|
||||
provider := getWindowRulesProvider(compositor)
|
||||
if provider == nil {
|
||||
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||
}
|
||||
|
||||
if err := provider.SetRule(rule); err != nil {
|
||||
writeRuleError(err.Error())
|
||||
}
|
||||
|
||||
writeRuleSuccess(rule.ID, provider.GetOverridePath())
|
||||
}
|
||||
|
||||
func runWindowrulesUpdate(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
ruleID := args[1]
|
||||
ruleJSON := args[2]
|
||||
|
||||
var rule windowrules.WindowRule
|
||||
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
|
||||
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
|
||||
}
|
||||
|
||||
rule.ID = ruleID
|
||||
|
||||
provider := getWindowRulesProvider(compositor)
|
||||
if provider == nil {
|
||||
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||
}
|
||||
|
||||
if err := provider.SetRule(rule); err != nil {
|
||||
writeRuleError(err.Error())
|
||||
}
|
||||
|
||||
writeRuleSuccess(rule.ID, provider.GetOverridePath())
|
||||
}
|
||||
|
||||
func runWindowrulesRemove(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
ruleID := args[1]
|
||||
|
||||
provider := getWindowRulesProvider(compositor)
|
||||
if provider == nil {
|
||||
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||
}
|
||||
|
||||
if err := provider.RemoveRule(ruleID); err != nil {
|
||||
writeRuleError(err.Error())
|
||||
}
|
||||
|
||||
writeRuleSuccess(ruleID, provider.GetOverridePath())
|
||||
}
|
||||
|
||||
func runWindowrulesReorder(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
idsJSON := args[1]
|
||||
|
||||
var ids []string
|
||||
if err := json.Unmarshal([]byte(idsJSON), &ids); err != nil {
|
||||
writeRuleError(fmt.Sprintf("Invalid JSON array: %v", err))
|
||||
}
|
||||
|
||||
provider := getWindowRulesProvider(compositor)
|
||||
if provider == nil {
|
||||
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||
}
|
||||
|
||||
if err := provider.ReorderRules(ids); err != nil {
|
||||
writeRuleError(err.Error())
|
||||
}
|
||||
|
||||
writeRuleSuccess("", provider.GetOverridePath())
|
||||
}
|
||||
|
||||
func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
|
||||
switch compositor {
|
||||
case "niri":
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return providers.NewNiriWritableProvider(configDir)
|
||||
case "hyprland":
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return providers.NewHyprlandWritableProvider(configDir)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func generateRuleID() string {
|
||||
return fmt.Sprintf("wr_%d", time.Now().UnixNano())
|
||||
}
|
||||
@@ -1,339 +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 := min(e.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 := min(e.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
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
||||
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
||||
)
|
||||
|
||||
var (
|
||||
immutablePolicyOnce sync.Once
|
||||
immutablePolicy immutableCommandPolicy
|
||||
immutablePolicyErr error
|
||||
)
|
||||
|
||||
//go:embed assets/cli-policy.default.json
|
||||
var defaultCLIPolicyJSON []byte
|
||||
|
||||
type immutableCommandPolicy struct {
|
||||
ImmutableSystem bool
|
||||
ImmutableReason string
|
||||
BlockedCommands []string
|
||||
Message string
|
||||
}
|
||||
|
||||
type cliPolicyFile struct {
|
||||
PolicyVersion int `json:"policy_version"`
|
||||
ImmutableSystem *bool `json:"immutable_system"`
|
||||
BlockedCommands *[]string `json:"blocked_commands"`
|
||||
Message *string `json:"message"`
|
||||
}
|
||||
|
||||
func normalizeCommandSpec(raw string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||
normalized = strings.TrimPrefix(normalized, "dms ")
|
||||
return strings.Join(strings.Fields(normalized), " ")
|
||||
}
|
||||
|
||||
func normalizeBlockedCommands(raw []string) []string {
|
||||
normalized := make([]string, 0, len(raw))
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, cmd := range raw {
|
||||
spec := normalizeCommandSpec(cmd)
|
||||
if spec == "" || seen[spec] {
|
||||
continue
|
||||
}
|
||||
seen[spec] = true
|
||||
normalized = append(normalized, spec)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
||||
normalizedPath := normalizeCommandSpec(commandPath)
|
||||
if normalizedPath == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, entry := range blocked {
|
||||
spec := normalizeCommandSpec(entry)
|
||||
if spec == "" {
|
||||
continue
|
||||
}
|
||||
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
||||
}
|
||||
|
||||
var policy cliPolicyFile
|
||||
if err := json.Unmarshal(data, &policy); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
||||
policyFile, err := loadPolicyFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if policyFile == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if policyFile.ImmutableSystem != nil {
|
||||
base.ImmutableSystem = *policyFile.ImmutableSystem
|
||||
}
|
||||
if policyFile.BlockedCommands != nil {
|
||||
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||
}
|
||||
if policyFile.Message != nil {
|
||||
msg := strings.TrimSpace(*policyFile.Message)
|
||||
if msg != "" {
|
||||
base.Message = msg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readOSReleaseMap(path string) map[string]string {
|
||||
values := make(map[string]string)
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return values
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||
values[key] = strings.ToLower(value)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func hasAnyToken(text string, tokens ...string) bool {
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
for _, token := range tokens {
|
||||
if strings.Contains(text, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func detectImmutableSystem() (bool, string) {
|
||||
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
||||
return true, "/run/ostree-booted is present"
|
||||
}
|
||||
|
||||
osRelease := readOSReleaseMap("/etc/os-release")
|
||||
if len(osRelease) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
id := osRelease["ID"]
|
||||
idLike := osRelease["ID_LIKE"]
|
||||
variantID := osRelease["VARIANT_ID"]
|
||||
name := osRelease["NAME"]
|
||||
prettyName := osRelease["PRETTY_NAME"]
|
||||
|
||||
immutableIDs := map[string]bool{
|
||||
"bluefin": true,
|
||||
"bazzite": true,
|
||||
"silverblue": true,
|
||||
"kinoite": true,
|
||||
"sericea": true,
|
||||
"onyx": true,
|
||||
"aurora": true,
|
||||
"fedora-iot": true,
|
||||
"fedora-coreos": true,
|
||||
}
|
||||
if immutableIDs[id] {
|
||||
return true, "os-release ID=" + id
|
||||
}
|
||||
|
||||
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
||||
if hasAnyToken(variantID, markers...) {
|
||||
return true, "os-release VARIANT_ID=" + variantID
|
||||
}
|
||||
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
||||
return true, "os-release ID_LIKE=" + idLike
|
||||
}
|
||||
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
||||
return true, "os-release identifies an atomic/ostree variant"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||
immutablePolicyOnce.Do(func() {
|
||||
detectedImmutable, reason := detectImmutableSystem()
|
||||
immutablePolicy = immutableCommandPolicy{
|
||||
ImmutableSystem: detectedImmutable,
|
||||
ImmutableReason: reason,
|
||||
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
|
||||
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||
}
|
||||
|
||||
var defaultPolicy cliPolicyFile
|
||||
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
||||
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
||||
return
|
||||
}
|
||||
if defaultPolicy.BlockedCommands != nil {
|
||||
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
||||
}
|
||||
if defaultPolicy.Message != nil {
|
||||
msg := strings.TrimSpace(*defaultPolicy.Message)
|
||||
if msg != "" {
|
||||
immutablePolicy.Message = msg
|
||||
}
|
||||
}
|
||||
|
||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
||||
immutablePolicyErr = err
|
||||
return
|
||||
}
|
||||
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
||||
immutablePolicyErr = err
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if immutablePolicyErr != nil {
|
||||
return nil, immutablePolicyErr
|
||||
}
|
||||
return &immutablePolicy, nil
|
||||
}
|
||||
|
||||
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||
policy, err := getImmutablePolicy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !policy.ImmutableSystem {
|
||||
return nil
|
||||
}
|
||||
|
||||
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
||||
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
||||
return nil
|
||||
}
|
||||
|
||||
reason := ""
|
||||
if policy.ImmutableReason != "" {
|
||||
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||
}
|
||||
|
||||
// preRunPrivileged combines the immutable-system check with a privesc tool
|
||||
// selection prompt (shown only when multiple tools are available and the
|
||||
// $DMS_PRIVESC env var isn't set).
|
||||
func preRunPrivileged(cmd *cobra.Command, args []string) error {
|
||||
if err := requireMutableSystemCommand(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"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")
|
||||
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
updateCmd.AddCommand(updateCheckCmd)
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
}
|
||||
|
||||
func main() {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"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")
|
||||
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
}
|
||||
|
||||
func main() {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type randrOutput struct {
|
||||
Name string `json:"name"`
|
||||
Scale float64 `json:"scale"`
|
||||
Width int32 `json:"width"`
|
||||
Height int32 `json:"height"`
|
||||
Refresh int32 `json:"refresh"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type randrHead struct {
|
||||
name string
|
||||
enabled bool
|
||||
scale float64
|
||||
currentModeID uint32
|
||||
modeIDs []uint32
|
||||
}
|
||||
|
||||
type randrMode struct {
|
||||
width int32
|
||||
height int32
|
||||
refresh int32
|
||||
}
|
||||
|
||||
type randrClient struct {
|
||||
display *wlclient.Display
|
||||
ctx *wlclient.Context
|
||||
manager *wlr_output_management.ZwlrOutputManagerV1
|
||||
heads map[uint32]*randrHead
|
||||
modes map[uint32]*randrMode
|
||||
done bool
|
||||
err error
|
||||
}
|
||||
|
||||
func queryRandr() ([]randrOutput, error) {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
|
||||
}
|
||||
|
||||
c := &randrClient{
|
||||
display: display,
|
||||
ctx: display.Context(),
|
||||
heads: make(map[uint32]*randrHead),
|
||||
modes: make(map[uint32]*randrMode),
|
||||
}
|
||||
defer c.ctx.Close()
|
||||
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get registry: %w", err)
|
||||
}
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
|
||||
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
|
||||
version := min(e.Version, 4)
|
||||
|
||||
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||
c.handleHead(e)
|
||||
})
|
||||
|
||||
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
|
||||
c.done = true
|
||||
})
|
||||
|
||||
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
|
||||
c.manager = mgr
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// First roundtrip: discover globals and bind manager
|
||||
syncCallback, err := display.Sync()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sync display: %w", err)
|
||||
}
|
||||
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
||||
if c.manager == nil {
|
||||
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
|
||||
c.done = true
|
||||
}
|
||||
// Otherwise wait for manager's DoneHandler
|
||||
})
|
||||
|
||||
for !c.done {
|
||||
if err := c.ctx.Dispatch(); err != nil {
|
||||
return nil, fmt.Errorf("dispatch error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.err != nil {
|
||||
return nil, c.err
|
||||
}
|
||||
|
||||
return c.buildOutputs(), nil
|
||||
}
|
||||
|
||||
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||
handle := e.Head
|
||||
headID := handle.ID()
|
||||
|
||||
head := &randrHead{
|
||||
modeIDs: make([]uint32, 0),
|
||||
}
|
||||
c.heads[headID] = head
|
||||
|
||||
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
||||
head.name = e.Name
|
||||
})
|
||||
|
||||
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
|
||||
head.enabled = e.Enabled != 0
|
||||
})
|
||||
|
||||
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||
head.scale = e.Scale
|
||||
})
|
||||
|
||||
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
|
||||
head.currentModeID = e.Mode.ID()
|
||||
})
|
||||
|
||||
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
|
||||
modeHandle := e.Mode
|
||||
modeID := modeHandle.ID()
|
||||
|
||||
head.modeIDs = append(head.modeIDs, modeID)
|
||||
|
||||
mode := &randrMode{}
|
||||
c.modes[modeID] = mode
|
||||
|
||||
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
|
||||
mode.width = e.Width
|
||||
mode.height = e.Height
|
||||
})
|
||||
|
||||
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
|
||||
mode.refresh = e.Refresh
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (c *randrClient) buildOutputs() []randrOutput {
|
||||
outputs := make([]randrOutput, 0, len(c.heads))
|
||||
|
||||
for _, head := range c.heads {
|
||||
out := randrOutput{
|
||||
Name: head.name,
|
||||
Scale: head.scale,
|
||||
Enabled: head.enabled,
|
||||
}
|
||||
|
||||
if mode, ok := c.modes[head.currentModeID]; ok {
|
||||
out.Width = mode.width
|
||||
out.Height = mode.height
|
||||
out.Refresh = mode.refresh
|
||||
}
|
||||
|
||||
outputs = append(outputs, out)
|
||||
}
|
||||
|
||||
return outputs
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
|
||||
func sendServerRequest(req models.Request) (*models.Response[any], error) {
|
||||
socketPath := getServerSocketPath()
|
||||
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to server (is it running?): %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Scan() // discard initial capabilities message
|
||||
|
||||
reqData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(reqData); err != nil {
|
||||
return nil, fmt.Errorf("failed to write request: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
||||
return nil, fmt.Errorf("failed to write newline: %w", err)
|
||||
}
|
||||
|
||||
if !scanner.Scan() {
|
||||
return nil, fmt.Errorf("failed to read response")
|
||||
}
|
||||
|
||||
var resp models.Response[any]
|
||||
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// sendServerRequestFireAndForget sends a request without waiting for a response.
|
||||
// Useful for commands that trigger UI or async operations.
|
||||
func sendServerRequestFireAndForget(req models.Request) error {
|
||||
socketPath := getServerSocketPath()
|
||||
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to server (is it running?): %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Scan() // discard initial capabilities message
|
||||
|
||||
reqData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(reqData); err != nil {
|
||||
return fmt.Errorf("failed to write request: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
||||
return fmt.Errorf("failed to write newline: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryServerRequest attempts to send a request but returns false if server unavailable.
|
||||
// Does not log errors - caller can decide what to do on failure.
|
||||
func tryServerRequest(req models.Request) (*models.Response[any], bool) {
|
||||
resp, err := sendServerRequest(req)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func getServerSocketPath() string {
|
||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if runtimeDir == "" {
|
||||
runtimeDir = os.TempDir()
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(runtimeDir)
|
||||
if err != nil {
|
||||
return filepath.Join(runtimeDir, "danklinux.sock")
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if name == "danklinux.sock" {
|
||||
return filepath.Join(runtimeDir, name)
|
||||
}
|
||||
if len(name) > 10 && name[:10] == "danklinux-" && filepath.Ext(name) == ".sock" {
|
||||
return filepath.Join(runtimeDir, name)
|
||||
}
|
||||
}
|
||||
|
||||
return server.GetSocketPath()
|
||||
}
|
||||
@@ -1,733 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||
)
|
||||
|
||||
type ipcTargets map[string]map[string][]string
|
||||
|
||||
// getProcessExitCode returns the exit code from a ProcessState.
|
||||
// For normal exits, returns the exit code directly.
|
||||
// For signal termination, returns 128 + signal number (Unix convention).
|
||||
func getProcessExitCode(state *os.ProcessState) int {
|
||||
if state == nil {
|
||||
return 1
|
||||
}
|
||||
if code := state.ExitCode(); code != -1 {
|
||||
return code
|
||||
}
|
||||
// Process was killed by signal - extract signal number
|
||||
if status, ok := state.Sys().(syscall.WaitStatus); ok {
|
||||
if status.Signaled() {
|
||||
return 128 + int(status.Signal())
|
||||
}
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
var isSessionManaged bool
|
||||
|
||||
func execDetachedRestart(targetPID int) {
|
||||
selfPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
cmd.Start()
|
||||
}
|
||||
|
||||
func runDetachedRestart(targetPIDStr string) {
|
||||
targetPID, err := strconv.Atoi(targetPIDStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
proc, err := os.FindProcess(targetPID)
|
||||
if err == nil {
|
||||
proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
killShell()
|
||||
runShellDaemon(false)
|
||||
}
|
||||
|
||||
func getRuntimeDir() string {
|
||||
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
|
||||
return runtime
|
||||
}
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
func 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 os.Getenv("QT_LOGGING_RULES") == "" {
|
||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||
}
|
||||
}
|
||||
|
||||
if isSessionManaged && hasSystemdRun() {
|
||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
||||
if !strings.HasPrefix(configPath, homeDir) {
|
||||
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
||||
}
|
||||
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||
}
|
||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||
}
|
||||
|
||||
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:
|
||||
if sig == syscall.SIGUSR1 {
|
||||
if isSessionManaged {
|
||||
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
||||
cancel()
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
os.Remove(socketPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||
execDetachedRestart(os.Getpid())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
||||
select {
|
||||
case <-errChan:
|
||||
cancel()
|
||||
os.Remove(socketPath)
|
||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
|
||||
log.Infof("\nReceived signal %v, shutting down...", sig)
|
||||
cancel()
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
os.Remove(socketPath)
|
||||
return
|
||||
|
||||
case err := <-errChan:
|
||||
log.Error(err)
|
||||
cancel()
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
os.Remove(socketPath)
|
||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 := slices.Contains(os.Args, "--daemon-child")
|
||||
|
||||
if !isDaemonChild {
|
||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||
|
||||
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("Error starting daemon: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
socketPath := server.GetSocketPath()
|
||||
|
||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
||||
log.Warnf("Failed to write config state file: %v", err)
|
||||
}
|
||||
defer os.Remove(configStateFile)
|
||||
|
||||
errChan := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("server panic: %v", r)
|
||||
}
|
||||
}()
|
||||
server.CLIVersion = Version
|
||||
if err := server.Start(false); err != nil {
|
||||
errChan <- fmt.Errorf("server error: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Infof("Spawning quickshell with -p %s", configPath)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||
if os.Getenv("QT_LOGGING_RULES") == "" {
|
||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||
}
|
||||
}
|
||||
|
||||
if isSessionManaged && hasSystemdRun() {
|
||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
||||
if !strings.HasPrefix(configPath, homeDir) {
|
||||
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
||||
}
|
||||
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||
}
|
||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||
}
|
||||
|
||||
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:
|
||||
if sig == syscall.SIGUSR1 {
|
||||
if isSessionManaged {
|
||||
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
||||
cancel()
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
os.Remove(socketPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||
execDetachedRestart(os.Getpid())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
||||
select {
|
||||
case <-errChan:
|
||||
cancel()
|
||||
os.Remove(socketPath)
|
||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
|
||||
cancel()
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
os.Remove(socketPath)
|
||||
return
|
||||
|
||||
case <-errChan:
|
||||
cancel()
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
os.Remove(socketPath)
|
||||
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
||||
out, err := exec.Command("qs", "ipc", "--help").Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(out), "--any-display")
|
||||
})
|
||||
|
||||
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||
targets := make(ipcTargets)
|
||||
var currentTarget string
|
||||
for line := range strings.SplitSeq(output, "\n") {
|
||||
if after, ok := strings.CutPrefix(line, "target "); ok {
|
||||
currentTarget = strings.TrimSpace(after)
|
||||
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{"ipc"}
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", configPath, "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 getFirstDMSPID() (int, bool) {
|
||||
dir := getRuntimeDir()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if proc.Signal(syscall.Signal(0)) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return pid, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func runShellIPCCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
printIPCHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if args[0] != "call" {
|
||||
args = append([]string{"call"}, args...)
|
||||
}
|
||||
|
||||
cmdArgs := []string{"ipc"}
|
||||
|
||||
switch pid, ok := getFirstDMSPID(); {
|
||||
case ok:
|
||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||
default:
|
||||
if err := findConfig(nil, nil); err != nil {
|
||||
log.Fatalf("Error finding config: %v", err)
|
||||
}
|
||||
// ! TODO - remove check when QS 0.3 is released
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, 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)
|
||||
}
|
||||
}
|
||||
|
||||
func printIPCHelp() {
|
||||
fmt.Println("Usage: dms ipc <target> <function> [args...]")
|
||||
fmt.Println()
|
||||
|
||||
cmdArgs := []string{"ipc"}
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||
cmd := exec.Command("qs", cmdArgs...)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
|
||||
return
|
||||
}
|
||||
|
||||
targets := parseTargetsFromIPCShowOutput(string(output))
|
||||
if len(targets) == 0 {
|
||||
fmt.Println("No IPC targets available")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Targets:")
|
||||
|
||||
targetNames := make([]string, 0, len(targets))
|
||||
for name := range targets {
|
||||
targetNames = append(targetNames, name)
|
||||
}
|
||||
slices.Sort(targetNames)
|
||||
|
||||
for _, targetName := range targetNames {
|
||||
funcs := targets[targetName]
|
||||
funcNames := make([]string, 0, len(funcs))
|
||||
for fn := range funcs {
|
||||
funcNames = append(funcNames, fn)
|
||||
}
|
||||
slices.Sort(funcNames)
|
||||
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
|
||||
}
|
||||
}
|
||||
@@ -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}}
|
||||
`
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
||||
// safe to run as root (e.g. shell completion, help).
|
||||
func isReadOnlyCommand(args []string) bool {
|
||||
for _, arg := range args[1:] {
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
continue
|
||||
}
|
||||
switch arg {
|
||||
case "completion", "help", "__complete":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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 := slices.Contains(knownStates, stateStr)
|
||||
|
||||
if !isKnownState {
|
||||
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
||||
}
|
||||
}
|
||||
|
||||
shouldDisable := false
|
||||
switch stateStr {
|
||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||
shouldDisable = true
|
||||
case "disabled", "masked", "masked-runtime", "not-found":
|
||||
shouldDisable = false
|
||||
default:
|
||||
shouldDisable = true
|
||||
}
|
||||
|
||||
return stateStr, shouldDisable, nil
|
||||
}
|
||||
|
||||
func getSystemdServiceState(serviceName string) (*systemdServiceState, error) {
|
||||
state := &systemdServiceState{
|
||||
Name: serviceName,
|
||||
Exists: false,
|
||||
}
|
||||
|
||||
enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check enabled state: %w", err)
|
||||
}
|
||||
|
||||
state.EnabledState = enabledState
|
||||
state.NeedsDisable = needsDisable
|
||||
|
||||
if enabledState == "not-found" {
|
||||
state.Exists = false
|
||||
return state, nil
|
||||
}
|
||||
|
||||
state.Exists = true
|
||||
return state, nil
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
module github.com/AvengeMedia/DankMaterialShell/core
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
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-20260121213736-8b7053306ca6
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
|
||||
golang.org/x/image v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // 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-20260209124918-37866f83c2d3 // 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.6.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.48.0 // indirect
|
||||
golang.org/x/net v0.50.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.4.2 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // 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-20260216160506-e6a3f881772f
|
||||
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.41.0
|
||||
golang.org/x/text v0.34.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
// v0.0.1 tag is missing a LICENSE file; master has it.
|
||||
// See: https://github.com/mattn/go-localereader/issues/2
|
||||
replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75
|
||||
-179
@@ -1,179 +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/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
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 v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||
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.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
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.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
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.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
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/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
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-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
|
||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
|
||||
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.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
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.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
github.com/kevinburke/ssh_config v1.6.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.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.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-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
|
||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/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.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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=$(awk '{print $1}' expected.sha256)
|
||||
|
||||
# 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"
|
||||
@@ -1,35 +0,0 @@
|
||||
package blur
|
||||
|
||||
import (
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
||||
|
||||
func ProbeSupport() (bool, error) {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer display.Context().Close()
|
||||
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
found := false
|
||||
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case extBackgroundEffectInterface:
|
||||
found = true
|
||||
}
|
||||
})
|
||||
|
||||
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return found, nil
|
||||
}
|
||||
@@ -1,630 +0,0 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||
const envMime = "_DMS_CLIPBOARD_MIME"
|
||||
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||
|
||||
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||
func MaybeServeAndExit() {
|
||||
if os.Getenv(envServe) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
mimeType := os.Getenv(envMime)
|
||||
pasteOnce := os.Getenv(envPasteOnce) == "1"
|
||||
cachePath := os.Getenv(envCacheFile)
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case cachePath != "":
|
||||
data, err = os.ReadFile(cachePath)
|
||||
os.Remove(cachePath)
|
||||
default:
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func Copy(data []byte, mimeType string) error {
|
||||
return copyForkCached(data, mimeType, false)
|
||||
}
|
||||
|
||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||
if foreground {
|
||||
return serveClipboard(data, mimeType, pasteOnce)
|
||||
}
|
||||
return copyForkCached(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||
if foreground {
|
||||
buf, err := io.ReadAll(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read source: %w", err)
|
||||
}
|
||||
return serveClipboard(buf, mimeType, pasteOnce)
|
||||
}
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||
cmd := exec.Command(os.Args[0])
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
cmd.Env = append(os.Environ(),
|
||||
envServe+"=1",
|
||||
envMime+"="+mimeType,
|
||||
)
|
||||
if pasteOnce {
|
||||
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||
}
|
||||
cmd.Env = append(cmd.Env, extra...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func waitReady(cmd *exec.Cmd) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
||||
cacheFile, err := createClipboardCacheFile()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create cache file: %w", err)
|
||||
}
|
||||
cachePath := cacheFile.Name()
|
||||
|
||||
if _, err := cacheFile.Write(data); err != nil {
|
||||
cacheFile.Close()
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("write cache file: %w", err)
|
||||
}
|
||||
if err := cacheFile.Close(); err != nil {
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("close cache file: %w", err)
|
||||
}
|
||||
|
||||
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
||||
cmd.Stdin = nil
|
||||
if err := waitReady(cmd); err != nil {
|
||||
os.Remove(cachePath)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
cmd := newForkCmd(mimeType, pasteOnce)
|
||||
|
||||
switch src := data.(type) {
|
||||
case *os.File:
|
||||
cmd.Stdin = src
|
||||
return waitReady(cmd)
|
||||
|
||||
default:
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(stdin, data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func signalReady() {
|
||||
if os.Getenv(envServe) == "" {
|
||||
return
|
||||
}
|
||||
os.Stdout.Write([]byte{1})
|
||||
}
|
||||
|
||||
func createClipboardCacheFile() (*os.File, error) {
|
||||
preferredDirs := []string{}
|
||||
|
||||
if cacheDir, err := os.UserCacheDir(); err == nil {
|
||||
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
|
||||
}
|
||||
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
|
||||
|
||||
for _, dir := range preferredDirs {
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
continue
|
||||
}
|
||||
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
|
||||
if err == nil {
|
||||
return cachedData, nil
|
||||
}
|
||||
}
|
||||
return os.CreateTemp("", "dms-clipboard-*")
|
||||
}
|
||||
|
||||
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
ctx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
source, err := dataControlMgr.CreateDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create data source: %w", err)
|
||||
}
|
||||
|
||||
if err := source.Offer(mimeType); err != nil {
|
||||
return fmt.Errorf("offer mime type: %w", err)
|
||||
}
|
||||
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
|
||||
if err := source.Offer("text/plain"); err != nil {
|
||||
return fmt.Errorf("offer text/plain: %w", err)
|
||||
}
|
||||
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
|
||||
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
|
||||
}
|
||||
if err := source.Offer("UTF8_STRING"); err != nil {
|
||||
return fmt.Errorf("offer UTF8_STRING: %w", err)
|
||||
}
|
||||
if err := source.Offer("STRING"); err != nil {
|
||||
return fmt.Errorf("offer STRING: %w", err)
|
||||
}
|
||||
if err := source.Offer("TEXT"); err != nil {
|
||||
return fmt.Errorf("offer TEXT: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
_, _ = file.Write(data)
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||
close(cancelled)
|
||||
})
|
||||
|
||||
if err := device.SetSelection(source); err != nil {
|
||||
return fmt.Errorf("set selection: %w", err)
|
||||
}
|
||||
|
||||
display.Roundtrip()
|
||||
signalReady()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CopyText(text string) error {
|
||||
return Copy([]byte(text), "text/plain;charset=utf-8")
|
||||
}
|
||||
|
||||
func Paste() ([]byte, string, error) {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
ctx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return nil, "", fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
||||
|
||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
offerMimeTypes[e.Id] = nil
|
||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
||||
})
|
||||
})
|
||||
|
||||
var selectionOffer *ext_data_control.ExtDataControlOfferV1
|
||||
gotSelection := false
|
||||
|
||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
||||
selectionOffer = e.Id
|
||||
gotSelection = true
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if !gotSelection || selectionOffer == nil {
|
||||
return nil, "", fmt.Errorf("no clipboard data")
|
||||
}
|
||||
|
||||
mimeTypes := offerMimeTypes[selectionOffer]
|
||||
selectedMime := selectPreferredMimeType(mimeTypes)
|
||||
if selectedMime == "" {
|
||||
return nil, "", fmt.Errorf("no supported mime type")
|
||||
}
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("create pipe: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil {
|
||||
w.Close()
|
||||
return nil, "", fmt.Errorf("receive: %w", err)
|
||||
}
|
||||
w.Close()
|
||||
|
||||
display.Roundtrip()
|
||||
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("read: %w", err)
|
||||
}
|
||||
|
||||
return data, selectedMime, nil
|
||||
}
|
||||
|
||||
func PasteText() (string, error) {
|
||||
data, _, err := Paste()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func selectPreferredMimeType(mimes []string) string {
|
||||
preferred := []string{
|
||||
"text/plain;charset=utf-8",
|
||||
"text/plain",
|
||||
"UTF8_STRING",
|
||||
"STRING",
|
||||
"TEXT",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
}
|
||||
|
||||
for _, pref := range preferred {
|
||||
for _, mime := range mimes {
|
||||
if mime == pref {
|
||||
return mime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mimes) > 0 {
|
||||
return mimes[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func IsImageMimeType(mime string) bool {
|
||||
return len(mime) > 6 && mime[:6] == "image/"
|
||||
}
|
||||
|
||||
type Offer struct {
|
||||
MimeType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
||||
if !foreground {
|
||||
return copyMultiFork(offers, pasteOnce)
|
||||
}
|
||||
return copyMultiServe(offers, pasteOnce)
|
||||
}
|
||||
|
||||
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
||||
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
}
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
for _, offer := range offers {
|
||||
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
||||
if _, err := stdin.Write(offer.Data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write offer data: %w", err)
|
||||
}
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
ctx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
source, err := dataControlMgr.CreateDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create data source: %w", err)
|
||||
}
|
||||
|
||||
offerMap := make(map[string][]byte)
|
||||
for _, offer := range offers {
|
||||
if err := source.Offer(offer.MimeType); err != nil {
|
||||
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
||||
}
|
||||
offerMap[offer.MimeType] = offer.Data
|
||||
}
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
|
||||
if data, ok := offerMap[e.MimeType]; ok {
|
||||
_, _ = file.Write(data)
|
||||
}
|
||||
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||
close(cancelled)
|
||||
})
|
||||
|
||||
if err := device.SetSelection(source); err != nil {
|
||||
return fmt.Errorf("set selection: %w", err)
|
||||
}
|
||||
|
||||
display.Roundtrip()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
"hash/fnv"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type StoreConfig struct {
|
||||
MaxHistory int
|
||||
MaxEntrySize int64
|
||||
}
|
||||
|
||||
func DefaultStoreConfig() StoreConfig {
|
||||
return StoreConfig{
|
||||
MaxHistory: 100,
|
||||
MaxEntrySize: 5 * 1024 * 1024,
|
||||
}
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
ID uint64
|
||||
Data []byte
|
||||
MimeType string
|
||||
Preview string
|
||||
Size int
|
||||
Timestamp time.Time
|
||||
IsImage bool
|
||||
Hash uint64
|
||||
}
|
||||
|
||||
func Store(data []byte, mimeType string) error {
|
||||
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
||||
}
|
||||
|
||||
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if int64(len(data)) > cfg.MaxEntrySize {
|
||||
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
||||
}
|
||||
|
||||
dbPath, err := GetDBPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get db path: %w", err)
|
||||
}
|
||||
|
||||
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
entry := Entry{
|
||||
Data: data,
|
||||
MimeType: mimeType,
|
||||
Size: len(data),
|
||||
Timestamp: time.Now(),
|
||||
IsImage: IsImageMimeType(mimeType),
|
||||
Hash: computeHash(data),
|
||||
}
|
||||
|
||||
switch {
|
||||
case entry.IsImage:
|
||||
entry.Preview = imagePreview(data, mimeType)
|
||||
default:
|
||||
entry.Preview = textPreview(data)
|
||||
}
|
||||
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deduplicateInTx(b, entry.Hash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry.ID = id
|
||||
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.Put(itob(id), encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return trimLengthInTx(b, cfg.MaxHistory)
|
||||
})
|
||||
}
|
||||
|
||||
func GetDBPath() (string, error) {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cacheDir = filepath.Join(homeDir, ".cache")
|
||||
}
|
||||
|
||||
newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
|
||||
newPath := filepath.Join(newDir, "db")
|
||||
|
||||
if _, err := os.Stat(newPath); err == nil {
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
oldDir := filepath.Join(cacheDir, "dms-clipboard")
|
||||
oldPath := filepath.Join(oldDir, "db")
|
||||
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.Rename(oldPath, newPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
os.Remove(oldDir)
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||
c := b.Cursor()
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
if extractHash(v) != hash {
|
||||
continue
|
||||
}
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimLengthInTx(b *bolt.Bucket, maxHistory int) error {
|
||||
c := b.Cursor()
|
||||
var count int
|
||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||
if count < maxHistory {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeEntry(e Entry) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
binary.Write(buf, binary.BigEndian, e.ID)
|
||||
binary.Write(buf, binary.BigEndian, uint32(len(e.Data)))
|
||||
buf.Write(e.Data)
|
||||
binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType)))
|
||||
buf.WriteString(e.MimeType)
|
||||
binary.Write(buf, binary.BigEndian, uint32(len(e.Preview)))
|
||||
buf.WriteString(e.Preview)
|
||||
binary.Write(buf, binary.BigEndian, int32(e.Size))
|
||||
binary.Write(buf, binary.BigEndian, e.Timestamp.Unix())
|
||||
if e.IsImage {
|
||||
buf.WriteByte(1)
|
||||
} else {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func itob(v uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, v)
|
||||
return b
|
||||
}
|
||||
|
||||
func computeHash(data []byte) uint64 {
|
||||
h := fnv.New64a()
|
||||
h.Write(data)
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func extractHash(data []byte) uint64 {
|
||||
if len(data) < 8 {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint64(data[len(data)-8:])
|
||||
}
|
||||
|
||||
func textPreview(data []byte) string {
|
||||
text := string(data)
|
||||
text = strings.TrimSpace(text)
|
||||
text = strings.Join(strings.Fields(text), " ")
|
||||
|
||||
if len(text) > 100 {
|
||||
return text[:100] + "…"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func imagePreview(data []byte, format string) string {
|
||||
config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format)
|
||||
}
|
||||
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
||||
}
|
||||
|
||||
func sizeStr(size int) string {
|
||||
units := []string{"B", "KiB", "MiB"}
|
||||
var i int
|
||||
fsize := float64(size)
|
||||
for fsize >= 1024 && i < len(units)-1 {
|
||||
fsize /= 1024
|
||||
i++
|
||||
}
|
||||
return fmt.Sprintf("%.0f %s", fsize, units[i])
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type ClipboardChange struct {
|
||||
Data []byte
|
||||
MimeType string
|
||||
MimeTypes []string
|
||||
}
|
||||
|
||||
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
wlCtx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(wlCtx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
||||
|
||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
offerMimeTypes[e.Id] = nil
|
||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
||||
})
|
||||
})
|
||||
|
||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mimes := offerMimeTypes[e.Id]
|
||||
selectedMime := selectPreferredMimeType(mimes)
|
||||
if selectedMime == "" {
|
||||
return
|
||||
}
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
|
||||
w.Close()
|
||||
r.Close()
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
|
||||
go func() {
|
||||
defer r.Close()
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
callback(data, selectedMime)
|
||||
}()
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
return fmt.Errorf("set read deadline: %w", err)
|
||||
}
|
||||
if err := wlCtx.Dispatch(); err != nil {
|
||||
if isTimeoutError(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("dispatch: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
wlCtx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(wlCtx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
||||
|
||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
offerMimeTypes[e.Id] = nil
|
||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
||||
})
|
||||
})
|
||||
|
||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mimes := offerMimeTypes[e.Id]
|
||||
selectedMime := selectPreferredMimeType(mimes)
|
||||
if selectedMime == "" {
|
||||
return
|
||||
}
|
||||
|
||||
mimesCopy := make([]string, len(mimes))
|
||||
copy(mimesCopy, mimes)
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
|
||||
w.Close()
|
||||
r.Close()
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
|
||||
go func() {
|
||||
defer r.Close()
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
callback(data, selectedMime, mimesCopy)
|
||||
}()
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
return fmt.Errorf("set read deadline: %w", err)
|
||||
}
|
||||
if err := wlCtx.Dispatch(); err != nil {
|
||||
if isTimeoutError(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("dispatch: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTimeoutError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
||||
ch := make(chan ClipboardChange, 16)
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
err := Watch(ctx, func(data []byte, mimeType string) {
|
||||
select {
|
||||
case ch <- ClipboardChange{Data: data, MimeType: mimeType}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
if err != nil && err != context.Canceled {
|
||||
errCh <- err
|
||||
}
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return ch, errCh
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,741 +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 := min(e.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 := min(e.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 := min(e.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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,314 +0,0 @@
|
||||
package colorpicker
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSurfaceState_ConcurrentPointerMotion(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const goroutines = 50
|
||||
const iterations = 100
|
||||
|
||||
for i := range goroutines {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := range iterations {
|
||||
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const goroutines = 30
|
||||
const iterations = 100
|
||||
|
||||
for i := range goroutines / 2 {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for range iterations {
|
||||
s.SetScale(int32(id%3 + 1))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
for range goroutines / 2 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range iterations {
|
||||
scale := s.Scale()
|
||||
assert.GreaterOrEqual(t, scale, int32(1))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const goroutines = 20
|
||||
const iterations = 100
|
||||
|
||||
for i := range goroutines / 2 {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := range iterations {
|
||||
_ = s.OnLayerConfigure(1920+id, 1080+j)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
for range goroutines / 2 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range iterations {
|
||||
w, h := s.LogicalSize()
|
||||
_ = w
|
||||
_ = h
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const goroutines = 30
|
||||
const iterations = 100
|
||||
|
||||
for range goroutines / 3 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range iterations {
|
||||
s.OnPointerButton(0x110, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for range goroutines / 3 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range iterations {
|
||||
s.OnKey(1, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for range goroutines / 3 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range iterations {
|
||||
picked, cancelled := s.IsDone()
|
||||
_ = picked
|
||||
_ = cancelled
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const goroutines = 20
|
||||
const iterations = 100
|
||||
|
||||
for range goroutines {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range iterations {
|
||||
_ = s.IsReady()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const goroutines = 20
|
||||
const iterations = 100
|
||||
|
||||
for range goroutines {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range iterations {
|
||||
s.SwapBuffers()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSurfaceState_ZeroScale(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
s.SetScale(0)
|
||||
assert.Equal(t, int32(1), s.Scale())
|
||||
}
|
||||
|
||||
func TestSurfaceState_NegativeScale(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
s.SetScale(-5)
|
||||
assert.Equal(t, int32(1), s.Scale())
|
||||
}
|
||||
|
||||
func TestSurfaceState_ZeroDimensionConfigure(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
|
||||
err := s.OnLayerConfigure(0, 100)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = s.OnLayerConfigure(100, 0)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = s.OnLayerConfigure(-1, 100)
|
||||
assert.NoError(t, err)
|
||||
|
||||
w, h := s.LogicalSize()
|
||||
assert.Equal(t, 0, w)
|
||||
assert.Equal(t, 0, h)
|
||||
}
|
||||
|
||||
func TestSurfaceState_PickColorNilBuffer(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
color, ok := s.PickColor()
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, Color{}, color)
|
||||
}
|
||||
|
||||
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
buf := s.Redraw()
|
||||
assert.Nil(t, buf)
|
||||
}
|
||||
|
||||
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
buf := s.RedrawScreenOnly()
|
||||
assert.Nil(t, buf)
|
||||
}
|
||||
|
||||
func TestSurfaceState_FrontRenderBufferNil(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
buf := s.FrontRenderBuffer()
|
||||
assert.Nil(t, buf)
|
||||
}
|
||||
|
||||
func TestSurfaceState_ScreenBufferNil(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
buf := s.ScreenBuffer()
|
||||
assert.Nil(t, buf)
|
||||
}
|
||||
|
||||
func TestSurfaceState_DestroyMultipleTimes(t *testing.T) {
|
||||
s := NewSurfaceState(FormatHex, false)
|
||||
s.Destroy()
|
||||
s.Destroy()
|
||||
}
|
||||
|
||||
func TestClamp(t *testing.T) {
|
||||
tests := []struct {
|
||||
v, lo, hi, expected int
|
||||
}{
|
||||
{5, 0, 10, 5},
|
||||
{-5, 0, 10, 0},
|
||||
{15, 0, 10, 10},
|
||||
{0, 0, 10, 0},
|
||||
{10, 0, 10, 10},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := clamp(tt.v, tt.lo, tt.hi)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampF(t *testing.T) {
|
||||
tests := []struct {
|
||||
v, lo, hi, expected float64
|
||||
}{
|
||||
{5.0, 0.0, 10.0, 5.0},
|
||||
{-5.0, 0.0, 10.0, 0.0},
|
||||
{15.0, 0.0, 10.0, 10.0},
|
||||
{0.0, 0.0, 10.0, 0.0},
|
||||
{10.0, 0.0, 10.0, 10.0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := clampF(tt.v, tt.lo, tt.hi)
|
||||
assert.InDelta(t, tt.expected, result, 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbs(t *testing.T) {
|
||||
tests := []struct {
|
||||
v, expected int
|
||||
}{
|
||||
{5, 5},
|
||||
{-5, 5},
|
||||
{0, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := abs(tt.v)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlendColors(t *testing.T) {
|
||||
bg := Color{R: 0, G: 0, B: 0, A: 255}
|
||||
fg := Color{R: 255, G: 255, B: 255, A: 255}
|
||||
|
||||
result := blendColors(bg, fg, 0.0)
|
||||
assert.Equal(t, bg.R, result.R)
|
||||
assert.Equal(t, bg.G, result.G)
|
||||
assert.Equal(t, bg.B, result.B)
|
||||
|
||||
result = blendColors(bg, fg, 1.0)
|
||||
assert.Equal(t, fg.R, result.R)
|
||||
assert.Equal(t, fg.G, result.G)
|
||||
assert.Equal(t, fg.B, result.B)
|
||||
|
||||
result = blendColors(bg, fg, 0.5)
|
||||
assert.InDelta(t, 127, int(result.R), 1)
|
||||
assert.InDelta(t, 127, int(result.G), 1)
|
||||
assert.InDelta(t, 127, int(result.B), 1)
|
||||
|
||||
result = blendColors(bg, fg, -1.0)
|
||||
assert.Equal(t, bg.R, result.R)
|
||||
|
||||
result = blendColors(bg, fg, 2.0)
|
||||
assert.Equal(t, fg.R, result.R)
|
||||
}
|
||||
@@ -1,699 +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, 0o755); 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, 0o755); 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, 0o644); 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, dmsDir)
|
||||
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), 0o644); 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)},
|
||||
{"outputs.kdl", ""},
|
||||
{"cursor.kdl", ""},
|
||||
{"windowrules.kdl", ""},
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
path := filepath.Join(dmsDir, cfg.name)
|
||||
// Skip if file already exists and is not empty to preserve user modifications
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(cfg.content), 0o644); 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, 0o755); 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, 0o644); 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), 0o644); 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", "themes", "dankcolors"),
|
||||
}
|
||||
|
||||
themesDir := filepath.Dir(colorResult.Path)
|
||||
if err := os.MkdirAll(themesDir, 0o755); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0o644); 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, 0o755); 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, 0o644); 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), 0o644); 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), 0o644); 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), 0o644); 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, 0o755); 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, 0o644); 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), 0o644); 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), 0o644); 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
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
||||
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
||||
|
||||
if len(existingOutputs) == 0 {
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
outputsPath := filepath.Join(dmsDir, "outputs.kdl")
|
||||
if _, err := os.Stat(outputsPath); err != nil {
|
||||
var outputsContent strings.Builder
|
||||
for _, output := range existingOutputs {
|
||||
outputsContent.WriteString(output)
|
||||
outputsContent.WriteString("\n\n")
|
||||
}
|
||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
|
||||
} else {
|
||||
cd.log("Migrated output sections to dms/outputs.kdl")
|
||||
}
|
||||
}
|
||||
|
||||
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
|
||||
|
||||
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
|
||||
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
|
||||
|
||||
if len(inputMatches) < 1 {
|
||||
return "", fmt.Errorf("could not find insertion point for output sections")
|
||||
}
|
||||
|
||||
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, 0o755); 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, 0o755); 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 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, 0o644); 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, dmsDir)
|
||||
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), 0o644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
if err := cd.deployHyprlandDmsConfigs(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 Hyprland configuration")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
||||
configs := []struct {
|
||||
name string
|
||||
content string
|
||||
}{
|
||||
{"colors.conf", HyprColorsConfig},
|
||||
{"layout.conf", HyprLayoutConfig},
|
||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||
{"outputs.conf", ""},
|
||||
{"cursor.conf", ""},
|
||||
{"windowrules.conf", ""},
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
path := filepath.Join(dmsDir, cfg.name)
|
||||
// Skip if file already exists and is not empty to preserve user modifications
|
||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(cfg.content), 0o644); 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) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
||||
|
||||
if len(existingMonitors) == 0 {
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
outputsPath := filepath.Join(dmsDir, "outputs.conf")
|
||||
if _, err := os.Stat(outputsPath); err != nil {
|
||||
var outputsContent strings.Builder
|
||||
for _, monitor := range existingMonitors {
|
||||
outputsContent.WriteString(monitor)
|
||||
outputsContent.WriteString("\n")
|
||||
}
|
||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
|
||||
} else {
|
||||
cd.log("Migrated monitor sections to dms/outputs.conf")
|
||||
}
|
||||
}
|
||||
|
||||
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;xcb")
|
||||
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;xcb",
|
||||
"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;xcb"
|
||||
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) {
|
||||
// Insert spawn-at-startup for dms after the environment block
|
||||
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
|
||||
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
|
||||
config = config[:loc[1]] + "\n" + spawnDms + config[loc[1]:]
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -1,626 +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) {
|
||||
tmpDir := t.TempDir()
|
||||
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig, tmpDir)
|
||||
|
||||
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), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0o644)
|
||||
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) {
|
||||
tmpDir := t.TempDir()
|
||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
|
||||
|
||||
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), "source = ./dms/binds.conf")
|
||||
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), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
||||
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), "source = ./dms/binds.conf")
|
||||
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, "source = ./dms/binds.conf")
|
||||
}
|
||||
|
||||
func TestGhosttyConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||
assert.Contains(t, GhosttyConfig, "theme = 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), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0o644)
|
||||
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\"")
|
||||
})
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func LocateDMSConfig() (string, error) {
|
||||
var primaryPaths []string
|
||||
|
||||
configHome, err := os.UserConfigDir()
|
||||
if err == nil && 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.SplitSeq(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.SplitSeq(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
|
||||
theme = dankcolors
|
||||
@@ -1,162 +0,0 @@
|
||||
# === Application Launchers ===
|
||||
bind = SUPER, T, exec, {{TERMINAL_COMMAND}}
|
||||
bind = SUPER, space, exec, dms ipc call spotlight toggle
|
||||
bind = SUPER, V, exec, dms ipc call clipboard toggle
|
||||
bind = SUPER, M, exec, dms ipc call processlist focusOrToggle
|
||||
bind = SUPER, comma, exec, dms ipc call settings focusOrToggle
|
||||
bind = SUPER, N, exec, dms ipc call notifications toggle
|
||||
bind = SUPER SHIFT, N, exec, dms ipc call notepad toggle
|
||||
bind = SUPER, Y, exec, dms ipc call dankdash wallpaper
|
||||
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
|
||||
bind = SUPER, X, exec, dms ipc call powermenu toggle
|
||||
|
||||
# === Cheat sheet
|
||||
bind = SUPER SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
||||
|
||||
# === Security ===
|
||||
bind = SUPER ALT, L, exec, dms ipc call lock lock
|
||||
bind = SUPER 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
|
||||
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
|
||||
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
||||
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
||||
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
||||
bindel = CTRL, XF86AudioRaiseVolume, exec, dms ipc call mpris increment 3
|
||||
bindel = CTRL, XF86AudioLowerVolume, exec, dms ipc call mpris decrement 3
|
||||
|
||||
# === Brightness Controls ===
|
||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
||||
|
||||
# === Window Management ===
|
||||
bind = SUPER, Q, killactive
|
||||
bind = SUPER, F, fullscreen, 1
|
||||
bind = SUPER SHIFT, F, fullscreen, 0
|
||||
bind = SUPER SHIFT, T, togglefloating
|
||||
bind = SUPER, W, togglegroup
|
||||
bind = SUPER SHIFT, W, exec, dms ipc call window-rules toggle
|
||||
|
||||
# === Focus Navigation ===
|
||||
bind = SUPER, left, movefocus, l
|
||||
bind = SUPER, down, movefocus, d
|
||||
bind = SUPER, up, movefocus, u
|
||||
bind = SUPER, right, movefocus, r
|
||||
bind = SUPER, H, movefocus, l
|
||||
bind = SUPER, J, movefocus, d
|
||||
bind = SUPER, K, movefocus, u
|
||||
bind = SUPER, L, movefocus, r
|
||||
|
||||
# === Window Movement ===
|
||||
bind = SUPER SHIFT, left, movewindow, l
|
||||
bind = SUPER SHIFT, down, movewindow, d
|
||||
bind = SUPER SHIFT, up, movewindow, u
|
||||
bind = SUPER SHIFT, right, movewindow, r
|
||||
bind = SUPER SHIFT, H, movewindow, l
|
||||
bind = SUPER SHIFT, J, movewindow, d
|
||||
bind = SUPER SHIFT, K, movewindow, u
|
||||
bind = SUPER SHIFT, L, movewindow, r
|
||||
|
||||
# === Column Navigation ===
|
||||
bind = SUPER, Home, focuswindow, first
|
||||
bind = SUPER, End, focuswindow, last
|
||||
|
||||
# === Monitor Navigation ===
|
||||
bind = SUPER CTRL, left, focusmonitor, l
|
||||
bind = SUPER CTRL, right, focusmonitor, r
|
||||
bind = SUPER CTRL, H, focusmonitor, l
|
||||
bind = SUPER CTRL, J, focusmonitor, d
|
||||
bind = SUPER CTRL, K, focusmonitor, u
|
||||
bind = SUPER CTRL, L, focusmonitor, r
|
||||
|
||||
# === Move to Monitor ===
|
||||
bind = SUPER SHIFT CTRL, left, movewindow, mon:l
|
||||
bind = SUPER SHIFT CTRL, down, movewindow, mon:d
|
||||
bind = SUPER SHIFT CTRL, up, movewindow, mon:u
|
||||
bind = SUPER SHIFT CTRL, right, movewindow, mon:r
|
||||
bind = SUPER SHIFT CTRL, H, movewindow, mon:l
|
||||
bind = SUPER SHIFT CTRL, J, movewindow, mon:d
|
||||
bind = SUPER SHIFT CTRL, K, movewindow, mon:u
|
||||
bind = SUPER SHIFT CTRL, L, movewindow, mon:r
|
||||
|
||||
# === Workspace Navigation ===
|
||||
bind = SUPER, Page_Down, workspace, e+1
|
||||
bind = SUPER, Page_Up, workspace, e-1
|
||||
bind = SUPER, U, workspace, e+1
|
||||
bind = SUPER, I, workspace, e-1
|
||||
bind = SUPER CTRL, down, movetoworkspace, e+1
|
||||
bind = SUPER CTRL, up, movetoworkspace, e-1
|
||||
bind = SUPER CTRL, U, movetoworkspace, e+1
|
||||
bind = SUPER CTRL, I, movetoworkspace, e-1
|
||||
|
||||
# === Workspace Management ===
|
||||
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
|
||||
|
||||
# === Move Workspaces ===
|
||||
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
||||
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
||||
bind = SUPER SHIFT, U, movetoworkspace, e+1
|
||||
bind = SUPER SHIFT, I, movetoworkspace, e-1
|
||||
|
||||
# === Mouse Wheel Navigation ===
|
||||
bind = SUPER, mouse_down, workspace, e+1
|
||||
bind = SUPER, mouse_up, workspace, e-1
|
||||
bind = SUPER CTRL, mouse_down, movetoworkspace, e+1
|
||||
bind = SUPER CTRL, mouse_up, movetoworkspace, e-1
|
||||
|
||||
# === Numbered Workspaces ===
|
||||
bind = SUPER, 1, workspace, 1
|
||||
bind = SUPER, 2, workspace, 2
|
||||
bind = SUPER, 3, workspace, 3
|
||||
bind = SUPER, 4, workspace, 4
|
||||
bind = SUPER, 5, workspace, 5
|
||||
bind = SUPER, 6, workspace, 6
|
||||
bind = SUPER, 7, workspace, 7
|
||||
bind = SUPER, 8, workspace, 8
|
||||
bind = SUPER, 9, workspace, 9
|
||||
|
||||
# === Move to Numbered Workspaces ===
|
||||
bind = SUPER SHIFT, 1, movetoworkspace, 1
|
||||
bind = SUPER SHIFT, 2, movetoworkspace, 2
|
||||
bind = SUPER SHIFT, 3, movetoworkspace, 3
|
||||
bind = SUPER SHIFT, 4, movetoworkspace, 4
|
||||
bind = SUPER SHIFT, 5, movetoworkspace, 5
|
||||
bind = SUPER SHIFT, 6, movetoworkspace, 6
|
||||
bind = SUPER SHIFT, 7, movetoworkspace, 7
|
||||
bind = SUPER SHIFT, 8, movetoworkspace, 8
|
||||
bind = SUPER SHIFT, 9, movetoworkspace, 9
|
||||
|
||||
# === Column Management ===
|
||||
bind = SUPER, bracketleft, layoutmsg, preselect l
|
||||
bind = SUPER, bracketright, layoutmsg, preselect r
|
||||
|
||||
# === Sizing & Layout ===
|
||||
bind = SUPER, R, layoutmsg, togglesplit
|
||||
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||
bindmd = SUPER, mouse:273, Resize window, resizewindow
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindd = SUPER, code:20, Expand window left, resizeactive, -100 0
|
||||
bindd = SUPER, code:21, Shrink window left, resizeactive, 100 0
|
||||
|
||||
# === Manual Sizing ===
|
||||
binde = SUPER, minus, resizeactive, -10% 0
|
||||
binde = SUPER, equal, resizeactive, 10% 0
|
||||
binde = SUPER SHIFT, minus, resizeactive, 0 -10%
|
||||
binde = SUPER 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 = SUPER SHIFT, P, dpms, toggle
|
||||
@@ -1,25 +0,0 @@
|
||||
# ! Auto-generated file. Do not edit directly.
|
||||
# Remove source = ./dms/colors.conf from your config to override.
|
||||
|
||||
$primary = rgb(d0bcff)
|
||||
$outline = rgb(948f99)
|
||||
$error = rgb(f2b8b5)
|
||||
|
||||
general {
|
||||
col.active_border = $primary
|
||||
col.inactive_border = $outline
|
||||
}
|
||||
|
||||
group {
|
||||
col.border_active = $primary
|
||||
col.border_inactive = $outline
|
||||
col.border_locked_active = $error
|
||||
col.border_locked_inactive = $outline
|
||||
|
||||
groupbar {
|
||||
col.active = $primary
|
||||
col.inactive = $outline
|
||||
col.locked_active = $error
|
||||
col.locked_inactive = $outline
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# Auto-generated by DMS - do not edit manually
|
||||
|
||||
general {
|
||||
gaps_in = 4
|
||||
gaps_out = 4
|
||||
border_size = 2
|
||||
}
|
||||
|
||||
decoration {
|
||||
rounding = 12
|
||||
}
|
||||
@@ -1,121 +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
|
||||
|
||||
# ==================
|
||||
# INPUT CONFIG
|
||||
# ==================
|
||||
input {
|
||||
kb_layout = us
|
||||
numlock_by_default = true
|
||||
}
|
||||
|
||||
# ==================
|
||||
# GENERAL LAYOUT
|
||||
# ==================
|
||||
general {
|
||||
gaps_in = 5
|
||||
gaps_out = 5
|
||||
border_size = 2
|
||||
|
||||
layout = dwindle
|
||||
}
|
||||
|
||||
# ==================
|
||||
# DECORATION
|
||||
# ==================
|
||||
decoration {
|
||||
rounding = 12
|
||||
|
||||
active_opacity = 1.0
|
||||
inactive_opacity = 1.0
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# ==================
|
||||
# WINDOW RULES
|
||||
# ==================
|
||||
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
|
||||
|
||||
windowrule = rounding 12, match:class ^(org\.gnome\.)
|
||||
|
||||
windowrule = tile on, match:class ^(gnome-control-center)$
|
||||
windowrule = tile on, match:class ^(pavucontrol)$
|
||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||
|
||||
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
||||
windowrule = float on, match:class ^(gnome-calculator)$
|
||||
windowrule = float on, match:class ^(galculator)$
|
||||
windowrule = float on, match:class ^(blueman-manager)$
|
||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
||||
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
||||
|
||||
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||
|
||||
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
||||
windowrule = float on, match:class ^(zoom)$
|
||||
|
||||
# DMS windows floating by default
|
||||
# ! Hyprland doesn't size these windows correctly so disabling by default here
|
||||
# windowrule = float on, match:class ^(org.quickshell)$
|
||||
|
||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||
layerrule = no_anim on, match:namespace ^dms:.*
|
||||
|
||||
source = ./dms/colors.conf
|
||||
source = ./dms/outputs.conf
|
||||
source = ./dms/layout.conf
|
||||
source = ./dms/cursor.conf
|
||||
source = ./dms/binds.conf
|
||||
@@ -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
|
||||
@@ -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,10 +0,0 @@
|
||||
// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
recent-windows {
|
||||
highlight {
|
||||
corner-radius 12
|
||||
}
|
||||
}
|
||||
@@ -1,221 +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";
|
||||
}
|
||||
|
||||
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
|
||||
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";
|
||||
}
|
||||
XF86AudioPause allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||
}
|
||||
XF86AudioPlay allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||
}
|
||||
XF86AudioPrev allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "previous";
|
||||
}
|
||||
XF86AudioNext allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "next";
|
||||
}
|
||||
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "increment" "3";
|
||||
}
|
||||
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
|
||||
}
|
||||
|
||||
// === 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; }
|
||||
Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; }
|
||||
|
||||
// === 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; }
|
||||
|
||||
// === Workspace Management ===
|
||||
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
|
||||
spawn "dms" "ipc" "call" "workspace-rename" "open";
|
||||
}
|
||||
|
||||
// === 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,39 +0,0 @@
|
||||
// ! Auto-generated file. Do not edit directly.
|
||||
// Remove `include "dms/colors.kdl"` from your config to override.
|
||||
|
||||
layout {
|
||||
background-color "transparent"
|
||||
|
||||
focus-ring {
|
||||
active-color "#d0bcff"
|
||||
inactive-color "#948f99"
|
||||
urgent-color "#f2b8b5"
|
||||
}
|
||||
|
||||
border {
|
||||
active-color "#d0bcff"
|
||||
inactive-color "#948f99"
|
||||
urgent-color "#f2b8b5"
|
||||
}
|
||||
|
||||
shadow {
|
||||
color "#00000070"
|
||||
}
|
||||
|
||||
tab-indicator {
|
||||
active-color "#d0bcff"
|
||||
inactive-color "#948f99"
|
||||
urgent-color "#f2b8b5"
|
||||
}
|
||||
|
||||
insert-hint {
|
||||
color "#d0bcff80"
|
||||
}
|
||||
}
|
||||
|
||||
recent-windows {
|
||||
highlight {
|
||||
active-color "#4f378b"
|
||||
urgent-color "#f2b8b5"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
hotkey-overlay {
|
||||
skip-at-startup
|
||||
}
|
||||
|
||||
environment {
|
||||
DMS_RUN_GREETER "1"
|
||||
}
|
||||
|
||||
gestures {
|
||||
hot-corners {
|
||||
off
|
||||
}
|
||||
}
|
||||
|
||||
layout {
|
||||
background-color "#000000"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// ! DO NOT EDIT !
|
||||
// ! AUTO-GENERATED BY DMS !
|
||||
// ! CHANGES WILL BE OVERWRITTEN !
|
||||
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,279 +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 {
|
||||
// You can set rules, model, layout, variant and options.
|
||||
// For more information, see xkeyboard-config(7).
|
||||
|
||||
// For example:
|
||||
// layout "us,ru"
|
||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
||||
|
||||
// If this section is empty, niri will fetch xkb settings
|
||||
// from org.freedesktop.locale1. You can control these using
|
||||
// localectl set-x11-keymap.
|
||||
}
|
||||
|
||||
// Enable numlock on startup, omitting this setting disables it.
|
||||
numlock
|
||||
}
|
||||
|
||||
// Next sections include libinput settings.
|
||||
// Omitting settings disables them, or leaves them at their default values.
|
||||
// All commented-out settings here are examples, not defaults.
|
||||
touchpad {
|
||||
// off
|
||||
tap
|
||||
// dwt
|
||||
// dwtp
|
||||
// drag false
|
||||
// drag-lock
|
||||
natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "two-finger"
|
||||
// disabled-on-external-mouse
|
||||
}
|
||||
|
||||
mouse {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "no-scroll"
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "on-button-down"
|
||||
// scroll-button 273
|
||||
// scroll-button-lock
|
||||
// middle-emulation
|
||||
}
|
||||
|
||||
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||
// warp-mouse-to-focus
|
||||
|
||||
// Focus windows and outputs automatically when moving the mouse into them.
|
||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||
// focus-follows-mouse max-scroll-amount="0%"
|
||||
}
|
||||
// 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.
|
||||
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#"^org\.gnome\.Calculator$"#
|
||||
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#"^xdg-desktop-portal$"#
|
||||
open-floating true
|
||||
}
|
||||
window-rule {
|
||||
match app-id=r#"^steam$"# title=r#"^notificationtoasts_\d+_desktop$"#
|
||||
default-floating-position x=10 y=10 relative-to="bottom-right"
|
||||
open-focused false
|
||||
}
|
||||
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 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$"#
|
||||
match app-id=r#"com.danklinux.dms$"#
|
||||
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"
|
||||
include "dms/outputs.kdl"
|
||||
include "dms/cursor.kdl"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user