mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 23:32:50 -04:00
Compare commits
217 Commits
v0.4.3
...
c49a875ec2
| Author | SHA1 | Date | |
|---|---|---|---|
| c49a875ec2 | |||
| 2a002304b9 | |||
| d9522818ae | |||
| 800588e121 | |||
| 991c31ebdb | |||
| 48f77e1691 | |||
| 42de6fd074 | |||
| 62845b470c | |||
| fd20986cf8 | |||
| 61369cde9e | |||
| 644384ce8b | |||
| 97c11a2482 | |||
| 1e7e1c2d78 | |||
| 1c7201fb04 | |||
| 61ec0c697a | |||
| 4b5fce1bfc | |||
| 6cc6e7c8e9 | |||
| 89298fce30 | |||
| a3a27e07fa | |||
| 4f32376f22 | |||
| 58bf189941 | |||
| bcfa508da5 | |||
| c0ae3ef58b | |||
| 1e70d7b4c3 | |||
| f8dc6ad2bc | |||
| e22482988f | |||
| 4eb896629d | |||
| b310e66275 | |||
| b39da1bea7 | |||
| fa575d0574 | |||
| dfe2f3771b | |||
| 46caeb0445 | |||
| 59cc9c7006 | |||
| 12e91534eb | |||
| d9da88ceb5 | |||
| 2dbfec0307 | |||
| 09cf8c9641 | |||
| f1bed4d6a3 | |||
| 2ed6c33c83 | |||
| 7ad532ed17 | |||
| 92fe8c5b14 | |||
| 8e95572589 | |||
| 62da862a66 | |||
| 993e34f548 | |||
| e39465aece | |||
| 8fd616b680 | |||
| cc054b27de | |||
| dfdaa82245 | |||
| 99a307e0ad | |||
| 5ddea836a1 | |||
| 208d92aa06 | |||
| 6ef9ddd4f3 | |||
| 1c92d39185 | |||
| c0f072217c | |||
| 542562f988 | |||
| 4e6f0d5e87 | |||
| 10639a5ead | |||
| 06d668e710 | |||
| d1472dfcba | |||
| ccb4da3cd8 | |||
| 46e96b49f0 | |||
| 984cfe7f98 | |||
| d769300137 | |||
| d175d66828 | |||
| c1a314332e | |||
| 046ac59d21 | |||
| 00c06f07d0 | |||
| 3e2ab40c6a | |||
| 350ffd0052 | |||
| ecd1a622d2 | |||
| f13968aa61 | |||
| 4d1ffde54c | |||
| d69017a706 | |||
| f2deaeccdb | |||
| ea9b0d2a79 | |||
| 2e6dbedb8b | |||
| 6f359df8f9 | |||
| f6db20cd06 | |||
| 6287fae065 | |||
| e441607ce3 | |||
| b5379a95fa | |||
| 64ec5be919 | |||
| 3916512d66 | |||
| e2f426a1bd | |||
| aa1df8dfcf | |||
| 67557555f2 | |||
| 4cb652abd9 | |||
| d11868b99f | |||
| 1798417e6a | |||
| 43dc3e5bb1 | |||
| 91891a14ed | |||
| 20f7d60147 | |||
| 7e17e7d37a | |||
| cbb244f785 | |||
| 1c264d858b | |||
| 217037c2ae | |||
| b4dbd0b69c | |||
| 89a2b5c00b | |||
| 929b6dae1a | |||
| 52fe493da9 | |||
| 3e6be3e762 | |||
| 7a8cc449b9 | |||
| 8f5a9d6e9f | |||
| 1c5e31fea9 | |||
| fd08ae18ab | |||
| a7eb3de06e | |||
| 8902dd7c44 | |||
| 6387d8400c | |||
| 597cacb9cc | |||
| 3e285ad9ff | |||
| cc1fa89790 | |||
| b0ed007751 | |||
| e1e2650d2b | |||
| b23f17b633 | |||
| 818e40b2df | |||
| 5685e39631 | |||
| 72534b7674 | |||
| 328490d23d | |||
| 97a0696930 | |||
| cb4e0660e0 | |||
| 67c642de4c | |||
| 0d7c2e1024 | |||
| 16a779a41b | |||
| c4ca3c8644 | |||
| aabcbe34f3 | |||
| f06626e441 | |||
| c4e1a71776 | |||
| 77e6c16bd2 | |||
| 9d1fac3570 | |||
| b7aeaa7fc5 | |||
| f6d8c9ff61 | |||
| 0490794d6c | |||
| 335c83dd3c | |||
| 91da720c26 | |||
| b6ac744a68 | |||
| 526c4092fd | |||
| ed06dda384 | |||
| 6465b11e9b | |||
| b2879878a1 | |||
| 3e17b086fb | |||
| 0545e6bcda | |||
| 27a907433f | |||
| 69616800e3 | |||
| abf1f53432 | |||
| 881c5f75cb | |||
| 4e45796ade | |||
| 1ce4ea5230 | |||
| f2a2437baa | |||
| 508dc9db1e | |||
| a914e3557f | |||
| f489dc062f | |||
| a7e09f4850 | |||
| 8ea97530d4 | |||
| 13ab54e83a | |||
| 4bc40325cb | |||
| 58d9355ea3 | |||
| d46b7528e7 | |||
| 1858597fc9 | |||
| 83cce5afe4 | |||
| 201bd8dc1f | |||
| b62ba69060 | |||
| 5d2f5557e5 | |||
| cf75c1aad0 | |||
| 76a60df88b | |||
| 9322c79b4e | |||
| 12365edcf0 | |||
| 5efc1f9dad | |||
| ab976cbb24 | |||
| db584b7897 | |||
| 0fdc0748cf | |||
| 2e79c21dc2 | |||
| 5490a230bd | |||
| a6b059b30d | |||
| 712e6011aa | |||
| 68f6f87410 | |||
| 50cdd68b7b | |||
| e8510b925e | |||
| 24e800501a | |||
| 6013c994a6 | |||
| 46c90628b9 | |||
| d2d2dac5d1 | |||
| fd3e7470f4 | |||
| b79e9f72ce | |||
| 77eb5dd3bf | |||
| b17c14a07b | |||
| 494d90be22 | |||
| da7e599e65 | |||
| e3b7360f39 | |||
| 367130882d | |||
| d8563ba79d | |||
| e527453964 | |||
| 88fe3c5fbd | |||
| 748faf92c1 | |||
| 0126aded78 | |||
| 695a75ea09 | |||
| 80e690f9fc | |||
| e8770b90ef | |||
| eec9da42bf | |||
| 1c8f0d6292 | |||
| b753c8840b | |||
| 95589982a5 | |||
| 37a10bd453 | |||
| 7abc76e92c | |||
| 7aa4467bda | |||
| 471938adb6 | |||
| 201a7e3b34 | |||
| 11ec3723c3 | |||
| 75eb736856 | |||
| 8fea126c20 | |||
| cc02d09c4d | |||
| af95631a1d | |||
| 7b3d2ab85a | |||
| c52df96af9 | |||
| dee5fa60af | |||
| 5e99fdd9c9 | |||
| eb01fe757b | |||
| c52483da2c |
@@ -1,5 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# DISABLED for now
|
||||||
|
exit 0
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|||||||
@@ -9,17 +9,9 @@ assignees: ""
|
|||||||
<!-- If your issue is related to ICONS
|
<!-- If your issue is related to ICONS
|
||||||
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
||||||
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
||||||
- Follow the [THEMING](https://github.com/AvengeMedia/DankMaterialShell/tree/master?tab=readme-ov-file#theming) section to ensure your QT environment variable is configured correctl for themes.
|
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
|
||||||
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
||||||
|
|
||||||
<!-- If your issue is related to APP LAUNCHER/DOCK/Running Apps being stale
|
|
||||||
Quickshell does not ever update its DesktopEntires.
|
|
||||||
There is an open PR for it, that has been stuck unmerged over there to fix it.
|
|
||||||
We unfortunately are at the mercy of quickshell to merge it.
|
|
||||||
Until then, newly installed and removed apps will not react until the
|
|
||||||
shell is restarted.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Compositor
|
## Compositor
|
||||||
|
|
||||||
- [ ] niri
|
- [ ] niri
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: Go CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
paths:
|
||||||
|
- 'core/**'
|
||||||
|
- '.github/workflows/go-ci.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
gofmt -s -l .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: Build dms
|
||||||
|
run: go build -v ./cmd/dms
|
||||||
|
|
||||||
|
- name: Build dankinstall
|
||||||
|
run: go build -v ./cmd/dankinstall
|
||||||
+541
-70
@@ -1,47 +1,192 @@
|
|||||||
# Release from a dispatch event from the danklinux repo
|
name: Release
|
||||||
name: Create Release from DMS
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
repository_dispatch:
|
push:
|
||||||
types: [dms_release]
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
actions: write
|
actions: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.event.client_payload.tag }}
|
group: release-${{ github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_release_from_dms:
|
build-core:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
env:
|
strategy:
|
||||||
TAG: ${{ github.event.client_payload.tag }}
|
matrix:
|
||||||
DMS_REPO: ${{ github.event.client_payload.dms_repo }}
|
arch: [amd64, arm64]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Ensure VERSION and tag
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
git config user.name "github-actions[bot]"
|
echo "The following files are not formatted:"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
gofmt -s -l .
|
||||||
|
exit 1
|
||||||
echo "${TAG}" > VERSION
|
|
||||||
|
|
||||||
git add -A VERSION
|
|
||||||
|
|
||||||
if ! git diff --cached --quiet; then
|
|
||||||
git commit -m "Update VERSION to ${TAG} (from DMS)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git tag -f "${TAG}"
|
- name: Run tests
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
git push origin HEAD
|
- name: Build dankinstall (${{ matrix.arch }})
|
||||||
git push -f origin "${TAG}"
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
cd cmd/dankinstall
|
||||||
|
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-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=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-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=${GITHUB_REF#refs/tags/}" \
|
||||||
|
-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: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Update VERSION
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
version="${GITHUB_REF#refs/tags/}"
|
||||||
|
version_no_v="${version#v}"
|
||||||
|
echo "Updating to version: $version"
|
||||||
|
|
||||||
|
# Update VERSION file in quickshell/
|
||||||
|
echo "${version}" > quickshell/VERSION
|
||||||
|
|
||||||
|
git add quickshell/VERSION
|
||||||
|
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git commit -m "chore: bump version to $version"
|
||||||
|
git push origin HEAD:master || git push origin HEAD:main
|
||||||
|
echo "Pushed version updates to master"
|
||||||
|
else
|
||||||
|
echo "No version changes needed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Force-push the tag to point to the commit with updated VERSION
|
||||||
|
git tag -f "${version}"
|
||||||
|
git push -f origin "${version}"
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs: [build-core, update-versions]
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fetch updated tag after version bump
|
||||||
|
run: |
|
||||||
|
git fetch origin --force tag ${{ github.ref_name }}
|
||||||
|
git checkout ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Download core artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: core-assets-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: ./_core_assets
|
||||||
|
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
@@ -55,17 +200,25 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cat > RELEASE_BODY.md << 'EOF'
|
cat > RELEASE_BODY.md << 'EOF'
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://install.danklinux.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
## Assets
|
## Assets
|
||||||
|
|
||||||
### Complete Packages
|
### Complete Packages
|
||||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + installation guide)
|
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + installation guide)
|
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
|
||||||
### Individual Components
|
### Individual Components
|
||||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||||
|
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||||
|
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||||
- **`dms-qml.tar.gz`** - QML source code only
|
- **`dms-qml.tar.gz`** - QML source code only
|
||||||
|
|
||||||
### Checksums
|
### Checksums
|
||||||
@@ -89,30 +242,14 @@ jobs:
|
|||||||
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
cat RELEASE_BODY.md >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create/Update DankMaterialShell Release
|
- name: Prepare release assets
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.TAG }}
|
|
||||||
name: Release ${{ env.TAG }}
|
|
||||||
body: ${{ steps.changelog.outputs.changelog }}
|
|
||||||
draft: false
|
|
||||||
prerelease: ${{ contains(env.TAG, '-') }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Download and prepare release assets
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
mkdir -p _release_assets
|
mkdir -p _release_assets
|
||||||
|
|
||||||
# Download DMS CLI binaries from the danklinux repo
|
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
|
||||||
gh release download "${TAG}" -R "${DMS_REPO}" --dir ./_dms_assets
|
for file in _core_assets/dms-*.gz*; do
|
||||||
|
|
||||||
# Rename CLI binaries to dms-cli-* format and copy distropkg binaries
|
|
||||||
for file in _dms_assets/dms-*.gz*; do
|
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
basename=$(basename "$file")
|
basename=$(basename "$file")
|
||||||
if [[ "$basename" == dms-distropkg-* ]]; then
|
if [[ "$basename" == dms-distropkg-* ]]; then
|
||||||
@@ -124,13 +261,21 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Create QML source package (exclude .git, .github, build artifacts)
|
# Copy dankinstall binaries
|
||||||
tar --exclude='.git' \
|
cp _core_assets/dankinstall-*.gz* _release_assets/
|
||||||
|
|
||||||
|
# Copy completions
|
||||||
|
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create QML source package (exclude build artifacts and git files)
|
||||||
|
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||||
|
cp LICENSE CONTRIBUTING.md quickshell/
|
||||||
|
|
||||||
|
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||||
|
(cd quickshell && tar --exclude='.git' \
|
||||||
--exclude='.github' \
|
--exclude='.github' \
|
||||||
--exclude='_dms_assets' \
|
|
||||||
--exclude='_release_assets' \
|
|
||||||
--exclude='*.tar.gz' \
|
--exclude='*.tar.gz' \
|
||||||
-czf _release_assets/dms-qml.tar.gz .
|
-czf ../_release_assets/dms-qml.tar.gz .)
|
||||||
|
|
||||||
# Generate checksum for QML package
|
# Generate checksum for QML package
|
||||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||||
@@ -139,24 +284,36 @@ jobs:
|
|||||||
for arch in amd64 arm64; do
|
for arch in amd64 arm64; do
|
||||||
mkdir -p _temp_full/dms
|
mkdir -p _temp_full/dms
|
||||||
mkdir -p _temp_full/bin
|
mkdir -p _temp_full/bin
|
||||||
|
mkdir -p _temp_full/completions
|
||||||
|
|
||||||
# Extract QML source to temp directory
|
# Extract QML source
|
||||||
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
||||||
|
|
||||||
# Copy CLI binary if it exists
|
# Add CLI binaries
|
||||||
if [ -f "_dms_assets/dms-${arch}.gz" ]; then
|
if [ -f "_core_assets/dms-${arch}.gz" ]; then
|
||||||
gunzip -c "_dms_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
gunzip -c "_core_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||||
chmod +x _temp_full/bin/dms
|
chmod +x _temp_full/bin/dms
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy distropkg binary if it exists
|
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
|
||||||
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
|
gunzip -c "_core_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||||
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
|
||||||
chmod +x _temp_full/bin/dms-distropkg
|
chmod +x _temp_full/bin/dms-distropkg
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create INSTALL.md
|
# Add shell completions
|
||||||
cat > _temp_full/INSTALL.md << 'EOF'
|
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
|
# DankMaterialShell Installation
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -176,16 +333,23 @@ jobs:
|
|||||||
2. **Install the DMS CLI binaries:**
|
2. **Install the DMS CLI binaries:**
|
||||||
```bash
|
```bash
|
||||||
sudo install -m 755 bin/dms /usr/local/bin/dms
|
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||||
# or install to a local directory:
|
|
||||||
mkdir -p ~/.local/bin
|
|
||||||
install -m 755 bin/dms ~/.local/bin/dms
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start the shell:**
|
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
|
```bash
|
||||||
dms run
|
dms run
|
||||||
# or directly with quickshell (will lack some dbus integrations & plugin management):
|
|
||||||
quickshell -p ~/.config/quickshell/dms
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -196,10 +360,9 @@ jobs:
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- Run with verbose output: `quickshell -v -p ~/.config/quickshell/dms`
|
- Run with verbose output: `DMS_LOG_LEVEL=debug dms run`
|
||||||
- Check logs in `~/.local/state/DankMaterialShell/`
|
|
||||||
- Ensure all dependencies are installed
|
- Ensure all dependencies are installed
|
||||||
EOF
|
EOFINSTALL
|
||||||
|
|
||||||
# Create the full package
|
# Create the full package
|
||||||
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
(cd _temp_full && tar -czf "../_release_assets/dms-full-${arch}.tar.gz" .)
|
||||||
@@ -211,11 +374,319 @@ jobs:
|
|||||||
rm -rf _temp_full
|
rm -rf _temp_full
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Attach all assets to release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG }}
|
tag_name: ${{ env.TAG }}
|
||||||
|
name: Release ${{ env.TAG }}
|
||||||
|
body: ${{ steps.changelog.outputs.changelog }}
|
||||||
files: _release_assets/**
|
files: _release_assets/**
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ contains(env.TAG, '-') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
trigger-obs-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Update OBS packages
|
||||||
|
run: |
|
||||||
|
VERSION="${{ github.ref_name }}"
|
||||||
|
cd distro
|
||||||
|
bash scripts/obs-upload.sh dms "Update to $VERSION"
|
||||||
|
|
||||||
|
trigger-ppa-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
debhelper \
|
||||||
|
devscripts \
|
||||||
|
dput \
|
||||||
|
lftp \
|
||||||
|
build-essential \
|
||||||
|
fakeroot \
|
||||||
|
dpkg-dev
|
||||||
|
|
||||||
|
- name: Configure GPG
|
||||||
|
env:
|
||||||
|
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "$GPG_KEY" | gpg --import
|
||||||
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload to PPA
|
||||||
|
run: |
|
||||||
|
VERSION="${{ github.ref_name }}"
|
||||||
|
cd distro/ubuntu/ppa
|
||||||
|
bash create-and-upload.sh ../dms dms questing
|
||||||
|
|
||||||
|
copr-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Building DMS stable version: $VERSION"
|
||||||
|
|
||||||
|
- name: Setup build environment
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y rpm wget curl jq gzip
|
||||||
|
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
|
- name: Download release assets
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
cd ~/rpmbuild/SOURCES
|
||||||
|
|
||||||
|
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||||
|
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Generate stable spec file
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
|
|
||||||
|
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||||
|
# Spec for DMS stable releases - Generated by GitHub Actions
|
||||||
|
|
||||||
|
%global debug_package %{nil}
|
||||||
|
%global version VERSION_PLACEHOLDER
|
||||||
|
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
|
|
||||||
|
Name: dms
|
||||||
|
Version: %{version}
|
||||||
|
Release: 1%{?dist}
|
||||||
|
Summary: %{pkg_summary}
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
Source0: dms-qml.tar.gz
|
||||||
|
|
||||||
|
BuildRequires: gzip
|
||||||
|
BuildRequires: wget
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
|
Requires: (quickshell or quickshell-git)
|
||||||
|
Requires: accountsservice
|
||||||
|
Requires: dms-cli
|
||||||
|
Requires: dgop
|
||||||
|
|
||||||
|
Recommends: cava
|
||||||
|
Recommends: cliphist
|
||||||
|
Recommends: danksearch
|
||||||
|
Recommends: hyprpicker
|
||||||
|
Recommends: matugen
|
||||||
|
Recommends: wl-clipboard
|
||||||
|
Recommends: NetworkManager
|
||||||
|
Recommends: qt6-qtmultimedia
|
||||||
|
Suggests: qt6ct
|
||||||
|
|
||||||
|
%description
|
||||||
|
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
||||||
|
and optimized for the niri and hyprland compositors. Features notifications,
|
||||||
|
app launcher, wallpaper customization, and fully customizable with plugins.
|
||||||
|
|
||||||
|
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
||||||
|
process monitoring, notification center, clipboard history, dock, control center,
|
||||||
|
lock screen, and comprehensive plugin system.
|
||||||
|
|
||||||
|
%package -n dms-cli
|
||||||
|
Summary: DankMaterialShell CLI tool
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
%description -n dms-cli
|
||||||
|
Command-line interface for DankMaterialShell configuration and management.
|
||||||
|
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||||
|
|
||||||
|
%package -n dgop
|
||||||
|
Summary: Stateless CPU/GPU monitor for DankMaterialShell
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/AvengeMedia/dgop
|
||||||
|
Provides: dgop
|
||||||
|
|
||||||
|
%description -n dgop
|
||||||
|
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
|
||||||
|
network statistics. Designed for integration with DankMaterialShell but can be
|
||||||
|
used standalone. This package always includes the latest stable dgop release.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup -q -c -n dms-qml
|
||||||
|
|
||||||
|
# Download architecture-specific binaries during build
|
||||||
|
case "%{_arch}" in
|
||||||
|
x86_64)
|
||||||
|
ARCH_SUFFIX="amd64"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
ARCH_SUFFIX="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: %{_arch}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||||
|
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
||||||
|
chmod +x %{_builddir}/dms-cli
|
||||||
|
|
||||||
|
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
|
||||||
|
echo "Failed to download dgop for architecture %{_arch}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
|
||||||
|
chmod +x %{_builddir}/dgop
|
||||||
|
|
||||||
|
%build
|
||||||
|
|
||||||
|
%install
|
||||||
|
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
|
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
||||||
|
|
||||||
|
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||||
|
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||||
|
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||||
|
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||||
|
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||||
|
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||||
|
|
||||||
|
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
|
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||||
|
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||||
|
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
|
%posttrans
|
||||||
|
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
|
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
|
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||||
|
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" -ge 2 ]; then
|
||||||
|
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
%files
|
||||||
|
%license LICENSE
|
||||||
|
%doc README.md CONTRIBUTING.md
|
||||||
|
%{_datadir}/quickshell/dms/
|
||||||
|
%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
%files -n dms-cli
|
||||||
|
%{_bindir}/dms
|
||||||
|
%{_datadir}/bash-completion/completions/dms
|
||||||
|
%{_datadir}/zsh/site-functions/_dms
|
||||||
|
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
|
%files -n dgop
|
||||||
|
%{_bindir}/dgop
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
||||||
|
- Stable release VERSION_PLACEHOLDER
|
||||||
|
- Built from GitHub release
|
||||||
|
- Includes latest dms-cli and dgop binaries
|
||||||
|
SPECEOF
|
||||||
|
|
||||||
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
|
||||||
|
- name: Build SRPM
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
cd ~/rpmbuild/SPECS
|
||||||
|
rpmbuild -bs dms.spec
|
||||||
|
|
||||||
|
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
||||||
|
SRPM_NAME=$(basename "$SRPM")
|
||||||
|
|
||||||
|
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||||
|
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||||
|
echo "SRPM built: $SRPM_NAME"
|
||||||
|
|
||||||
|
- name: Upload SRPM artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
- name: Install Copr CLI
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y python3-pip
|
||||||
|
pip3 install copr-cli
|
||||||
|
|
||||||
|
mkdir -p ~/.config
|
||||||
|
cat > ~/.config/copr << EOF
|
||||||
|
[copr-cli]
|
||||||
|
login = ${{ secrets.COPR_LOGIN }}
|
||||||
|
username = avengemedia
|
||||||
|
token = ${{ secrets.COPR_TOKEN }}
|
||||||
|
copr_url = https://copr.fedorainfracloud.org
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/copr
|
||||||
|
|
||||||
|
- name: Upload to Copr
|
||||||
|
run: |
|
||||||
|
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
echo "Uploading SRPM to avengemedia/dms..."
|
||||||
|
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||||
|
echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
|
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
|
if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
|
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
name: DMS Copr Stable Release
|
name: DMS Copr Stable Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
|
||||||
workflows: ["Create Release from DMS"]
|
|
||||||
types: [completed]
|
|
||||||
branches: [master]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
release:
|
||||||
|
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
|
||||||
|
required: false
|
||||||
|
default: '1'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-upload:
|
build-and-upload:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -24,19 +23,23 @@ jobs:
|
|||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
|
# Get version from manual input or latest release
|
||||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
echo "Using manual version: $VERSION"
|
echo "Using manual version: $VERSION"
|
||||||
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
|
|
||||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
|
||||||
echo "Using latest release version from workflow_run: $VERSION"
|
|
||||||
else
|
else
|
||||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
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"
|
echo "Using latest release version: $VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
RELEASE="${{ github.event.inputs.release }}"
|
||||||
|
if [ -z "$RELEASE" ]; then
|
||||||
|
RELEASE="1"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "✅ Building DMS stable version: $VERSION"
|
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||||
|
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
|
||||||
|
|
||||||
- name: Setup build environment
|
- name: Setup build environment
|
||||||
run: |
|
run: |
|
||||||
@@ -65,6 +68,7 @@ jobs:
|
|||||||
- name: Generate stable spec file
|
- name: Generate stable spec file
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
RELEASE="${{ steps.version.outputs.release }}"
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
|
|
||||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||||
@@ -76,10 +80,10 @@ jobs:
|
|||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Version: %{version}
|
Version: %{version}
|
||||||
Release: 1%{?dist}
|
Release: RELEASE_PLACEHOLDER%{?dist}
|
||||||
Summary: %{pkg_summary}
|
Summary: %{pkg_summary}
|
||||||
|
|
||||||
License: GPL-3.0-only
|
License: MIT
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
Source0: dms-qml.tar.gz
|
||||||
@@ -114,8 +118,8 @@ jobs:
|
|||||||
|
|
||||||
%package -n dms-cli
|
%package -n dms-cli
|
||||||
Summary: DankMaterialShell CLI tool
|
Summary: DankMaterialShell CLI tool
|
||||||
License: GPL-3.0-only
|
License: MIT
|
||||||
URL: https://github.com/AvengeMedia/danklinux
|
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
%description -n dms-cli
|
%description -n dms-cli
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
Command-line interface for DankMaterialShell configuration and management.
|
||||||
@@ -151,7 +155,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
# Download dms-cli for target architecture
|
# Download dms-cli for target architecture
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/danklinux/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -172,6 +176,14 @@ jobs:
|
|||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
||||||
|
|
||||||
|
# Shell completions
|
||||||
|
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||||
|
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||||
|
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||||
|
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||||
|
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||||
|
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
@@ -180,7 +192,7 @@ jobs:
|
|||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/*.spec
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
%posttrans
|
%posttrans
|
||||||
# Clean up old installation path from previous versions (only if empty)
|
# Clean up old installation path from previous versions (only if empty)
|
||||||
@@ -204,21 +216,25 @@ jobs:
|
|||||||
|
|
||||||
%files -n dms-cli
|
%files -n dms-cli
|
||||||
%{_bindir}/dms
|
%{_bindir}/dms
|
||||||
|
%{_datadir}/bash-completion/completions/dms
|
||||||
|
%{_datadir}/zsh/site-functions/_dms
|
||||||
|
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
%files -n dgop
|
%files -n dgop
|
||||||
%{_bindir}/dgop
|
%{_bindir}/dgop
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
||||||
- Stable release VERSION_PLACEHOLDER
|
- Stable release VERSION_PLACEHOLDER
|
||||||
- Built from GitHub release
|
- Built from GitHub release
|
||||||
- Includes latest dms-cli and dgop binaries
|
- Includes latest dms-cli and dgop binaries
|
||||||
SPECEOF
|
SPECEOF
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
|
||||||
echo "✅ Spec file generated for v${VERSION}"
|
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Spec file preview ==="
|
echo "=== Spec file preview ==="
|
||||||
head -40 ~/rpmbuild/SPECS/dms.spec
|
head -40 ~/rpmbuild/SPECS/dms.spec
|
||||||
@@ -292,7 +308,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Version:** ${{ steps.version.outputs.version }}" >> $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 "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
name: Update OBS Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to update (dms, dms-git, or all)'
|
||||||
|
required: false
|
||||||
|
default: 'all'
|
||||||
|
rebuild_release:
|
||||||
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-obs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine packages to update
|
||||||
|
id: packages
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by tag: $VERSION"
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by schedule: updating git package"
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=all" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update version in packaging files
|
||||||
|
if: steps.packages.outputs.version != ''
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
|
VERSION_NO_V="${VERSION#v}"
|
||||||
|
echo "Updating packaging to version $VERSION_NO_V"
|
||||||
|
|
||||||
|
# Update openSUSE spec files
|
||||||
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/*.spec
|
||||||
|
|
||||||
|
# Update Debian _service files
|
||||||
|
for service in distro/debian/*/_service; do
|
||||||
|
if [[ -f "$service" ]]; then
|
||||||
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add distro/
|
||||||
|
git commit -m "chore: update packaging to $VERSION" || echo "No changes to commit"
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Upload to OBS
|
||||||
|
env:
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
MESSAGE="Automated update from GitHub Actions"
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd distro
|
||||||
|
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
bash scripts/obs-upload.sh dms "$MESSAGE"
|
||||||
|
bash scripts/obs-upload.sh dms-git "Automated git update"
|
||||||
|
else
|
||||||
|
bash scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
name: Update PPA Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to upload (dms, dms-git, or all)'
|
||||||
|
required: false
|
||||||
|
default: 'dms-git'
|
||||||
|
rebuild_release:
|
||||||
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload-ppa:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
debhelper \
|
||||||
|
devscripts \
|
||||||
|
dput \
|
||||||
|
lftp \
|
||||||
|
build-essential \
|
||||||
|
fakeroot \
|
||||||
|
dpkg-dev
|
||||||
|
|
||||||
|
- name: Configure GPG
|
||||||
|
env:
|
||||||
|
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "$GPG_KEY" | gpg --import
|
||||||
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Determine packages to upload
|
||||||
|
id: packages
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by schedule: uploading git package"
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload to PPA
|
||||||
|
env:
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
|
||||||
|
cd distro/ubuntu/ppa
|
||||||
|
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms to PPA..."
|
||||||
|
if [ -n "$REBUILD_RELEASE" ]; then
|
||||||
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash create-and-upload.sh "../dms" dms questing
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-git to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash create-and-upload.sh "../dms-git" dms-git questing
|
||||||
|
else
|
||||||
|
PPA_NAME="$PACKAGES"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading $PACKAGES to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash create-and-upload.sh "../$PACKAGES" "$PPA_NAME" questing
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
name: Update Vendor Hash
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "core/go.mod"
|
||||||
|
- "core/go.sum"
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-vendor-hash:
|
||||||
|
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: Update vendorHash in flake.nix
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Try to build and capture the expected hash from error message
|
||||||
|
echo "Attempting nix build to get new vendorHash..."
|
||||||
|
|
||||||
|
if output=$(nix build .#dmsCli 2>&1); then
|
||||||
|
echo "Build succeeded, no hash update needed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the expected hash from the error message
|
||||||
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
||||||
|
|
||||||
|
if [ -z "$new_hash" ]; then
|
||||||
|
echo "Could not extract new vendorHash from build output"
|
||||||
|
echo "Build output:"
|
||||||
|
echo "$output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "New vendorHash: $new_hash"
|
||||||
|
|
||||||
|
# Get current hash from flake.nix
|
||||||
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
|
echo "Current vendorHash: $current_hash"
|
||||||
|
|
||||||
|
if [ "$current_hash" = "$new_hash" ]; then
|
||||||
|
echo "vendorHash is already up to date"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update the hash in flake.nix
|
||||||
|
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||||
|
|
||||||
|
# Verify the build works with the new hash
|
||||||
|
echo "Verifying build with new vendorHash..."
|
||||||
|
nix build .#dmsCli
|
||||||
|
|
||||||
|
echo "vendorHash updated successfully!"
|
||||||
|
|
||||||
|
- name: Commit and push vendorHash update
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! git diff --quiet flake.nix; then
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
git add flake.nix
|
||||||
|
git commit -m "nix: update vendorHash for go.mod changes"
|
||||||
|
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if git push; then
|
||||||
|
echo "Successfully pushed vendorHash update"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Push attempt $attempt failed, pulling and retrying..."
|
||||||
|
git pull --rebase
|
||||||
|
sleep $((attempt*2))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Failed to push after retries" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No changes to flake.nix"
|
||||||
|
fi
|
||||||
+42
-2
@@ -27,7 +27,6 @@ qrc_*.cpp
|
|||||||
ui_*.h
|
ui_*.h
|
||||||
*.qmlc
|
*.qmlc
|
||||||
*.jsc
|
*.jsc
|
||||||
Makefile*
|
|
||||||
*build-*
|
*build-*
|
||||||
*.qm
|
*.qm
|
||||||
*.prl
|
*.prl
|
||||||
@@ -101,4 +100,45 @@ go.work.sum
|
|||||||
|
|
||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Code coverage profiles and other test artifacts
|
||||||
|
*.out
|
||||||
|
coverage.*
|
||||||
|
*.coverprofile
|
||||||
|
profile.cov
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Editor/IDE
|
||||||
|
# .idea/
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# Extracted source trees in Ubuntu package directories
|
||||||
|
distro/ubuntu/*/dms-git-repo/
|
||||||
|
distro/ubuntu/*/DankMaterialShell-*/
|
||||||
|
distro/ubuntu/danklinux/*/dsearch-*/
|
||||||
|
distro/ubuntu/danklinux/*/dgop-*/
|
||||||
|
|||||||
+29
-15
@@ -2,28 +2,42 @@
|
|||||||
|
|
||||||
Contributions are welcome and encouraged.
|
Contributions are welcome and encouraged.
|
||||||
|
|
||||||
## Formatting
|
To contribute fork this repository, make your changes, and open a pull request.
|
||||||
|
|
||||||
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported.
|
## VSCode Setup
|
||||||
|
|
||||||
We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
|
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.
|
||||||
|
|
||||||
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
|
### QML (`quickshell` directory)
|
||||||
|
|
||||||
|
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
|
||||||
|
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"customLocalFormatters.formatters": [
|
{
|
||||||
{
|
"qt-qml.doNotAskForQmllsDownload": true,
|
||||||
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
|
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
|
||||||
"languages": ["qml"]
|
}
|
||||||
}
|
|
||||||
],
|
|
||||||
"[qml]": {
|
|
||||||
"editor.defaultFormatter": "jkillian.custom-local-formatters",
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
|
3. Create empty `.qmlls.ini` file in `quickshell/` directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd quickshell
|
||||||
|
touch .qmlls.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
|
### GO (`core` directory)
|
||||||
|
|
||||||
|
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||||
|
2. Ensure code is formatted with `make fmt`
|
||||||
|
3. Add appropriate test coverage and ensure tests pass with `make test`
|
||||||
|
4. Run `go mod tidy`
|
||||||
|
5. Open pull request
|
||||||
|
|
||||||
## Pull request
|
## Pull request
|
||||||
|
|
||||||
|
|||||||
@@ -1,941 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Common.settings
|
|
||||||
import qs.Services
|
|
||||||
import "settings/SettingsSpec.js" as Spec
|
|
||||||
import "settings/SettingsStore.js" as Store
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property int settingsConfigVersion: 1
|
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
|
|
||||||
enum Position {
|
|
||||||
Top,
|
|
||||||
Bottom,
|
|
||||||
Left,
|
|
||||||
Right
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AnimationSpeed {
|
|
||||||
None,
|
|
||||||
Short,
|
|
||||||
Medium,
|
|
||||||
Long,
|
|
||||||
Custom
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SuspendBehavior {
|
|
||||||
Suspend,
|
|
||||||
Hibernate,
|
|
||||||
SuspendThenHibernate
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property string defaultFontFamily: "Inter Variable"
|
|
||||||
readonly property string defaultMonoFontFamily: "Fira Code"
|
|
||||||
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
|
||||||
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
|
||||||
readonly property string _configDir: Paths.strip(_configUrl)
|
|
||||||
readonly property string pluginSettingsPath: _configDir + "/DankMaterialShell/plugin_settings.json"
|
|
||||||
|
|
||||||
property bool _loading: false
|
|
||||||
property bool _pluginSettingsLoading: false
|
|
||||||
property bool hasTriedDefaultSettings: false
|
|
||||||
property var pluginSettings: ({})
|
|
||||||
|
|
||||||
property alias dankBarLeftWidgetsModel: leftWidgetsModel
|
|
||||||
property alias dankBarCenterWidgetsModel: centerWidgetsModel
|
|
||||||
property alias dankBarRightWidgetsModel: rightWidgetsModel
|
|
||||||
|
|
||||||
property string currentThemeName: "blue"
|
|
||||||
property string customThemeFile: ""
|
|
||||||
property string matugenScheme: "scheme-tonal-spot"
|
|
||||||
property bool runUserMatugenTemplates: true
|
|
||||||
property string matugenTargetMonitor: ""
|
|
||||||
property real dankBarTransparency: 1.0
|
|
||||||
property real dankBarWidgetTransparency: 1.0
|
|
||||||
property real popupTransparency: 1.0
|
|
||||||
property real dockTransparency: 1
|
|
||||||
property string widgetBackgroundColor: "sch"
|
|
||||||
property real cornerRadius: 12
|
|
||||||
|
|
||||||
property bool use24HourClock: true
|
|
||||||
property bool showSeconds: false
|
|
||||||
property bool useFahrenheit: false
|
|
||||||
property bool nightModeEnabled: false
|
|
||||||
property int animationSpeed: SettingsData.AnimationSpeed.Short
|
|
||||||
property int customAnimationDuration: 500
|
|
||||||
property string wallpaperFillMode: "Fill"
|
|
||||||
property bool blurredWallpaperLayer: false
|
|
||||||
property bool blurWallpaperOnOverview: false
|
|
||||||
|
|
||||||
property bool showLauncherButton: true
|
|
||||||
property bool showWorkspaceSwitcher: true
|
|
||||||
property bool showFocusedWindow: true
|
|
||||||
property bool showWeather: true
|
|
||||||
property bool showMusic: true
|
|
||||||
property bool showClipboard: true
|
|
||||||
property bool showCpuUsage: true
|
|
||||||
property bool showMemUsage: true
|
|
||||||
property bool showCpuTemp: true
|
|
||||||
property bool showGpuTemp: true
|
|
||||||
property int selectedGpuIndex: 0
|
|
||||||
property var enabledGpuPciIds: []
|
|
||||||
property bool showSystemTray: true
|
|
||||||
property bool showClock: true
|
|
||||||
property bool showNotificationButton: true
|
|
||||||
property bool showBattery: true
|
|
||||||
property bool showControlCenterButton: true
|
|
||||||
|
|
||||||
property bool controlCenterShowNetworkIcon: true
|
|
||||||
property bool controlCenterShowBluetoothIcon: true
|
|
||||||
property bool controlCenterShowAudioIcon: true
|
|
||||||
property var controlCenterWidgets: [{
|
|
||||||
"id": "volumeSlider",
|
|
||||||
"enabled": true,
|
|
||||||
"width": 50
|
|
||||||
}, {
|
|
||||||
"id": "brightnessSlider",
|
|
||||||
"enabled": true,
|
|
||||||
"width": 50
|
|
||||||
}, {
|
|
||||||
"id": "wifi",
|
|
||||||
"enabled": true,
|
|
||||||
"width": 50
|
|
||||||
}, {
|
|
||||||
"id": "bluetooth",
|
|
||||||
"enabled": true,
|
|
||||||
"width": 50
|
|
||||||
}, {
|
|
||||||
"id": "audioOutput",
|
|
||||||
"enabled": true,
|
|
||||||
"width": 50
|
|
||||||
}, {
|
|
||||||
"id": "audioInput",
|
|
||||||
"enabled": true,
|
|
||||||
"width": 50
|
|
||||||
}, {
|
|
||||||
"id": "nightMode",
|
|
||||||
"enabled": true,
|
|
||||||
"width": 50
|
|
||||||
}, {
|
|
||||||
"id": "darkMode",
|
|
||||||
"enabled": true,
|
|
||||||
"width": 50
|
|
||||||
}]
|
|
||||||
|
|
||||||
property bool showWorkspaceIndex: false
|
|
||||||
property bool showWorkspacePadding: false
|
|
||||||
property bool workspaceScrolling: false
|
|
||||||
property bool showWorkspaceApps: false
|
|
||||||
property int maxWorkspaceIcons: 3
|
|
||||||
property bool workspacesPerMonitor: true
|
|
||||||
property bool dwlShowAllTags: false
|
|
||||||
property var workspaceNameIcons: ({})
|
|
||||||
property bool waveProgressEnabled: true
|
|
||||||
property bool clockCompactMode: false
|
|
||||||
property bool focusedWindowCompactMode: false
|
|
||||||
property bool runningAppsCompactMode: true
|
|
||||||
property bool keyboardLayoutNameCompactMode: false
|
|
||||||
property bool runningAppsCurrentWorkspace: false
|
|
||||||
property bool runningAppsGroupByApp: false
|
|
||||||
property string clockDateFormat: ""
|
|
||||||
property string lockDateFormat: ""
|
|
||||||
property int mediaSize: 1
|
|
||||||
|
|
||||||
property var dankBarLeftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
|
||||||
property var dankBarCenterWidgets: ["music", "clock", "weather"]
|
|
||||||
property var dankBarRightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"]
|
|
||||||
property var dankBarWidgetOrder: []
|
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
|
||||||
property string spotlightModalViewMode: "list"
|
|
||||||
property bool sortAppsAlphabetically: false
|
|
||||||
|
|
||||||
property string weatherLocation: "New York, NY"
|
|
||||||
property string weatherCoordinates: "40.7128,-74.0060"
|
|
||||||
property bool useAutoLocation: false
|
|
||||||
property bool weatherEnabled: true
|
|
||||||
|
|
||||||
property string networkPreference: "auto"
|
|
||||||
property string vpnLastConnected: ""
|
|
||||||
|
|
||||||
property string iconTheme: "System Default"
|
|
||||||
property var availableIconThemes: ["System Default"]
|
|
||||||
property string systemDefaultIconTheme: ""
|
|
||||||
property bool qt5ctAvailable: false
|
|
||||||
property bool qt6ctAvailable: false
|
|
||||||
property bool gtkAvailable: false
|
|
||||||
|
|
||||||
property string launcherLogoMode: "apps"
|
|
||||||
property string launcherLogoCustomPath: ""
|
|
||||||
property string launcherLogoColorOverride: ""
|
|
||||||
property bool launcherLogoColorInvertOnMode: false
|
|
||||||
property real launcherLogoBrightness: 0.5
|
|
||||||
property real launcherLogoContrast: 1
|
|
||||||
property int launcherLogoSizeOffset: 0
|
|
||||||
|
|
||||||
property string fontFamily: "Inter Variable"
|
|
||||||
property string monoFontFamily: "Fira Code"
|
|
||||||
property int fontWeight: Font.Normal
|
|
||||||
property real fontScale: 1.0
|
|
||||||
property real dankBarFontScale: 1.0
|
|
||||||
|
|
||||||
property bool notepadUseMonospace: true
|
|
||||||
property string notepadFontFamily: ""
|
|
||||||
property real notepadFontSize: 14
|
|
||||||
property bool notepadShowLineNumbers: false
|
|
||||||
property real notepadTransparencyOverride: -1
|
|
||||||
property real notepadLastCustomTransparency: 0.7
|
|
||||||
|
|
||||||
onNotepadUseMonospaceChanged: saveSettings()
|
|
||||||
onNotepadFontFamilyChanged: saveSettings()
|
|
||||||
onNotepadFontSizeChanged: saveSettings()
|
|
||||||
onNotepadShowLineNumbersChanged: saveSettings()
|
|
||||||
onNotepadTransparencyOverrideChanged: {
|
|
||||||
if (notepadTransparencyOverride > 0) {
|
|
||||||
notepadLastCustomTransparency = notepadTransparencyOverride
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
onNotepadLastCustomTransparencyChanged: saveSettings()
|
|
||||||
|
|
||||||
property bool soundsEnabled: true
|
|
||||||
property bool useSystemSoundTheme: false
|
|
||||||
property bool soundNewNotification: true
|
|
||||||
property bool soundVolumeChanged: true
|
|
||||||
property bool soundPluggedIn: true
|
|
||||||
|
|
||||||
property int acMonitorTimeout: 0
|
|
||||||
property int acLockTimeout: 0
|
|
||||||
property int acSuspendTimeout: 0
|
|
||||||
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
|
||||||
property int batteryMonitorTimeout: 0
|
|
||||||
property int batteryLockTimeout: 0
|
|
||||||
property int batterySuspendTimeout: 0
|
|
||||||
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
|
||||||
property bool lockBeforeSuspend: false
|
|
||||||
property bool preventIdleForMedia: false
|
|
||||||
property bool loginctlLockIntegration: true
|
|
||||||
property string launchPrefix: ""
|
|
||||||
property var brightnessDevicePins: ({})
|
|
||||||
|
|
||||||
property bool gtkThemingEnabled: false
|
|
||||||
property bool qtThemingEnabled: false
|
|
||||||
property bool syncModeWithPortal: true
|
|
||||||
|
|
||||||
property bool showDock: false
|
|
||||||
property bool dockAutoHide: false
|
|
||||||
property bool dockGroupByApp: false
|
|
||||||
property bool dockOpenOnOverview: false
|
|
||||||
property int dockPosition: SettingsData.Position.Bottom
|
|
||||||
property real dockSpacing: 4
|
|
||||||
property real dockBottomGap: 0
|
|
||||||
property real dockMargin: 0
|
|
||||||
property real dockIconSize: 40
|
|
||||||
property string dockIndicatorStyle: "circle"
|
|
||||||
|
|
||||||
property bool notificationOverlayEnabled: false
|
|
||||||
property bool dankBarAutoHide: false
|
|
||||||
property bool dankBarOpenOnOverview: false
|
|
||||||
property bool dankBarVisible: true
|
|
||||||
property int overviewRows: 2
|
|
||||||
property int overviewColumns: 5
|
|
||||||
property real overviewScale: 0.16
|
|
||||||
property real dankBarSpacing: 4
|
|
||||||
property real dankBarBottomGap: 0
|
|
||||||
property real dankBarInnerPadding: 4
|
|
||||||
property int dankBarPosition: SettingsData.Position.Top
|
|
||||||
property bool dankBarIsVertical: dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right
|
|
||||||
|
|
||||||
property bool dankBarSquareCorners: false
|
|
||||||
property bool dankBarNoBackground: false
|
|
||||||
property bool dankBarGothCornersEnabled: false
|
|
||||||
property bool dankBarGothCornerRadiusOverride: false
|
|
||||||
property real dankBarGothCornerRadiusValue: 12
|
|
||||||
property bool dankBarBorderEnabled: false
|
|
||||||
property string dankBarBorderColor: "surfaceText"
|
|
||||||
property real dankBarBorderOpacity: 1.0
|
|
||||||
property real dankBarBorderThickness: 1
|
|
||||||
|
|
||||||
onDankBarGothCornerRadiusOverrideChanged: saveSettings()
|
|
||||||
onDankBarGothCornerRadiusValueChanged: saveSettings()
|
|
||||||
onDankBarBorderColorChanged: saveSettings()
|
|
||||||
onDankBarBorderOpacityChanged: saveSettings()
|
|
||||||
onDankBarBorderThicknessChanged: saveSettings()
|
|
||||||
|
|
||||||
property bool popupGapsAuto: true
|
|
||||||
property int popupGapsManual: 4
|
|
||||||
|
|
||||||
property bool modalDarkenBackground: true
|
|
||||||
|
|
||||||
property bool lockScreenShowPowerActions: true
|
|
||||||
property bool enableFprint: false
|
|
||||||
property int maxFprintTries: 3
|
|
||||||
property bool fprintdAvailable: false
|
|
||||||
property bool hideBrightnessSlider: false
|
|
||||||
|
|
||||||
property int notificationTimeoutLow: 5000
|
|
||||||
property int notificationTimeoutNormal: 5000
|
|
||||||
property int notificationTimeoutCritical: 0
|
|
||||||
property int notificationPopupPosition: SettingsData.Position.Top
|
|
||||||
|
|
||||||
property bool osdAlwaysShowValue: false
|
|
||||||
|
|
||||||
property bool powerActionConfirm: true
|
|
||||||
property string customPowerActionLock: ""
|
|
||||||
property string customPowerActionLogout: ""
|
|
||||||
property string customPowerActionSuspend: ""
|
|
||||||
property string customPowerActionHibernate: ""
|
|
||||||
property string customPowerActionReboot: ""
|
|
||||||
property string customPowerActionPowerOff: ""
|
|
||||||
|
|
||||||
property bool updaterUseCustomCommand: false
|
|
||||||
property string updaterCustomCommand: ""
|
|
||||||
property string updaterTerminalAdditionalParams: ""
|
|
||||||
|
|
||||||
property var screenPreferences: ({})
|
|
||||||
property var showOnLastDisplay: ({})
|
|
||||||
|
|
||||||
signal forceDankBarLayoutRefresh
|
|
||||||
signal forceDockLayoutRefresh
|
|
||||||
signal widgetDataChanged
|
|
||||||
signal workspaceIconsUpdated
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
Processes.settingsRoot = root
|
|
||||||
loadSettings()
|
|
||||||
initializeListModels()
|
|
||||||
Processes.detectFprintd()
|
|
||||||
Processes.checkPluginSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyStoredTheme() {
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.switchTheme(currentThemeName, false, false)
|
|
||||||
} else {
|
|
||||||
Qt.callLater(function() {
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.switchTheme(currentThemeName, false, false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function regenSystemThemes() {
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateNiriLayout() {
|
|
||||||
if (typeof NiriService !== "undefined" && typeof CompositorService !== "undefined" && CompositorService.isNiri) {
|
|
||||||
NiriService.generateNiriLayoutConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyStoredIconTheme() {
|
|
||||||
updateGtkIconTheme()
|
|
||||||
updateQtIconTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateGtkIconTheme() {
|
|
||||||
const gtkThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme
|
|
||||||
if (gtkThemeName === "System Default" || gtkThemeName === "") return
|
|
||||||
|
|
||||||
if (typeof DMSService !== "undefined" && DMSService.apiVersion >= 3 && typeof PortalService !== "undefined") {
|
|
||||||
PortalService.setSystemIconTheme(gtkThemeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
const configScript = `mkdir -p ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0
|
|
||||||
|
|
||||||
for config_dir in ${_configDir}/gtk-3.0 ${_configDir}/gtk-4.0; do
|
|
||||||
settings_file="$config_dir/settings.ini"
|
|
||||||
if [ -f "$settings_file" ]; then
|
|
||||||
if grep -q "^gtk-icon-theme-name=" "$settings_file"; then
|
|
||||||
sed -i 's/^gtk-icon-theme-name=.*/gtk-icon-theme-name=${gtkThemeName}/' "$settings_file"
|
|
||||||
else
|
|
||||||
if grep -q "\\[Settings\\]" "$settings_file"; then
|
|
||||||
sed -i '/\\[Settings\\]/a gtk-icon-theme-name=${gtkThemeName}' "$settings_file"
|
|
||||||
else
|
|
||||||
echo -e '\\n[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' >> "$settings_file"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e '[Settings]\\ngtk-icon-theme-name=${gtkThemeName}' > "$settings_file"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true
|
|
||||||
pkill -HUP -f 'gtk' 2>/dev/null || true`
|
|
||||||
|
|
||||||
Quickshell.execDetached(["sh", "-lc", configScript])
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateQtIconTheme() {
|
|
||||||
const qtThemeName = (iconTheme === "System Default") ? "" : iconTheme
|
|
||||||
if (!qtThemeName) return
|
|
||||||
|
|
||||||
const home = _homeUrl.replace("file://", "").replace(/'/g, "'\\''")
|
|
||||||
const qtThemeNameEscaped = qtThemeName.replace(/'/g, "'\\''")
|
|
||||||
|
|
||||||
const script = `mkdir -p ${_configDir}/qt5ct ${_configDir}/qt6ct ${_configDir}/environment.d 2>/dev/null || true
|
|
||||||
update_qt_icon_theme() {
|
|
||||||
local config_file="$1"
|
|
||||||
local theme_name="$2"
|
|
||||||
if [ -f "$config_file" ]; then
|
|
||||||
if grep -q "^\\[Appearance\\]" "$config_file"; then
|
|
||||||
if grep -q "^icon_theme=" "$config_file"; then
|
|
||||||
sed -i "s/^icon_theme=.*/icon_theme=$theme_name/" "$config_file"
|
|
||||||
else
|
|
||||||
sed -i "/^\\[Appearance\\]/a icon_theme=$theme_name" "$config_file"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "\\n[Appearance]\\nicon_theme=%s\\n" "$theme_name" >> "$config_file"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "[Appearance]\\nicon_theme=%s\\n" "$theme_name" > "$config_file"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
|
|
||||||
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'
|
|
||||||
rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`
|
|
||||||
|
|
||||||
Quickshell.execDetached(["sh", "-lc", script])
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var _hooks: ({
|
|
||||||
applyStoredTheme: applyStoredTheme,
|
|
||||||
regenSystemThemes: regenSystemThemes,
|
|
||||||
updateNiriLayout: updateNiriLayout,
|
|
||||||
applyStoredIconTheme: applyStoredIconTheme
|
|
||||||
})
|
|
||||||
|
|
||||||
function set(key, value) {
|
|
||||||
Spec.set(root, key, value, saveSettings, _hooks)
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSettings() {
|
|
||||||
_loading = true
|
|
||||||
try {
|
|
||||||
const txt = settingsFile.text()
|
|
||||||
const obj = (txt && txt.trim()) ? JSON.parse(txt) : null
|
|
||||||
Store.parse(root, obj)
|
|
||||||
const shouldMigrate = Store.migrate(root, obj)
|
|
||||||
applyStoredTheme()
|
|
||||||
applyStoredIconTheme()
|
|
||||||
Processes.detectIcons()
|
|
||||||
Processes.detectQtTools()
|
|
||||||
if (obj && obj.configVersion === undefined) {
|
|
||||||
const cleaned = Store.cleanup(txt)
|
|
||||||
if (cleaned) {
|
|
||||||
settingsFile.setText(cleaned)
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
if (shouldMigrate) {
|
|
||||||
savePluginSettings()
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("SettingsData: Failed to load settings:", e.message)
|
|
||||||
applyStoredTheme()
|
|
||||||
applyStoredIconTheme()
|
|
||||||
} finally {
|
|
||||||
_loading = false
|
|
||||||
}
|
|
||||||
loadPluginSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPluginSettings() {
|
|
||||||
_pluginSettingsLoading = true
|
|
||||||
parsePluginSettings(pluginSettingsFile.text())
|
|
||||||
_pluginSettingsLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePluginSettings(content) {
|
|
||||||
_pluginSettingsLoading = true
|
|
||||||
try {
|
|
||||||
if (content && content.trim()) {
|
|
||||||
pluginSettings = JSON.parse(content)
|
|
||||||
} else {
|
|
||||||
pluginSettings = {}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("SettingsData: Failed to parse plugin settings:", e.message)
|
|
||||||
pluginSettings = {}
|
|
||||||
} finally {
|
|
||||||
_pluginSettingsLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSettings() {
|
|
||||||
if (_loading) return
|
|
||||||
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePluginSettings() {
|
|
||||||
if (_pluginSettingsLoading) return
|
|
||||||
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectAvailableIconThemes() {
|
|
||||||
Processes.detectIcons()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
|
||||||
if (use24HourClock) {
|
|
||||||
return showSeconds ? "hh:mm:ss" : "hh:mm"
|
|
||||||
} else {
|
|
||||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEffectiveClockDateFormat() {
|
|
||||||
return clockDateFormat && clockDateFormat.length > 0 ? clockDateFormat : "ddd d"
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEffectiveLockDateFormat() {
|
|
||||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeListModels() {
|
|
||||||
Lists.init(leftWidgetsModel, centerWidgetsModel, rightWidgetsModel, dankBarLeftWidgets, dankBarCenterWidgets, dankBarRightWidgets)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateListModel(listModel, order) {
|
|
||||||
Lists.update(listModel, order)
|
|
||||||
widgetDataChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasNamedWorkspaces() {
|
|
||||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri) return false
|
|
||||||
|
|
||||||
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
|
|
||||||
var ws = NiriService.allWorkspaces[i]
|
|
||||||
if (ws.name && ws.name.trim() !== "") return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNamedWorkspaces() {
|
|
||||||
var namedWorkspaces = []
|
|
||||||
if (typeof NiriService === "undefined" || !CompositorService.isNiri) return namedWorkspaces
|
|
||||||
|
|
||||||
for (const ws of NiriService.allWorkspaces) {
|
|
||||||
if (ws.name && ws.name.trim() !== "") {
|
|
||||||
namedWorkspaces.push(ws.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return namedWorkspaces
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPopupYPosition(barHeight) {
|
|
||||||
const gothOffset = dankBarGothCornersEnabled ? Theme.cornerRadius : 0
|
|
||||||
return barHeight + dankBarSpacing + dankBarBottomGap - gothOffset + Theme.popupDistance
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPopupTriggerPosition(globalPos, screen, barThickness, widgetWidth) {
|
|
||||||
const screenX = screen ? screen.x : 0
|
|
||||||
const screenY = screen ? screen.y : 0
|
|
||||||
const relativeX = globalPos.x - screenX
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
|
|
||||||
if (dankBarPosition === SettingsData.Position.Left || dankBarPosition === SettingsData.Position.Right) {
|
|
||||||
return {
|
|
||||||
"x": relativeY,
|
|
||||||
"y": barThickness + dankBarSpacing + Theme.popupDistance,
|
|
||||||
"width": widgetWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"x": relativeX,
|
|
||||||
"y": barThickness + dankBarSpacing + Theme.popupDistance,
|
|
||||||
"width": widgetWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilteredScreens(componentId) {
|
|
||||||
var prefs = screenPreferences && screenPreferences[componentId] || ["all"]
|
|
||||||
if (prefs.includes("all")) {
|
|
||||||
return Quickshell.screens
|
|
||||||
}
|
|
||||||
var filtered = Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
|
||||||
if (filtered.length === 0 && showOnLastDisplay && showOnLastDisplay[componentId] && Quickshell.screens.length === 1) {
|
|
||||||
return Quickshell.screens
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendTestNotifications() {
|
|
||||||
sendTestNotification(0)
|
|
||||||
testNotifTimer1.start()
|
|
||||||
testNotifTimer2.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendTestNotification(index) {
|
|
||||||
const notifications = [["Notification Position Test", "DMS test notification 1 of 3 ~ Hi there!", "preferences-system"], ["Second Test", "DMS Notification 2 of 3 ~ Check it out!", "applications-graphics"], ["Third Test", "DMS notification 3 of 3 ~ Enjoy!", "face-smile"]]
|
|
||||||
|
|
||||||
if (index < 0 || index >= notifications.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const notif = notifications[index]
|
|
||||||
testNotificationProcess.command = ["notify-send", "-h", "int:transient:1", "-a", "DMS", "-i", notif[2], notif[0], notif[1]]
|
|
||||||
testNotificationProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function setMatugenScheme(scheme) {
|
|
||||||
var normalized = scheme || "scheme-tonal-spot"
|
|
||||||
if (matugenScheme === normalized) return
|
|
||||||
set("matugenScheme", normalized)
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRunUserMatugenTemplates(enabled) {
|
|
||||||
if (runUserMatugenTemplates === enabled) return
|
|
||||||
set("runUserMatugenTemplates", enabled)
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMatugenTargetMonitor(monitorName) {
|
|
||||||
if (matugenTargetMonitor === monitorName) return
|
|
||||||
set("matugenTargetMonitor", monitorName)
|
|
||||||
if (typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function setCornerRadius(radius) {
|
|
||||||
set("cornerRadius", radius)
|
|
||||||
NiriService.generateNiriLayoutConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWeatherLocation(displayName, coordinates) {
|
|
||||||
weatherLocation = displayName
|
|
||||||
weatherCoordinates = coordinates
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setIconTheme(themeName) {
|
|
||||||
iconTheme = themeName
|
|
||||||
updateGtkIconTheme()
|
|
||||||
updateQtIconTheme()
|
|
||||||
saveSettings()
|
|
||||||
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGtkThemingEnabled(enabled) {
|
|
||||||
set("gtkThemingEnabled", enabled)
|
|
||||||
if (enabled && typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setQtThemingEnabled(enabled) {
|
|
||||||
set("qtThemingEnabled", enabled)
|
|
||||||
if (enabled && typeof Theme !== "undefined") {
|
|
||||||
Theme.generateSystemThemesFromCurrentTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setShowDock(enabled) {
|
|
||||||
showDock = enabled
|
|
||||||
if (enabled && dockPosition === dankBarPosition) {
|
|
||||||
if (dankBarPosition === SettingsData.Position.Top) {
|
|
||||||
setDockPosition(SettingsData.Position.Bottom)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (dankBarPosition === SettingsData.Position.Bottom) {
|
|
||||||
setDockPosition(SettingsData.Position.Top)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (dankBarPosition === SettingsData.Position.Left) {
|
|
||||||
setDockPosition(SettingsData.Position.Right)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (dankBarPosition === SettingsData.Position.Right) {
|
|
||||||
setDockPosition(SettingsData.Position.Left)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDockPosition(position) {
|
|
||||||
dockPosition = position
|
|
||||||
if (position === SettingsData.Position.Bottom && dankBarPosition === SettingsData.Position.Bottom && showDock) {
|
|
||||||
setDankBarPosition(SettingsData.Position.Top)
|
|
||||||
}
|
|
||||||
if (position === SettingsData.Position.Top && dankBarPosition === SettingsData.Position.Top && showDock) {
|
|
||||||
setDankBarPosition(SettingsData.Position.Bottom)
|
|
||||||
}
|
|
||||||
if (position === SettingsData.Position.Left && dankBarPosition === SettingsData.Position.Left && showDock) {
|
|
||||||
setDankBarPosition(SettingsData.Position.Right)
|
|
||||||
}
|
|
||||||
if (position === SettingsData.Position.Right && dankBarPosition === SettingsData.Position.Right && showDock) {
|
|
||||||
setDankBarPosition(SettingsData.Position.Left)
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
Qt.callLater(() => forceDockLayoutRefresh())
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDankBarSpacing(spacing) {
|
|
||||||
set("dankBarSpacing", spacing)
|
|
||||||
if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
|
|
||||||
NiriService.generateNiriLayoutConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDankBarPosition(position) {
|
|
||||||
dankBarPosition = position
|
|
||||||
if (position === SettingsData.Position.Bottom && dockPosition === SettingsData.Position.Bottom && showDock) {
|
|
||||||
setDockPosition(SettingsData.Position.Top)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (position === SettingsData.Position.Top && dockPosition === SettingsData.Position.Top && showDock) {
|
|
||||||
setDockPosition(SettingsData.Position.Bottom)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (position === SettingsData.Position.Left && dockPosition === SettingsData.Position.Left && showDock) {
|
|
||||||
setDockPosition(SettingsData.Position.Right)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (position === SettingsData.Position.Right && dockPosition === SettingsData.Position.Right && showDock) {
|
|
||||||
setDockPosition(SettingsData.Position.Left)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDankBarLeftWidgets(order) {
|
|
||||||
dankBarLeftWidgets = order
|
|
||||||
updateListModel(leftWidgetsModel, order)
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDankBarCenterWidgets(order) {
|
|
||||||
dankBarCenterWidgets = order
|
|
||||||
updateListModel(centerWidgetsModel, order)
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDankBarRightWidgets(order) {
|
|
||||||
dankBarRightWidgets = order
|
|
||||||
updateListModel(rightWidgetsModel, order)
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetDankBarWidgetsToDefault() {
|
|
||||||
var defaultLeft = ["launcherButton", "workspaceSwitcher", "focusedWindow"]
|
|
||||||
var defaultCenter = ["music", "clock", "weather"]
|
|
||||||
var defaultRight = ["systemTray", "clipboard", "notificationButton", "battery", "controlCenterButton"]
|
|
||||||
dankBarLeftWidgets = defaultLeft
|
|
||||||
dankBarCenterWidgets = defaultCenter
|
|
||||||
dankBarRightWidgets = defaultRight
|
|
||||||
updateListModel(leftWidgetsModel, defaultLeft)
|
|
||||||
updateListModel(centerWidgetsModel, defaultCenter)
|
|
||||||
updateListModel(rightWidgetsModel, defaultRight)
|
|
||||||
showLauncherButton = true
|
|
||||||
showWorkspaceSwitcher = true
|
|
||||||
showFocusedWindow = true
|
|
||||||
showWeather = true
|
|
||||||
showMusic = true
|
|
||||||
showClipboard = true
|
|
||||||
showCpuUsage = true
|
|
||||||
showMemUsage = true
|
|
||||||
showCpuTemp = true
|
|
||||||
showGpuTemp = true
|
|
||||||
showSystemTray = true
|
|
||||||
showClock = true
|
|
||||||
showNotificationButton = true
|
|
||||||
showBattery = true
|
|
||||||
showControlCenterButton = true
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setWorkspaceNameIcon(workspaceName, iconData) {
|
|
||||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
|
||||||
iconMap[workspaceName] = iconData
|
|
||||||
workspaceNameIcons = iconMap
|
|
||||||
saveSettings()
|
|
||||||
workspaceIconsUpdated()
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWorkspaceNameIcon(workspaceName) {
|
|
||||||
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
|
|
||||||
delete iconMap[workspaceName]
|
|
||||||
workspaceNameIcons = iconMap
|
|
||||||
saveSettings()
|
|
||||||
workspaceIconsUpdated()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkspaceNameIcon(workspaceName) {
|
|
||||||
return workspaceNameIcons[workspaceName] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleDankBarVisible() {
|
|
||||||
dankBarVisible = !dankBarVisible
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPluginSetting(pluginId, key, defaultValue) {
|
|
||||||
if (!pluginSettings[pluginId]) {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPluginSetting(pluginId, key, value) {
|
|
||||||
const updated = JSON.parse(JSON.stringify(pluginSettings))
|
|
||||||
if (!updated[pluginId]) {
|
|
||||||
updated[pluginId] = {}
|
|
||||||
}
|
|
||||||
updated[pluginId][key] = value
|
|
||||||
pluginSettings = updated
|
|
||||||
savePluginSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePluginSettings(pluginId) {
|
|
||||||
if (pluginSettings[pluginId]) {
|
|
||||||
delete pluginSettings[pluginId]
|
|
||||||
savePluginSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPluginSettingsForPlugin(pluginId) {
|
|
||||||
const settings = pluginSettings[pluginId]
|
|
||||||
return settings ? JSON.parse(JSON.stringify(settings)) : {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: leftWidgetsModel
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: centerWidgetsModel
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
|
||||||
id: rightWidgetsModel
|
|
||||||
}
|
|
||||||
|
|
||||||
property Process testNotificationProcess
|
|
||||||
|
|
||||||
testNotificationProcess: Process {
|
|
||||||
command: []
|
|
||||||
running: false
|
|
||||||
}
|
|
||||||
|
|
||||||
property Timer testNotifTimer1
|
|
||||||
|
|
||||||
testNotifTimer1: Timer {
|
|
||||||
interval: 400
|
|
||||||
repeat: false
|
|
||||||
onTriggered: sendTestNotification(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
property Timer testNotifTimer2
|
|
||||||
|
|
||||||
testNotifTimer2: Timer {
|
|
||||||
interval: 800
|
|
||||||
repeat: false
|
|
||||||
onTriggered: sendTestNotification(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias settingsFile: settingsFile
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: settingsFile
|
|
||||||
|
|
||||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
|
|
||||||
blockLoading: true
|
|
||||||
blockWrites: true
|
|
||||||
atomicWrites: true
|
|
||||||
watchChanges: !isGreeterMode
|
|
||||||
onLoaded: {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
try {
|
|
||||||
const txt = settingsFile.text()
|
|
||||||
const obj = (txt && txt.trim()) ? JSON.parse(txt) : null
|
|
||||||
Store.parse(root, obj)
|
|
||||||
Store.migrate(root, obj)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("SettingsData: Failed to reload settings:", e.message)
|
|
||||||
}
|
|
||||||
hasTriedDefaultSettings = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onLoadFailed: error => {
|
|
||||||
if (!isGreeterMode && !hasTriedDefaultSettings) {
|
|
||||||
hasTriedDefaultSettings = true
|
|
||||||
Processes.checkDefaultSettings()
|
|
||||||
} else if (!isGreeterMode) {
|
|
||||||
applyStoredTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: pluginSettingsFile
|
|
||||||
|
|
||||||
path: isGreeterMode ? "" : pluginSettingsPath
|
|
||||||
blockLoading: true
|
|
||||||
blockWrites: true
|
|
||||||
atomicWrites: true
|
|
||||||
watchChanges: !isGreeterMode
|
|
||||||
onLoaded: {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
parsePluginSettings(pluginSettingsFile.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onLoadFailed: error => {
|
|
||||||
if (!isGreeterMode) {
|
|
||||||
pluginSettings = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool pluginSettingsFileExists: false
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function reveal(): string {
|
|
||||||
root.setDankBarVisible(true)
|
|
||||||
return "BAR_SHOW_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide(): string {
|
|
||||||
root.setDankBarVisible(false)
|
|
||||||
return "BAR_HIDE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
root.toggleDankBarVisible()
|
|
||||||
return root.dankBarVisible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function status(): string {
|
|
||||||
return root.dankBarVisible ? "visible" : "hidden"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "bar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
.pragma library
|
|
||||||
|
|
||||||
.import "./SettingsSpec.js" as SpecModule
|
|
||||||
|
|
||||||
function parse(root, jsonObj) {
|
|
||||||
var SPEC = SpecModule.SPEC;
|
|
||||||
for (var k in SPEC) {
|
|
||||||
var spec = SPEC[k];
|
|
||||||
root[k] = spec.def;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!jsonObj) return;
|
|
||||||
|
|
||||||
for (var k in jsonObj) {
|
|
||||||
if (!SPEC[k]) continue;
|
|
||||||
var raw = jsonObj[k];
|
|
||||||
var spec = SPEC[k];
|
|
||||||
var coerce = spec.coerce;
|
|
||||||
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toJson(root) {
|
|
||||||
var SPEC = SpecModule.SPEC;
|
|
||||||
var out = {};
|
|
||||||
for (var k in SPEC) {
|
|
||||||
if (SPEC[k].persist === false) continue;
|
|
||||||
out[k] = root[k];
|
|
||||||
}
|
|
||||||
out.configVersion = root.settingsConfigVersion;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function migrate(root, jsonObj) {
|
|
||||||
var SPEC = SpecModule.SPEC;
|
|
||||||
if (!jsonObj) return;
|
|
||||||
|
|
||||||
if (jsonObj.themeIndex !== undefined || jsonObj.themeIsDynamic !== undefined) {
|
|
||||||
var themeNames = ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"];
|
|
||||||
if (jsonObj.themeIsDynamic) {
|
|
||||||
root.currentThemeName = "dynamic";
|
|
||||||
} else if (jsonObj.themeIndex >= 0 && jsonObj.themeIndex < themeNames.length) {
|
|
||||||
root.currentThemeName = themeNames[jsonObj.themeIndex];
|
|
||||||
}
|
|
||||||
console.info("Auto-migrated theme from index", jsonObj.themeIndex, "to", root.currentThemeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((jsonObj.dankBarWidgetOrder && jsonObj.dankBarWidgetOrder.length > 0) ||
|
|
||||||
(jsonObj.topBarWidgetOrder && jsonObj.topBarWidgetOrder.length > 0)) {
|
|
||||||
if (jsonObj.dankBarLeftWidgets === undefined && jsonObj.dankBarCenterWidgets === undefined && jsonObj.dankBarRightWidgets === undefined) {
|
|
||||||
var widgetOrder = jsonObj.dankBarWidgetOrder || jsonObj.topBarWidgetOrder;
|
|
||||||
root.dankBarLeftWidgets = widgetOrder.filter(function(w) { return ["launcherButton", "workspaceSwitcher", "focusedWindow"].indexOf(w) >= 0; });
|
|
||||||
root.dankBarCenterWidgets = widgetOrder.filter(function(w) { return ["clock", "music", "weather"].indexOf(w) >= 0; });
|
|
||||||
root.dankBarRightWidgets = widgetOrder.filter(function(w) { return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].indexOf(w) >= 0; });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonObj.useOSLogo !== undefined) {
|
|
||||||
root.launcherLogoMode = jsonObj.useOSLogo ? "os" : "apps";
|
|
||||||
root.launcherLogoColorOverride = jsonObj.osLogoColorOverride !== undefined ? jsonObj.osLogoColorOverride : "";
|
|
||||||
root.launcherLogoBrightness = jsonObj.osLogoBrightness !== undefined ? jsonObj.osLogoBrightness : 0.5;
|
|
||||||
root.launcherLogoContrast = jsonObj.osLogoContrast !== undefined ? jsonObj.osLogoContrast : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonObj.mediaCompactMode !== undefined && jsonObj.mediaSize === undefined) {
|
|
||||||
root.mediaSize = jsonObj.mediaCompactMode ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var k in SPEC) {
|
|
||||||
var spec = SPEC[k];
|
|
||||||
if (!spec.migrate) continue;
|
|
||||||
for (var i = 0; i < spec.migrate.length; i++) {
|
|
||||||
var oldKey = spec.migrate[i];
|
|
||||||
if (jsonObj[oldKey] !== undefined && jsonObj[k] === undefined) {
|
|
||||||
var raw = jsonObj[oldKey];
|
|
||||||
var coerce = spec.coerce;
|
|
||||||
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonObj.dankBarAtBottom !== undefined || jsonObj.topBarAtBottom !== undefined) {
|
|
||||||
var atBottom = jsonObj.dankBarAtBottom !== undefined ? jsonObj.dankBarAtBottom : jsonObj.topBarAtBottom;
|
|
||||||
root.dankBarPosition = atBottom ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonObj.pluginSettings !== undefined) {
|
|
||||||
root.pluginSettings = jsonObj.pluginSettings;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanup(fileText) {
|
|
||||||
var getValidKeys = SpecModule.getValidKeys;
|
|
||||||
if (!fileText || !fileText.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var settings = JSON.parse(fileText);
|
|
||||||
var validKeys = getValidKeys();
|
|
||||||
var needsSave = false;
|
|
||||||
|
|
||||||
for (var key in settings) {
|
|
||||||
if (validKeys.indexOf(key) < 0) {
|
|
||||||
console.log("SettingsData: Removing unused key:", key);
|
|
||||||
delete settings[key];
|
|
||||||
needsSave = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return needsSave ? JSON.stringify(settings, null, 2) : null;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("SettingsData: Failed to cleanup unused keys:", e.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-434
@@ -1,434 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property var powerMenuModalLoader
|
|
||||||
required property var processListModalLoader
|
|
||||||
required property var controlCenterLoader
|
|
||||||
required property var dankDashPopoutLoader
|
|
||||||
required property var notepadSlideoutVariants
|
|
||||||
required property var hyprKeybindsModalLoader
|
|
||||||
required property var dankBarLoader
|
|
||||||
required property var hyprlandOverviewLoader
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open() {
|
|
||||||
root.powerMenuModalLoader.active = true
|
|
||||||
if (root.powerMenuModalLoader.item)
|
|
||||||
root.powerMenuModalLoader.item.openCentered()
|
|
||||||
|
|
||||||
return "POWERMENU_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
if (root.powerMenuModalLoader.item)
|
|
||||||
root.powerMenuModalLoader.item.close()
|
|
||||||
|
|
||||||
return "POWERMENU_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
root.powerMenuModalLoader.active = true
|
|
||||||
if (root.powerMenuModalLoader.item) {
|
|
||||||
if (root.powerMenuModalLoader.item.shouldBeVisible) {
|
|
||||||
root.powerMenuModalLoader.item.close()
|
|
||||||
} else {
|
|
||||||
root.powerMenuModalLoader.item.openCentered()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "POWERMENU_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "powermenu"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
root.processListModalLoader.active = true
|
|
||||||
if (root.processListModalLoader.item)
|
|
||||||
root.processListModalLoader.item.show()
|
|
||||||
|
|
||||||
return "PROCESSLIST_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
if (root.processListModalLoader.item)
|
|
||||||
root.processListModalLoader.item.hide()
|
|
||||||
|
|
||||||
return "PROCESSLIST_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
root.processListModalLoader.active = true
|
|
||||||
if (root.processListModalLoader.item)
|
|
||||||
root.processListModalLoader.item.toggle()
|
|
||||||
|
|
||||||
return "PROCESSLIST_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "processlist"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
if (root.dankBarLoader.item) {
|
|
||||||
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
|
|
||||||
return "CONTROL_CENTER_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
return "CONTROL_CENTER_OPEN_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
if (root.controlCenterLoader.item) {
|
|
||||||
root.controlCenterLoader.item.close()
|
|
||||||
return "CONTROL_CENTER_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "CONTROL_CENTER_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
if (root.dankBarLoader.item) {
|
|
||||||
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
|
|
||||||
return "CONTROL_CENTER_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "CONTROL_CENTER_TOGGLE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "control-center"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(tab: string): string {
|
|
||||||
root.dankDashPopoutLoader.active = true
|
|
||||||
if (root.dankDashPopoutLoader.item) {
|
|
||||||
switch (tab.toLowerCase()) {
|
|
||||||
case "media":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 1
|
|
||||||
break
|
|
||||||
case "weather":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 0
|
|
||||||
break
|
|
||||||
}
|
|
||||||
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
|
|
||||||
root.dankDashPopoutLoader.item.dashVisible = true
|
|
||||||
return "DASH_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
return "DASH_OPEN_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
if (root.dankDashPopoutLoader.item) {
|
|
||||||
root.dankDashPopoutLoader.item.dashVisible = false
|
|
||||||
return "DASH_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "DASH_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(tab: string): string {
|
|
||||||
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
|
|
||||||
if (root.dankDashPopoutLoader.item) {
|
|
||||||
switch (tab.toLowerCase()) {
|
|
||||||
case "media":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 1
|
|
||||||
break
|
|
||||||
case "wallpaper":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 2
|
|
||||||
break
|
|
||||||
case "weather":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 0
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "DASH_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "DASH_TOGGLE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "dash"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function getFocusedScreenName() {
|
|
||||||
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
|
|
||||||
return Hyprland.focusedWorkspace.monitor.name
|
|
||||||
}
|
|
||||||
if (CompositorService.isNiri && NiriService.currentOutput) {
|
|
||||||
return NiriService.currentOutput
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveNotepadInstance() {
|
|
||||||
if (root.notepadSlideoutVariants.instances.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.notepadSlideoutVariants.instances.length === 1) {
|
|
||||||
return root.notepadSlideoutVariants.instances[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
var focusedScreen = getFocusedScreenName()
|
|
||||||
if (focusedScreen && root.notepadSlideoutVariants.instances.length > 0) {
|
|
||||||
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
|
|
||||||
var slideout = root.notepadSlideoutVariants.instances[i]
|
|
||||||
if (slideout.modelData && slideout.modelData.name === focusedScreen) {
|
|
||||||
return slideout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
|
|
||||||
var slideout = root.notepadSlideoutVariants.instances[i]
|
|
||||||
if (slideout.isVisible) {
|
|
||||||
return slideout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return root.notepadSlideoutVariants.instances[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function open(): string {
|
|
||||||
var instance = getActiveNotepadInstance()
|
|
||||||
if (instance) {
|
|
||||||
instance.show()
|
|
||||||
return "NOTEPAD_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
return "NOTEPAD_OPEN_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
var instance = getActiveNotepadInstance()
|
|
||||||
if (instance) {
|
|
||||||
instance.hide()
|
|
||||||
return "NOTEPAD_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "NOTEPAD_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
var instance = getActiveNotepadInstance()
|
|
||||||
if (instance) {
|
|
||||||
instance.toggle()
|
|
||||||
return "NOTEPAD_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "NOTEPAD_TOGGLE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "notepad"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function toggle(): string {
|
|
||||||
SessionService.toggleIdleInhibit()
|
|
||||||
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function enable(): string {
|
|
||||||
SessionService.enableIdleInhibit()
|
|
||||||
return "Idle inhibit enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function disable(): string {
|
|
||||||
SessionService.disableIdleInhibit()
|
|
||||||
return "Idle inhibit disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function status(): string {
|
|
||||||
return SessionService.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
function reason(newReason: string): string {
|
|
||||||
if (!newReason) {
|
|
||||||
return `Current reason: ${SessionService.inhibitReason}`
|
|
||||||
}
|
|
||||||
|
|
||||||
SessionService.setInhibitReason(newReason)
|
|
||||||
return `Inhibit reason set to: ${newReason}`
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "inhibit"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function list(): string {
|
|
||||||
return MprisController.availablePlayers.map(p => p.identity).join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
function play(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canPlay) {
|
|
||||||
MprisController.activePlayer.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pause(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canPause) {
|
|
||||||
MprisController.activePlayer.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function playPause(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) {
|
|
||||||
MprisController.activePlayer.togglePlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previous(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
|
|
||||||
MprisController.activePlayer.previous()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function next(): void {
|
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
|
|
||||||
MprisController.activePlayer.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop(): void {
|
|
||||||
if (MprisController.activePlayer) {
|
|
||||||
MprisController.activePlayer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "mpris"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function toggle(provider: string): string {
|
|
||||||
if (!provider) {
|
|
||||||
return "ERROR: No provider specified"
|
|
||||||
}
|
|
||||||
|
|
||||||
KeybindsService.loadProvider(provider)
|
|
||||||
root.hyprKeybindsModalLoader.active = true
|
|
||||||
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
|
||||||
root.hyprKeybindsModalLoader.item.close()
|
|
||||||
} else {
|
|
||||||
root.hyprKeybindsModalLoader.item.open()
|
|
||||||
}
|
|
||||||
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`
|
|
||||||
}
|
|
||||||
return `KEYBINDS_TOGGLE_FAILED: ${provider}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function open(provider: string): string {
|
|
||||||
if (!provider) {
|
|
||||||
return "ERROR: No provider specified"
|
|
||||||
}
|
|
||||||
|
|
||||||
KeybindsService.loadProvider(provider)
|
|
||||||
root.hyprKeybindsModalLoader.active = true
|
|
||||||
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
root.hyprKeybindsModalLoader.item.open()
|
|
||||||
return `KEYBINDS_OPEN_SUCCESS: ${provider}`
|
|
||||||
}
|
|
||||||
return `KEYBINDS_OPEN_FAILED: ${provider}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
root.hyprKeybindsModalLoader.item.close()
|
|
||||||
return "KEYBINDS_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "KEYBINDS_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "keybinds"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function openBinds(): string {
|
|
||||||
if (!CompositorService.isHyprland) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
KeybindsService.loadProvider("hyprland")
|
|
||||||
root.hyprKeybindsModalLoader.active = true
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
root.hyprKeybindsModalLoader.item.open()
|
|
||||||
return "HYPR_KEYBINDS_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_OPEN_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeBinds(): string {
|
|
||||||
if (!CompositorService.isHyprland) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
root.hyprKeybindsModalLoader.item.close()
|
|
||||||
return "HYPR_KEYBINDS_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_CLOSE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleBinds(): string {
|
|
||||||
if (!CompositorService.isHyprland) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
KeybindsService.loadProvider("hyprland")
|
|
||||||
root.hyprKeybindsModalLoader.active = true
|
|
||||||
if (root.hyprKeybindsModalLoader.item) {
|
|
||||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
|
||||||
root.hyprKeybindsModalLoader.item.close()
|
|
||||||
} else {
|
|
||||||
root.hyprKeybindsModalLoader.item.open()
|
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_TOGGLE_SUCCESS"
|
|
||||||
}
|
|
||||||
return "HYPR_KEYBINDS_TOGGLE_FAILED"
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleOverview(): string {
|
|
||||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
|
||||||
return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeOverview(): string {
|
|
||||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = false
|
|
||||||
return "OVERVIEW_CLOSE_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
function openOverview(): string {
|
|
||||||
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
|
|
||||||
return "HYPR_NOT_AVAILABLE"
|
|
||||||
}
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = true
|
|
||||||
return "OVERVIEW_OPEN_SUCCESS"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "hypr"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function wallpaper(): string {
|
|
||||||
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
|
|
||||||
return "SUCCESS: Toggled wallpaper browser"
|
|
||||||
}
|
|
||||||
return "ERROR: Failed to toggle wallpaper browser"
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "dankdash"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,674 +1,21 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
MIT License
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
Copyright (c) 2025 Avenge Media LLC
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this license document, but changing it is not allowed.
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
Preamble
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
The GNU General Public License is a free, copyleft license for
|
furnished to do so, subject to the following conditions:
|
||||||
software and other kinds of works.
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
The licenses for most software and other practical works are designed
|
copies or substantial portions of the Software.
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
share and change all versions of a program--to make sure it remains free
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
GNU General Public License for most of our software; it applies also to
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
any other work released this way by its authors. You can apply it to
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
your programs, too.
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|||||||
@@ -1,323 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string layerNamespace: "dms:modal"
|
|
||||||
WlrLayershell.namespace: layerNamespace
|
|
||||||
|
|
||||||
property alias content: contentLoader.sourceComponent
|
|
||||||
property alias contentLoader: contentLoader
|
|
||||||
property Item directContent: null
|
|
||||||
property real width: 400
|
|
||||||
property real height: 300
|
|
||||||
readonly property real screenWidth: screen ? screen.width : 1920
|
|
||||||
readonly property real screenHeight: screen ? screen.height : 1080
|
|
||||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
|
||||||
property bool showBackground: true
|
|
||||||
property real backgroundOpacity: 0.5
|
|
||||||
property string positioning: "center"
|
|
||||||
property point customPosition: Qt.point(0, 0)
|
|
||||||
property bool closeOnEscapeKey: true
|
|
||||||
property bool closeOnBackgroundClick: true
|
|
||||||
property string animationType: "scale"
|
|
||||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
|
||||||
property real animationScaleCollapsed: 0.96
|
|
||||||
property real animationOffset: Theme.spacingL
|
|
||||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
|
||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
|
||||||
property color borderColor: Theme.outlineMedium
|
|
||||||
property real borderWidth: 1
|
|
||||||
property real cornerRadius: Theme.cornerRadius
|
|
||||||
property bool enableShadow: false
|
|
||||||
property alias modalFocusScope: focusScope
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property bool shouldHaveFocus: shouldBeVisible
|
|
||||||
property bool allowFocusOverride: false
|
|
||||||
property bool allowStacking: false
|
|
||||||
property bool keepContentLoaded: false
|
|
||||||
|
|
||||||
signal opened
|
|
||||||
signal dialogClosed
|
|
||||||
signal backgroundClicked
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
ModalManager.openModal(root)
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
focusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
shouldBeVisible = false
|
|
||||||
closeTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
close()
|
|
||||||
} else {
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: shouldBeVisible
|
|
||||||
color: "transparent"
|
|
||||||
WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under modal
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (root.visible) {
|
|
||||||
opened()
|
|
||||||
} else {
|
|
||||||
if (Qt.inputMethod) {
|
|
||||||
Qt.inputMethod.hide()
|
|
||||||
Qt.inputMethod.reset()
|
|
||||||
}
|
|
||||||
dialogClosed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onCloseAllModalsExcept(excludedModal) {
|
|
||||||
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: ModalManager
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
|
|
||||||
interval: animationDuration + 120
|
|
||||||
onTriggered: {
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
|
||||||
onClicked: mouse => {
|
|
||||||
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
|
|
||||||
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
|
|
||||||
root.backgroundClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: background
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "black"
|
|
||||||
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
|
||||||
visible: root.showBackground && SettingsData.modalDarkenBackground
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: modalContainer
|
|
||||||
|
|
||||||
width: Theme.px(root.width, dpr)
|
|
||||||
height: Theme.px(root.height, dpr)
|
|
||||||
x: {
|
|
||||||
if (positioning === "center") {
|
|
||||||
return Theme.snap((root.screenWidth - width) / 2, dpr)
|
|
||||||
} else if (positioning === "top-right") {
|
|
||||||
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
|
|
||||||
} else if (positioning === "custom") {
|
|
||||||
return Theme.snap(root.customPosition.x, dpr)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (positioning === "center") {
|
|
||||||
return Theme.snap((root.screenHeight - height) / 2, dpr)
|
|
||||||
} else if (positioning === "top-right") {
|
|
||||||
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
|
|
||||||
} else if (positioning === "custom") {
|
|
||||||
return Theme.snap(root.customPosition.y, dpr)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property bool slide: root.animationType === "slide"
|
|
||||||
readonly property real offsetX: slide ? 15 : 0
|
|
||||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
|
||||||
|
|
||||||
property real animX: 0
|
|
||||||
property real animY: 0
|
|
||||||
property real scaleValue: root.animationScaleCollapsed
|
|
||||||
|
|
||||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
|
||||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr)
|
|
||||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr)
|
|
||||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on animX {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on animY {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scaleValue {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: contentContainer
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
color: root.backgroundColor
|
|
||||||
radius: root.cornerRadius
|
|
||||||
border.color: root.borderColor
|
|
||||||
border.width: root.borderWidth
|
|
||||||
clip: false
|
|
||||||
layer.enabled: true
|
|
||||||
layer.smooth: false
|
|
||||||
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
|
|
||||||
layer.textureMirroring: ShaderEffectSource.NoMirroring
|
|
||||||
opacity: root.shouldBeVisible ? 1 : 0
|
|
||||||
scale: modalContainer.scaleValue
|
|
||||||
x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
|
|
||||||
y: Theme.snap(modalContainer.animY + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: root.shouldBeVisible
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: directContentWrapper
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.directContent !== null
|
|
||||||
focus: true
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (root.directContent) {
|
|
||||||
root.directContent.parent = directContentWrapper
|
|
||||||
root.directContent.anchors.fill = directContentWrapper
|
|
||||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onDirectContentChanged() {
|
|
||||||
if (root.directContent) {
|
|
||||||
root.directContent.parent = directContentWrapper
|
|
||||||
root.directContent.anchors.fill = directContentWrapper
|
|
||||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
|
|
||||||
asynchronous: false
|
|
||||||
focus: true
|
|
||||||
clip: false
|
|
||||||
visible: root.directContent === null
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (item) {
|
|
||||||
Qt.callLater(() => item.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: focusScope
|
|
||||||
|
|
||||||
objectName: "modalFocusScope"
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.shouldBeVisible || root.visible
|
|
||||||
focus: root.shouldBeVisible
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
if (root.closeOnEscapeKey && shouldHaveFocus) {
|
|
||||||
root.close()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && shouldHaveFocus) {
|
|
||||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onShouldHaveFocusChanged() {
|
|
||||||
if (shouldHaveFocus && shouldBeVisible) {
|
|
||||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,934 +0,0 @@
|
|||||||
import Qt.labs.folderlistmodel
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Modals.FileBrowser
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: fileBrowserModal
|
|
||||||
|
|
||||||
layerNamespace: "dms:file-browser"
|
|
||||||
|
|
||||||
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
|
||||||
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
|
|
||||||
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
|
|
||||||
property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation)
|
|
||||||
property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
|
||||||
property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
|
||||||
property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation)
|
|
||||||
property string currentPath: ""
|
|
||||||
property var fileExtensions: ["*.*"]
|
|
||||||
property alias filterExtensions: fileBrowserModal.fileExtensions
|
|
||||||
property string browserTitle: "Select File"
|
|
||||||
property string browserIcon: "folder_open"
|
|
||||||
property string browserType: "generic"
|
|
||||||
property bool showHiddenFiles: false
|
|
||||||
property int selectedIndex: -1
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property bool backButtonFocused: false
|
|
||||||
property bool saveMode: false
|
|
||||||
property string defaultFileName: ""
|
|
||||||
property int keyboardSelectionIndex: -1
|
|
||||||
property bool keyboardSelectionRequested: false
|
|
||||||
property bool showKeyboardHints: false
|
|
||||||
property bool showFileInfo: false
|
|
||||||
property string selectedFilePath: ""
|
|
||||||
property string selectedFileName: ""
|
|
||||||
property bool selectedFileIsDir: false
|
|
||||||
property bool showOverwriteConfirmation: false
|
|
||||||
property string pendingFilePath: ""
|
|
||||||
property var parentModal: null
|
|
||||||
property bool showSidebar: true
|
|
||||||
property string viewMode: "grid"
|
|
||||||
property string sortBy: "name"
|
|
||||||
property bool sortAscending: true
|
|
||||||
property int iconSizeIndex: 1
|
|
||||||
property var iconSizes: [80, 120, 160, 200]
|
|
||||||
property bool pathEditMode: false
|
|
||||||
property bool pathInputHasFocus: false
|
|
||||||
property int actualGridColumns: 5
|
|
||||||
property bool _initialized: false
|
|
||||||
|
|
||||||
signal fileSelected(string path)
|
|
||||||
|
|
||||||
function loadSettings() {
|
|
||||||
const type = browserType || "default"
|
|
||||||
const settings = CacheData.fileBrowserSettings[type]
|
|
||||||
const isImageBrowser = ["wallpaper", "profile"].includes(browserType)
|
|
||||||
|
|
||||||
if (settings) {
|
|
||||||
viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list")
|
|
||||||
sortBy = settings.sortBy || "name"
|
|
||||||
sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true
|
|
||||||
iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1
|
|
||||||
showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true
|
|
||||||
} else {
|
|
||||||
viewMode = isImageBrowser ? "grid" : "list"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSettings() {
|
|
||||||
if (!_initialized)
|
|
||||||
return
|
|
||||||
|
|
||||||
const type = browserType || "default"
|
|
||||||
let settings = CacheData.fileBrowserSettings
|
|
||||||
if (!settings[type]) {
|
|
||||||
settings[type] = {}
|
|
||||||
}
|
|
||||||
settings[type].viewMode = viewMode
|
|
||||||
settings[type].sortBy = sortBy
|
|
||||||
settings[type].sortAscending = sortAscending
|
|
||||||
settings[type].iconSizeIndex = iconSizeIndex
|
|
||||||
settings[type].showSidebar = showSidebar
|
|
||||||
settings[type].lastPath = currentPath
|
|
||||||
CacheData.fileBrowserSettings = settings
|
|
||||||
|
|
||||||
if (browserType === "wallpaper") {
|
|
||||||
CacheData.wallpaperLastPath = currentPath
|
|
||||||
} else if (browserType === "profile") {
|
|
||||||
CacheData.profileLastPath = currentPath
|
|
||||||
}
|
|
||||||
|
|
||||||
CacheData.saveCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewModeChanged: saveSettings()
|
|
||||||
onSortByChanged: saveSettings()
|
|
||||||
onSortAscendingChanged: saveSettings()
|
|
||||||
onIconSizeIndexChanged: saveSettings()
|
|
||||||
onShowSidebarChanged: saveSettings()
|
|
||||||
|
|
||||||
function isImageFile(fileName) {
|
|
||||||
if (!fileName) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const ext = fileName.toLowerCase().split('.').pop()
|
|
||||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastPath() {
|
|
||||||
const type = browserType || "default"
|
|
||||||
const settings = CacheData.fileBrowserSettings[type]
|
|
||||||
const lastPath = settings?.lastPath || ""
|
|
||||||
return (lastPath && lastPath !== "") ? lastPath : homeDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveLastPath(path) {
|
|
||||||
const type = browserType || "default"
|
|
||||||
let settings = CacheData.fileBrowserSettings
|
|
||||||
if (!settings[type]) {
|
|
||||||
settings[type] = {}
|
|
||||||
}
|
|
||||||
settings[type].lastPath = path
|
|
||||||
CacheData.fileBrowserSettings = settings
|
|
||||||
CacheData.saveCache()
|
|
||||||
|
|
||||||
if (browserType === "wallpaper") {
|
|
||||||
CacheData.wallpaperLastPath = path
|
|
||||||
} else if (browserType === "profile") {
|
|
||||||
CacheData.profileLastPath = path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSelectedFileData(path, name, isDir) {
|
|
||||||
selectedFilePath = path
|
|
||||||
selectedFileName = name
|
|
||||||
selectedFileIsDir = isDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateUp() {
|
|
||||||
const path = currentPath
|
|
||||||
if (path === homeDir)
|
|
||||||
return
|
|
||||||
|
|
||||||
const lastSlash = path.lastIndexOf('/')
|
|
||||||
if (lastSlash > 0) {
|
|
||||||
const newPath = path.substring(0, lastSlash)
|
|
||||||
if (newPath.length < homeDir.length) {
|
|
||||||
currentPath = homeDir
|
|
||||||
saveLastPath(homeDir)
|
|
||||||
} else {
|
|
||||||
currentPath = newPath
|
|
||||||
saveLastPath(newPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateTo(path) {
|
|
||||||
currentPath = path
|
|
||||||
saveLastPath(path)
|
|
||||||
selectedIndex = -1
|
|
||||||
backButtonFocused = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyboardFileSelection(index) {
|
|
||||||
if (index >= 0) {
|
|
||||||
keyboardSelectionTimer.targetIndex = index
|
|
||||||
keyboardSelectionTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeKeyboardSelection(index) {
|
|
||||||
keyboardSelectionIndex = index
|
|
||||||
keyboardSelectionRequested = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSaveFile(filePath) {
|
|
||||||
var normalizedPath = filePath
|
|
||||||
if (!normalizedPath.startsWith("file://")) {
|
|
||||||
normalizedPath = "file://" + filePath
|
|
||||||
}
|
|
||||||
|
|
||||||
var exists = false
|
|
||||||
var fileName = filePath.split('/').pop()
|
|
||||||
|
|
||||||
for (var i = 0; i < folderModel.count; i++) {
|
|
||||||
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
|
||||||
exists = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
pendingFilePath = normalizedPath
|
|
||||||
showOverwriteConfirmation = true
|
|
||||||
} else {
|
|
||||||
fileSelected(normalizedPath)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
objectName: "fileBrowserModal"
|
|
||||||
allowStacking: true
|
|
||||||
closeOnEscapeKey: false
|
|
||||||
shouldHaveFocus: shouldBeVisible
|
|
||||||
Component.onCompleted: {
|
|
||||||
loadSettings()
|
|
||||||
currentPath = getLastPath()
|
|
||||||
_initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
|
||||||
StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
|
||||||
StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(
|
|
||||||
StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
|
|
||||||
property int currentPathIndex: 0
|
|
||||||
|
|
||||||
width: 800
|
|
||||||
height: 600
|
|
||||||
enableShadow: true
|
|
||||||
visible: false
|
|
||||||
onBackgroundClicked: close()
|
|
||||||
onOpened: {
|
|
||||||
if (parentModal) {
|
|
||||||
parentModal.shouldHaveFocus = false
|
|
||||||
parentModal.allowFocusOverride = true
|
|
||||||
}
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader && contentLoader.item) {
|
|
||||||
contentLoader.item.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onDialogClosed: {
|
|
||||||
if (parentModal) {
|
|
||||||
parentModal.allowFocusOverride = false
|
|
||||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
|
||||||
return parentModal.shouldBeVisible
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
currentPath = getLastPath()
|
|
||||||
selectedIndex = -1
|
|
||||||
keyboardNavigationActive = false
|
|
||||||
backButtonFocused = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onCurrentPathChanged: {
|
|
||||||
selectedFilePath = ""
|
|
||||||
selectedFileName = ""
|
|
||||||
selectedFileIsDir = false
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
onSelectedIndexChanged: {
|
|
||||||
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
|
||||||
selectedFilePath = ""
|
|
||||||
selectedFileName = ""
|
|
||||||
selectedFileIsDir = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderListModel {
|
|
||||||
id: folderModel
|
|
||||||
|
|
||||||
showDirsFirst: true
|
|
||||||
showDotAndDotDot: false
|
|
||||||
showHidden: fileBrowserModal.showHiddenFiles
|
|
||||||
nameFilters: fileExtensions
|
|
||||||
showFiles: true
|
|
||||||
showDirs: true
|
|
||||||
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
|
|
||||||
sortField: {
|
|
||||||
switch (sortBy) {
|
|
||||||
case "name":
|
|
||||||
return FolderListModel.Name
|
|
||||||
case "size":
|
|
||||||
return FolderListModel.Size
|
|
||||||
case "modified":
|
|
||||||
return FolderListModel.Time
|
|
||||||
case "type":
|
|
||||||
return FolderListModel.Type
|
|
||||||
default:
|
|
||||||
return FolderListModel.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortReversed: !sortAscending
|
|
||||||
}
|
|
||||||
|
|
||||||
property var quickAccessLocations: [{
|
|
||||||
"name": "Home",
|
|
||||||
"path": homeDir,
|
|
||||||
"icon": "home"
|
|
||||||
}, {
|
|
||||||
"name": "Documents",
|
|
||||||
"path": docsDir,
|
|
||||||
"icon": "description"
|
|
||||||
}, {
|
|
||||||
"name": "Downloads",
|
|
||||||
"path": downloadDir,
|
|
||||||
"icon": "download"
|
|
||||||
}, {
|
|
||||||
"name": "Pictures",
|
|
||||||
"path": picsDir,
|
|
||||||
"icon": "image"
|
|
||||||
}, {
|
|
||||||
"name": "Music",
|
|
||||||
"path": musicDir,
|
|
||||||
"icon": "music_note"
|
|
||||||
}, {
|
|
||||||
"name": "Videos",
|
|
||||||
"path": videosDir,
|
|
||||||
"icon": "movie"
|
|
||||||
}, {
|
|
||||||
"name": "Desktop",
|
|
||||||
"path": desktopDir,
|
|
||||||
"icon": "computer"
|
|
||||||
}]
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: keyboardController
|
|
||||||
|
|
||||||
property int totalItems: folderModel.count
|
|
||||||
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
|
|
||||||
|
|
||||||
function handleKey(event) {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
close()
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.key === Qt.Key_F10) {
|
|
||||||
showKeyboardHints = !showKeyboardHints
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
|
||||||
showFileInfo = !showFileInfo
|
|
||||||
event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
|
||||||
if (currentPath !== homeDir) {
|
|
||||||
navigateUp()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!keyboardNavigationActive) {
|
|
||||||
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key
|
|
||||||
=== Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier)
|
|
||||||
|
|
||||||
if (isInitKey) {
|
|
||||||
keyboardNavigationActive = true
|
|
||||||
if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
} else {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
} else {
|
|
||||||
selectedIndex = 0
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Backtab:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = totalItems - 1
|
|
||||||
} else if (selectedIndex > 0) {
|
|
||||||
selectedIndex--
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
} else {
|
|
||||||
selectedIndex = totalItems - 1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_N:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_P:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (selectedIndex > 0) {
|
|
||||||
selectedIndex--
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_J:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_K:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (selectedIndex > 0) {
|
|
||||||
selectedIndex--
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_H:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (!backButtonFocused && selectedIndex > 0) {
|
|
||||||
selectedIndex--
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_L:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_Left:
|
|
||||||
if (pathInputHasFocus)
|
|
||||||
return
|
|
||||||
if (backButtonFocused)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (selectedIndex > 0) {
|
|
||||||
selectedIndex--
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Right:
|
|
||||||
if (pathInputHasFocus)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Up:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
if (gridColumns === 1) {
|
|
||||||
selectedIndex = 0
|
|
||||||
} else {
|
|
||||||
var col = selectedIndex % gridColumns
|
|
||||||
selectedIndex = Math.min(col, totalItems - 1)
|
|
||||||
}
|
|
||||||
} else if (selectedIndex >= gridColumns) {
|
|
||||||
selectedIndex -= gridColumns
|
|
||||||
} else if (selectedIndex > 0 && gridColumns === 1) {
|
|
||||||
selectedIndex--
|
|
||||||
} else if (currentPath !== homeDir) {
|
|
||||||
backButtonFocused = true
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Down:
|
|
||||||
if (backButtonFocused) {
|
|
||||||
backButtonFocused = false
|
|
||||||
selectedIndex = 0
|
|
||||||
} else if (gridColumns === 1) {
|
|
||||||
if (selectedIndex < totalItems - 1) {
|
|
||||||
selectedIndex++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var newIndex = selectedIndex + gridColumns
|
|
||||||
if (newIndex < totalItems) {
|
|
||||||
selectedIndex = newIndex
|
|
||||||
} else {
|
|
||||||
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns
|
|
||||||
var col = selectedIndex % gridColumns
|
|
||||||
var targetIndex = lastRowStart + col
|
|
||||||
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
|
||||||
selectedIndex = targetIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
case Qt.Key_Space:
|
|
||||||
if (backButtonFocused)
|
|
||||||
navigateUp()
|
|
||||||
else if (selectedIndex >= 0 && selectedIndex < totalItems)
|
|
||||||
fileBrowserModal.keyboardFileSelection(selectedIndex)
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: keyboardSelectionTimer
|
|
||||||
|
|
||||||
property int targetIndex: -1
|
|
||||||
|
|
||||||
interval: 1
|
|
||||||
onTriggered: {
|
|
||||||
executeKeyboardSelection(targetIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
keyboardController.handleKey(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 48
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: browserIcon
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: browserTitle
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: showHiddenFiles ? "visibility_off" : "visibility"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
|
|
||||||
onClicked: showHiddenFiles = !showHiddenFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: viewMode === "grid" ? "view_list" : "grid_view"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
visible: viewMode === "grid"
|
|
||||||
onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "info"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 49
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: showSidebar ? 201 : 0
|
|
||||||
height: parent.height
|
|
||||||
spacing: 0
|
|
||||||
visible: showSidebar
|
|
||||||
|
|
||||||
FileBrowserSidebar {
|
|
||||||
height: parent.height
|
|
||||||
quickAccessLocations: fileBrowserModal.quickAccessLocations
|
|
||||||
currentPath: fileBrowserModal.currentPath
|
|
||||||
onLocationSelected: path => navigateTo(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: 1
|
|
||||||
height: parent.height
|
|
||||||
color: Theme.outline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - (showSidebar ? 201 : 0)
|
|
||||||
height: parent.height
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
FileBrowserNavigation {
|
|
||||||
width: parent.width
|
|
||||||
currentPath: fileBrowserModal.currentPath
|
|
||||||
homeDir: fileBrowserModal.homeDir
|
|
||||||
backButtonFocused: fileBrowserModal.backButtonFocused
|
|
||||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
|
||||||
showSidebar: fileBrowserModal.showSidebar
|
|
||||||
pathEditMode: fileBrowserModal.pathEditMode
|
|
||||||
onNavigateUp: fileBrowserModal.navigateUp()
|
|
||||||
onNavigateTo: path => fileBrowserModal.navigateTo(path)
|
|
||||||
onPathInputFocusChanged: hasFocus => {
|
|
||||||
fileBrowserModal.pathInputHasFocus = hasFocus
|
|
||||||
if (hasFocus) {
|
|
||||||
fileBrowserModal.pathEditMode = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: gridContainer
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 41
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
property real gridCellWidth: iconSizes[iconSizeIndex] + 24
|
|
||||||
property real gridCellHeight: iconSizes[iconSizeIndex] + 56
|
|
||||||
property real availableGridWidth: width - Theme.spacingM * 2
|
|
||||||
property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
|
|
||||||
property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
|
|
||||||
|
|
||||||
onGridColumnsChanged: {
|
|
||||||
fileBrowserModal.actualGridColumns = gridColumns
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
fileBrowserModal.actualGridColumns = gridColumns
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: fileGrid
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: gridContainer.gridLeftMargin
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
visible: viewMode === "grid"
|
|
||||||
cellWidth: gridContainer.gridCellWidth
|
|
||||||
cellHeight: gridContainer.gridCellHeight
|
|
||||||
cacheBuffer: 260
|
|
||||||
model: folderModel
|
|
||||||
currentIndex: selectedIndex
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive && currentIndex >= 0)
|
|
||||||
positionViewAtIndex(currentIndex, GridView.Contain)
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: DankScrollbar {
|
|
||||||
id: gridScrollbar
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: FileBrowserGridDelegate {
|
|
||||||
iconSizes: fileBrowserModal.iconSizes
|
|
||||||
iconSizeIndex: fileBrowserModal.iconSizeIndex
|
|
||||||
selectedIndex: fileBrowserModal.selectedIndex
|
|
||||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
|
||||||
onItemClicked: (index, path, name, isDir) => {
|
|
||||||
selectedIndex = index
|
|
||||||
setSelectedFileData(path, name, isDir)
|
|
||||||
if (isDir) {
|
|
||||||
navigateTo(path)
|
|
||||||
} else {
|
|
||||||
fileSelected(path)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onItemSelected: (index, path, name, isDir) => {
|
|
||||||
setSelectedFileData(path, name, isDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onKeyboardSelectionRequestedChanged() {
|
|
||||||
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
|
|
||||||
fileBrowserModal.keyboardSelectionRequested = false
|
|
||||||
selectedIndex = index
|
|
||||||
setSelectedFileData(filePath, fileName, fileIsDir)
|
|
||||||
if (fileIsDir) {
|
|
||||||
navigateTo(filePath)
|
|
||||||
} else {
|
|
||||||
fileSelected(filePath)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: fileBrowserModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: fileList
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
visible: viewMode === "list"
|
|
||||||
spacing: 2
|
|
||||||
model: folderModel
|
|
||||||
currentIndex: selectedIndex
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive && currentIndex >= 0)
|
|
||||||
positionViewAtIndex(currentIndex, ListView.Contain)
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: DankScrollbar {
|
|
||||||
id: listScrollbar
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: FileBrowserListDelegate {
|
|
||||||
width: fileList.width
|
|
||||||
selectedIndex: fileBrowserModal.selectedIndex
|
|
||||||
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
|
|
||||||
onItemClicked: (index, path, name, isDir) => {
|
|
||||||
selectedIndex = index
|
|
||||||
setSelectedFileData(path, name, isDir)
|
|
||||||
if (isDir) {
|
|
||||||
navigateTo(path)
|
|
||||||
} else {
|
|
||||||
fileSelected(path)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onItemSelected: (index, path, name, isDir) => {
|
|
||||||
setSelectedFileData(path, name, isDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onKeyboardSelectionRequestedChanged() {
|
|
||||||
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
|
|
||||||
fileBrowserModal.keyboardSelectionRequested = false
|
|
||||||
selectedIndex = index
|
|
||||||
setSelectedFileData(filePath, fileName, fileIsDir)
|
|
||||||
if (fileIsDir) {
|
|
||||||
navigateTo(filePath)
|
|
||||||
} else {
|
|
||||||
fileSelected(filePath)
|
|
||||||
fileBrowserModal.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: fileBrowserModal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserSaveRow {
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
saveMode: fileBrowserModal.saveMode
|
|
||||||
defaultFileName: fileBrowserModal.defaultFileName
|
|
||||||
currentPath: fileBrowserModal.currentPath
|
|
||||||
onSaveRequested: filePath => handleSaveFile(filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyboardHints {
|
|
||||||
id: keyboardHints
|
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
showHints: fileBrowserModal.showKeyboardHints
|
|
||||||
}
|
|
||||||
|
|
||||||
FileInfo {
|
|
||||||
id: fileInfo
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
width: 300
|
|
||||||
showFileInfo: fileBrowserModal.showFileInfo
|
|
||||||
selectedIndex: fileBrowserModal.selectedIndex
|
|
||||||
sourceFolderModel: folderModel
|
|
||||||
currentPath: fileBrowserModal.currentPath
|
|
||||||
currentFileName: fileBrowserModal.selectedFileName
|
|
||||||
currentFileIsDir: fileBrowserModal.selectedFileIsDir
|
|
||||||
currentFileExtension: {
|
|
||||||
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.')
|
|
||||||
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserSortMenu {
|
|
||||||
id: sortMenu
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.topMargin: 120
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
sortBy: fileBrowserModal.sortBy
|
|
||||||
sortAscending: fileBrowserModal.sortAscending
|
|
||||||
onSortBySelected: value => {
|
|
||||||
fileBrowserModal.sortBy = value
|
|
||||||
}
|
|
||||||
onSortOrderSelected: ascending => {
|
|
||||||
fileBrowserModal.sortAscending = ascending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserOverwriteDialog {
|
|
||||||
anchors.fill: parent
|
|
||||||
showDialog: showOverwriteConfirmation
|
|
||||||
pendingFilePath: fileBrowserModal.pendingFilePath
|
|
||||||
onConfirmed: filePath => {
|
|
||||||
showOverwriteConfirmation = false
|
|
||||||
fileSelected(filePath)
|
|
||||||
pendingFilePath = ""
|
|
||||||
Qt.callLater(() => fileBrowserModal.close())
|
|
||||||
}
|
|
||||||
onCancelled: {
|
|
||||||
showOverwriteConfirmation = false
|
|
||||||
pendingFilePath = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,456 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
layerNamespace: "dms:power-menu"
|
|
||||||
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int optionCount: SessionService.hibernateSupported ? 5 : 4
|
|
||||||
property rect parentBounds: Qt.rect(0, 0, 0, 0)
|
|
||||||
property var parentScreen: null
|
|
||||||
|
|
||||||
signal powerActionRequested(string action, string title, string message)
|
|
||||||
|
|
||||||
function openCentered() {
|
|
||||||
parentBounds = Qt.rect(0, 0, 0, 0)
|
|
||||||
parentScreen = null
|
|
||||||
backgroundOpacity = 0.5
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFromControlCenter(bounds, targetScreen) {
|
|
||||||
parentBounds = bounds
|
|
||||||
parentScreen = targetScreen
|
|
||||||
backgroundOpacity = 0
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOption(action) {
|
|
||||||
close();
|
|
||||||
const actions = {
|
|
||||||
"logout": {
|
|
||||||
"title": I18n.tr("Log Out"),
|
|
||||||
"message": I18n.tr("Are you sure you want to log out?")
|
|
||||||
},
|
|
||||||
"suspend": {
|
|
||||||
"title": I18n.tr("Suspend"),
|
|
||||||
"message": I18n.tr("Are you sure you want to suspend the system?")
|
|
||||||
},
|
|
||||||
"hibernate": {
|
|
||||||
"title": I18n.tr("Hibernate"),
|
|
||||||
"message": I18n.tr("Are you sure you want to hibernate the system?")
|
|
||||||
},
|
|
||||||
"reboot": {
|
|
||||||
"title": I18n.tr("Reboot"),
|
|
||||||
"message": I18n.tr("Are you sure you want to reboot the system?")
|
|
||||||
},
|
|
||||||
"poweroff": {
|
|
||||||
"title": I18n.tr("Power Off"),
|
|
||||||
"message": I18n.tr("Are you sure you want to power off the system?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const selected = actions[action]
|
|
||||||
if (selected) {
|
|
||||||
root.powerActionRequested(action, selected.title, selected.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
width: 320
|
|
||||||
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
|
|
||||||
enableShadow: true
|
|
||||||
screen: parentScreen
|
|
||||||
positioning: parentBounds.width > 0 ? "custom" : "center"
|
|
||||||
customPosition: {
|
|
||||||
if (parentBounds.width > 0) {
|
|
||||||
const centerX = parentBounds.x + (parentBounds.width - width) / 2
|
|
||||||
const centerY = parentBounds.y + (parentBounds.height - height) / 2
|
|
||||||
return Qt.point(centerX, centerY)
|
|
||||||
}
|
|
||||||
return Qt.point(0, 0)
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return close();
|
|
||||||
}
|
|
||||||
onOpened: () => {
|
|
||||||
selectedIndex = 0;
|
|
||||||
Qt.callLater(() => modalFocusScope.forceActiveFocus());
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: (event) => {
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Up:
|
|
||||||
case Qt.Key_Backtab:
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Down:
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
const actions = ["logout", "suspend"];
|
|
||||||
if (SessionService.hibernateSupported) actions.push("hibernate");
|
|
||||||
actions.push("reboot", "poweroff");
|
|
||||||
if (selectedIndex < actions.length) {
|
|
||||||
selectOption(actions[selectedIndex]);
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
break;
|
|
||||||
case Qt.Key_N:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_P:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_J:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Qt.Key_K:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Options")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
return close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 0) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (logoutArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === 0 ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Log Out")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = 0;
|
|
||||||
selectOption("logout");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 1) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (suspendArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 1 ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === 1 ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Suspend")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = 1;
|
|
||||||
selectOption("suspend");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 2) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (hibernateArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 2 ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === 2 ? 1 : 0
|
|
||||||
visible: SessionService.hibernateSupported
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "ac_unit"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Hibernate")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: hibernateArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = 2;
|
|
||||||
selectOption("hibernate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
const rebootIndex = SessionService.hibernateSupported ? 3 : 2;
|
|
||||||
if (selectedIndex === rebootIndex) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (rebootArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "restart_alt"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Reboot")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = SessionService.hibernateSupported ? 3 : 2;
|
|
||||||
selectOption("reboot");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
const powerOffIndex = SessionService.hibernateSupported ? 4 : 3;
|
|
||||||
if (selectedIndex === powerOffIndex) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
|
||||||
} else if (powerOffArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power_settings_new"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Off")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: powerOffArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
selectedIndex = SessionService.hibernateSupported ? 4 : 3;
|
|
||||||
selectOption("poweroff");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
height: Theme.spacingS
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Modules.ProcessList
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: processListModal
|
|
||||||
|
|
||||||
layerNamespace: "dms:process-list-modal"
|
|
||||||
|
|
||||||
property int currentTab: 0
|
|
||||||
property var tabNames: ["Processes", "Performance", "System"]
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
if (!DgopService.dgopAvailable) {
|
|
||||||
console.warn("ProcessListModal: dgop is not available");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
open();
|
|
||||||
UserInfoService.getUptime();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close();
|
|
||||||
if (processContextMenu.visible) {
|
|
||||||
processContextMenu.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (!DgopService.dgopAvailable) {
|
|
||||||
console.warn("ProcessListModal: dgop is not available");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
hide();
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 900
|
|
||||||
height: 680
|
|
||||||
visible: false
|
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
enableShadow: true
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: processesTabComponent
|
|
||||||
|
|
||||||
ProcessesTab {
|
|
||||||
contextMenu: processContextMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: performanceTabComponent
|
|
||||||
|
|
||||||
PerformanceTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: systemTabComponent
|
|
||||||
|
|
||||||
SystemTab {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessContextMenu {
|
|
||||||
id: processContextMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: (event) => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
processListModal.hide();
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_1) {
|
|
||||||
currentTab = 0;
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_2) {
|
|
||||||
currentTab = 1;
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_3) {
|
|
||||||
currentTab = 2;
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error message when dgop is not available
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 400
|
|
||||||
height: 200
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
|
|
||||||
border.color: Theme.error
|
|
||||||
border.width: 2
|
|
||||||
visible: !DgopService.dgopAvailable
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "error"
|
|
||||||
size: 48
|
|
||||||
color: Theme.error
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("System Monitor Unavailable")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.error
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
visible: DgopService.dgopAvailable
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("System Monitor")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge + 4
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: Theme.surfaceText
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
return processListModal.hide();
|
|
||||||
}
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 52
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 4
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: tabNames
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
|
|
||||||
height: 44
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
|
||||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
|
||||||
border.width: currentTab === index ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
const tabIcons = ["list_alt", "analytics", "settings"];
|
|
||||||
return tabIcons[index] || "tab";
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
|
||||||
opacity: currentTab === index ? 1 : 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: currentTab === index ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.verticalCenterOffset: -1
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: tabMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
currentTab = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
border.color: Theme.outlineLight
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: processesTab
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: processListModal.visible && currentTab === 0
|
|
||||||
visible: currentTab === 0
|
|
||||||
opacity: currentTab === 0 ? 1 : 0
|
|
||||||
sourceComponent: processesTabComponent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: performanceTab
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: processListModal.visible && currentTab === 1
|
|
||||||
visible: currentTab === 1
|
|
||||||
opacity: currentTab === 1 ? 1 : 0
|
|
||||||
sourceComponent: performanceTabComponent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: systemTab
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
active: processListModal.visible && currentTab === 2
|
|
||||||
visible: currentTab === 2
|
|
||||||
opacity: currentTab === 2 ? 1 : 0
|
|
||||||
sourceComponent: systemTabComponent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Modals.FileBrowser
|
|
||||||
import qs.Modules.Settings
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: settingsModal
|
|
||||||
|
|
||||||
layerNamespace: "dms:settings"
|
|
||||||
|
|
||||||
property Component settingsContent
|
|
||||||
property alias profileBrowser: profileBrowser
|
|
||||||
property int currentTabIndex: 0
|
|
||||||
|
|
||||||
signal closingModal()
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
open();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
hide();
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
objectName: "settingsModal"
|
|
||||||
width: Math.min(800, screenWidth * 0.9)
|
|
||||||
height: Math.min(800, screenHeight * 0.85)
|
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
visible: false
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
return hide();
|
|
||||||
}
|
|
||||||
content: settingsContent
|
|
||||||
onOpened: () => {
|
|
||||||
Qt.callLater(() => modalFocusScope.forceActiveFocus())
|
|
||||||
}
|
|
||||||
modalFocusScope.Keys.onPressed: event => {
|
|
||||||
const tabCount = 11
|
|
||||||
if (event.key === Qt.Key_Down) {
|
|
||||||
currentTabIndex = (currentTabIndex + 1) % tabCount
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Tab && !event.modifiers) {
|
|
||||||
currentTabIndex = (currentTabIndex + 1) % tabCount
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
|
|
||||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function open(): string {
|
|
||||||
settingsModal.show();
|
|
||||||
return "SETTINGS_OPEN_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(): string {
|
|
||||||
settingsModal.hide();
|
|
||||||
return "SETTINGS_CLOSE_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(): string {
|
|
||||||
settingsModal.toggle();
|
|
||||||
return "SETTINGS_TOGGLE_SUCCESS";
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "settings"
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
function browse(type: string) {
|
|
||||||
if (type === "wallpaper") {
|
|
||||||
wallpaperBrowser.allowStacking = false;
|
|
||||||
wallpaperBrowser.open();
|
|
||||||
} else if (type === "profile") {
|
|
||||||
profileBrowser.allowStacking = false;
|
|
||||||
profileBrowser.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "file"
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: profileBrowser
|
|
||||||
|
|
||||||
allowStacking: true
|
|
||||||
parentModal: settingsModal
|
|
||||||
browserTitle: "Select Profile Image"
|
|
||||||
browserIcon: "person"
|
|
||||||
browserType: "profile"
|
|
||||||
showHiddenFiles: true
|
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
onFileSelected: (path) => {
|
|
||||||
PortalService.setProfileImage(path);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
onDialogClosed: () => {
|
|
||||||
allowStacking = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal {
|
|
||||||
id: wallpaperBrowser
|
|
||||||
|
|
||||||
allowStacking: true
|
|
||||||
parentModal: settingsModal
|
|
||||||
browserTitle: "Select Wallpaper"
|
|
||||||
browserIcon: "wallpaper"
|
|
||||||
browserType: "wallpaper"
|
|
||||||
showHiddenFiles: true
|
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
|
||||||
onFileSelected: (path) => {
|
|
||||||
SessionData.setWallpaper(path);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
onDialogClosed: () => {
|
|
||||||
allowStacking = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsContent: Component {
|
|
||||||
Item {
|
|
||||||
id: rootScope
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.rightMargin: Theme.spacingL
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
anchors.bottomMargin: Theme.spacingL
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 35
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "settings"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Settings")
|
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
circular: false
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
return settingsModal.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 35
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
SettingsSidebar {
|
|
||||||
id: sidebar
|
|
||||||
|
|
||||||
parentModal: settingsModal
|
|
||||||
currentIndex: settingsModal.currentTabIndex
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
settingsModal.currentTabIndex = currentIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsContent {
|
|
||||||
id: content
|
|
||||||
|
|
||||||
width: parent.width - sidebar.width
|
|
||||||
height: parent.height
|
|
||||||
parentModal: settingsModal
|
|
||||||
currentIndex: settingsModal.currentTabIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: resultsContainer
|
|
||||||
|
|
||||||
property var appLauncher: null
|
|
||||||
property var contextMenu: null
|
|
||||||
|
|
||||||
function resetScroll() {
|
|
||||||
resultsList.contentY = 0
|
|
||||||
resultsGrid.contentY = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
DankListView {
|
|
||||||
id: resultsList
|
|
||||||
|
|
||||||
property int itemHeight: 60
|
|
||||||
property int iconSize: 40
|
|
||||||
property bool showDescription: true
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
const itemY = index * (itemHeight + itemSpacing)
|
|
||||||
const itemBottom = itemY + itemHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
visible: appLauncher && appLauncher.viewMode === "list"
|
|
||||||
model: appLauncher ? appLauncher.model : null
|
|
||||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
|
||||||
clip: true
|
|
||||||
spacing: itemSpacing
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
onItemClicked: (index, modelData) => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
|
||||||
if (contextMenu)
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: () => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: AppLauncherListDelegate {
|
|
||||||
listView: resultsList
|
|
||||||
itemHeight: resultsList.itemHeight
|
|
||||||
iconSize: resultsList.iconSize
|
|
||||||
showDescription: resultsList.showDescription
|
|
||||||
hoverUpdatesSelection: resultsList.hoverUpdatesSelection
|
|
||||||
keyboardNavigationActive: resultsList.keyboardNavigationActive
|
|
||||||
isCurrentItem: ListView.isCurrentItem
|
|
||||||
iconMaterialSizeAdjustment: 0
|
|
||||||
iconUnicodeScale: 0.8
|
|
||||||
onItemClicked: (idx, modelData) => resultsList.itemClicked(idx, modelData)
|
|
||||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
|
||||||
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
|
|
||||||
resultsList.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: resultsList.keyboardNavigationReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankGridView {
|
|
||||||
id: resultsGrid
|
|
||||||
|
|
||||||
property int currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
|
||||||
property int columns: 4
|
|
||||||
property bool adaptiveColumns: false
|
|
||||||
property int minCellWidth: 120
|
|
||||||
property int maxCellWidth: 160
|
|
||||||
property int cellPadding: 8
|
|
||||||
property real iconSizeRatio: 0.55
|
|
||||||
property int maxIconSize: 48
|
|
||||||
property int minIconSize: 32
|
|
||||||
property bool hoverUpdatesSelection: false
|
|
||||||
property bool keyboardNavigationActive: appLauncher ? appLauncher.keyboardNavigationActive : false
|
|
||||||
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
|
||||||
property int baseCellHeight: baseCellWidth + 20
|
|
||||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
|
||||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
|
||||||
|
|
||||||
signal keyboardNavigationReset
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= count)
|
|
||||||
return
|
|
||||||
|
|
||||||
const itemY = Math.floor(index / actualColumns) * cellHeight
|
|
||||||
const itemBottom = itemY + cellHeight
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
visible: appLauncher && appLauncher.viewMode === "grid"
|
|
||||||
model: appLauncher ? appLauncher.model : null
|
|
||||||
clip: true
|
|
||||||
cellWidth: baseCellWidth
|
|
||||||
cellHeight: baseCellHeight
|
|
||||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
|
||||||
rightMargin: leftMargin
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
|
||||||
reuseItems: true
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex)
|
|
||||||
}
|
|
||||||
onItemClicked: (index, modelData) => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.launchApp(modelData)
|
|
||||||
}
|
|
||||||
onItemRightClicked: (index, modelData, mouseX, mouseY) => {
|
|
||||||
if (contextMenu)
|
|
||||||
contextMenu.show(mouseX, mouseY, modelData)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: () => {
|
|
||||||
if (appLauncher)
|
|
||||||
appLauncher.keyboardNavigationActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: AppLauncherGridDelegate {
|
|
||||||
gridView: resultsGrid
|
|
||||||
cellWidth: resultsGrid.cellWidth
|
|
||||||
cellHeight: resultsGrid.cellHeight
|
|
||||||
cellPadding: resultsGrid.cellPadding
|
|
||||||
minIconSize: resultsGrid.minIconSize
|
|
||||||
maxIconSize: resultsGrid.maxIconSize
|
|
||||||
iconSizeRatio: resultsGrid.iconSizeRatio
|
|
||||||
hoverUpdatesSelection: resultsGrid.hoverUpdatesSelection
|
|
||||||
keyboardNavigationActive: resultsGrid.keyboardNavigationActive
|
|
||||||
currentIndex: resultsGrid.currentIndex
|
|
||||||
onItemClicked: (idx, modelData) => resultsGrid.itemClicked(idx, modelData)
|
|
||||||
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
|
|
||||||
const modalPos = resultsContainer.parent.mapFromItem(null, mouseX, mouseY)
|
|
||||||
resultsGrid.itemRightClicked(idx, modelData, modalPos.x, modalPos.y)
|
|
||||||
}
|
|
||||||
onKeyboardNavigationReset: resultsGrid.keyboardNavigationReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,594 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modals.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
layerNamespace: "dms:wifi-password"
|
|
||||||
|
|
||||||
property string wifiPasswordSSID: ""
|
|
||||||
property string wifiPasswordInput: ""
|
|
||||||
property string wifiUsernameInput: ""
|
|
||||||
property bool requiresEnterprise: false
|
|
||||||
|
|
||||||
property string wifiAnonymousIdentityInput: ""
|
|
||||||
property string wifiDomainInput: ""
|
|
||||||
|
|
||||||
property bool isPromptMode: false
|
|
||||||
property string promptToken: ""
|
|
||||||
property string promptReason: ""
|
|
||||||
property var promptFields: []
|
|
||||||
property string promptSetting: ""
|
|
||||||
|
|
||||||
property bool isVpnPrompt: false
|
|
||||||
property string connectionName: ""
|
|
||||||
property string vpnServiceType: ""
|
|
||||||
property string connectionType: ""
|
|
||||||
|
|
||||||
function show(ssid) {
|
|
||||||
wifiPasswordSSID = ssid
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
isPromptMode = false
|
|
||||||
promptToken = ""
|
|
||||||
promptReason = ""
|
|
||||||
promptFields = []
|
|
||||||
promptSetting = ""
|
|
||||||
isVpnPrompt = false
|
|
||||||
connectionName = ""
|
|
||||||
vpnServiceType = ""
|
|
||||||
connectionType = ""
|
|
||||||
|
|
||||||
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
|
|
||||||
requiresEnterprise = network?.enterprise || false
|
|
||||||
|
|
||||||
open()
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item) {
|
|
||||||
if (requiresEnterprise && contentLoader.item.usernameInput) {
|
|
||||||
contentLoader.item.usernameInput.forceActiveFocus()
|
|
||||||
} else if (contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
|
|
||||||
isPromptMode = true
|
|
||||||
promptToken = token
|
|
||||||
promptReason = reason
|
|
||||||
promptFields = fields || []
|
|
||||||
promptSetting = setting || "802-11-wireless-security"
|
|
||||||
connectionType = connType || "802-11-wireless"
|
|
||||||
connectionName = connName || ssid || ""
|
|
||||||
vpnServiceType = vpnService || ""
|
|
||||||
|
|
||||||
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard")
|
|
||||||
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid
|
|
||||||
|
|
||||||
requiresEnterprise = setting === "802-1x"
|
|
||||||
|
|
||||||
if (reason === "wrong-password") {
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
} else {
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
open()
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item) {
|
|
||||||
if (reason === "wrong-password" && contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.text = ""
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
} else if (requiresEnterprise && contentLoader.item.usernameInput) {
|
|
||||||
contentLoader.item.usernameInput.forceActiveFocus()
|
|
||||||
} else if (contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeVisible: false
|
|
||||||
width: 420
|
|
||||||
height: requiresEnterprise ? 430 : 230
|
|
||||||
onShouldBeVisibleChanged: () => {
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onOpened: {
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (contentLoader.item) {
|
|
||||||
if (requiresEnterprise && contentLoader.item.usernameInput) {
|
|
||||||
contentLoader.item.usernameInput.forceActiveFocus()
|
|
||||||
} else if (contentLoader.item.passwordInput) {
|
|
||||||
contentLoader.item.passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onBackgroundClicked: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
NetworkService.cancelCredentials(promptToken)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: NetworkService
|
|
||||||
|
|
||||||
function onPasswordDialogShouldReopenChanged() {
|
|
||||||
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
|
|
||||||
wifiPasswordSSID = NetworkService.connectingSSID
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
open()
|
|
||||||
NetworkService.passwordDialogShouldReopen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
FocusScope {
|
|
||||||
id: wifiContent
|
|
||||||
|
|
||||||
property alias usernameInput: usernameInput
|
|
||||||
property alias passwordInput: passwordInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
NetworkService.cancelCredentials(promptToken)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 40
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (isVpnPrompt) {
|
|
||||||
return I18n.tr("Connect to VPN")
|
|
||||||
}
|
|
||||||
return I18n.tr("Connect to Wi-Fi")
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (isVpnPrompt) {
|
|
||||||
return I18n.tr("Enter password for ") + wifiPasswordSSID
|
|
||||||
}
|
|
||||||
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ")
|
|
||||||
return prefix + wifiPasswordSSID
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceTextMedium
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
visible: isPromptMode && promptReason === "wrong-password"
|
|
||||||
text: I18n.tr("Incorrect password")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.error
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
NetworkService.cancelCredentials(promptToken)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: usernameInput.activeFocus ? 2 : 1
|
|
||||||
visible: requiresEnterprise && !isVpnPrompt
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: () => {
|
|
||||||
usernameInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: usernameInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiUsernameInput
|
|
||||||
placeholderText: I18n.tr("Username")
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiUsernameInput = text
|
|
||||||
}
|
|
||||||
onAccepted: () => {
|
|
||||||
if (passwordInput) {
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: passwordInput.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: () => {
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: passwordInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiPasswordInput
|
|
||||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
|
||||||
placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
focus: !requiresEnterprise
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiPasswordInput = text
|
|
||||||
}
|
|
||||||
onAccepted: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
const secrets = {}
|
|
||||||
if (isVpnPrompt) {
|
|
||||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
|
||||||
} else if (promptSetting === "802-11-wireless-security") {
|
|
||||||
secrets["psk"] = passwordInput.text
|
|
||||||
} else if (promptSetting === "802-1x") {
|
|
||||||
if (usernameInput.text) secrets["identity"] = usernameInput.text
|
|
||||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
|
||||||
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
|
|
||||||
}
|
|
||||||
NetworkService.submitCredentials(promptToken, secrets, true)
|
|
||||||
} else {
|
|
||||||
const username = requiresEnterprise ? usernameInput.text : ""
|
|
||||||
NetworkService.connectToWifi(
|
|
||||||
wifiPasswordSSID,
|
|
||||||
passwordInput.text,
|
|
||||||
username,
|
|
||||||
wifiAnonymousIdentityInput,
|
|
||||||
wifiDomainInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
passwordInput.text = ""
|
|
||||||
if (requiresEnterprise) usernameInput.text = ""
|
|
||||||
}
|
|
||||||
Component.onCompleted: () => {
|
|
||||||
if (root.shouldBeVisible && !requiresEnterprise)
|
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: focusDelayTimer
|
|
||||||
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
onTriggered: () => {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
if (requiresEnterprise && usernameInput) {
|
|
||||||
usernameInput.forceActiveFocus()
|
|
||||||
} else {
|
|
||||||
passwordInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
if (root.shouldBeVisible)
|
|
||||||
focusDelayTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: requiresEnterprise && !isVpnPrompt
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: anonInput.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: () => {
|
|
||||||
anonInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: anonInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiAnonymousIdentityInput
|
|
||||||
placeholderText: I18n.tr("Anonymous Identity (optional)")
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiAnonymousIdentityInput = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: requiresEnterprise && !isVpnPrompt
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceHover
|
|
||||||
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
|
||||||
border.width: domainMatchInput.activeFocus ? 2 : 1
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: () => {
|
|
||||||
domainMatchInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankTextField {
|
|
||||||
id: domainMatchInput
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: wifiDomainInput
|
|
||||||
placeholderText: I18n.tr("Domain (optional)")
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: root.shouldBeVisible
|
|
||||||
onTextEdited: () => {
|
|
||||||
wifiDomainInput = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: showPasswordCheckbox
|
|
||||||
|
|
||||||
property bool checked: false
|
|
||||||
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
radius: 4
|
|
||||||
color: checked ? Theme.primary : "transparent"
|
|
||||||
border.color: checked ? Theme.primary : Theme.outlineButton
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "check"
|
|
||||||
size: 12
|
|
||||||
color: Theme.background
|
|
||||||
visible: parent.checked
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Show password")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
|
||||||
border.color: Theme.surfaceVariantAlpha
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: cancelText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: I18n.tr("Cancel")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: cancelArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
NetworkService.cancelCredentials(promptToken)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
|
||||||
height: 36
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
|
||||||
enabled: {
|
|
||||||
if (isVpnPrompt) {
|
|
||||||
return passwordInput.text.length > 0
|
|
||||||
}
|
|
||||||
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0
|
|
||||||
}
|
|
||||||
opacity: enabled ? 1 : 0.5
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: connectText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: I18n.tr("Connect")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.background
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: connectArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: parent.enabled
|
|
||||||
onClicked: () => {
|
|
||||||
if (isPromptMode) {
|
|
||||||
const secrets = {}
|
|
||||||
if (isVpnPrompt) {
|
|
||||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
|
||||||
} else if (promptSetting === "802-11-wireless-security") {
|
|
||||||
secrets["psk"] = passwordInput.text
|
|
||||||
} else if (promptSetting === "802-1x") {
|
|
||||||
if (usernameInput.text) secrets["identity"] = usernameInput.text
|
|
||||||
if (passwordInput.text) secrets["password"] = passwordInput.text
|
|
||||||
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
|
|
||||||
}
|
|
||||||
NetworkService.submitCredentials(promptToken, secrets, true)
|
|
||||||
} else {
|
|
||||||
const username = requiresEnterprise ? usernameInput.text : ""
|
|
||||||
NetworkService.connectToWifi(
|
|
||||||
wifiPasswordSSID,
|
|
||||||
passwordInput.text,
|
|
||||||
username,
|
|
||||||
wifiAnonymousIdentityInput,
|
|
||||||
wifiDomainInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
wifiPasswordInput = ""
|
|
||||||
wifiUsernameInput = ""
|
|
||||||
wifiAnonymousIdentityInput = ""
|
|
||||||
wifiDomainInput = ""
|
|
||||||
passwordInput.text = ""
|
|
||||||
if (requiresEnterprise) usernameInput.text = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property string powerOptionsText: I18n.tr("Power Options")
|
|
||||||
readonly property string logOutText: I18n.tr("Log Out")
|
|
||||||
readonly property string suspendText: I18n.tr("Suspend")
|
|
||||||
readonly property string rebootText: I18n.tr("Reboot")
|
|
||||||
readonly property string powerOffText: I18n.tr("Power Off")
|
|
||||||
|
|
||||||
property bool powerMenuVisible: false
|
|
||||||
signal powerActionRequested(string action, string title, string message)
|
|
||||||
|
|
||||||
visible: powerMenuVisible
|
|
||||||
implicitWidth: 400
|
|
||||||
implicitHeight: 320
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.min(320, parent.width - Theme.spacingL * 2)
|
|
||||||
height: 320 // Fixed height to prevent cropping
|
|
||||||
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
|
||||||
y: Theme.barHeight + Theme.spacingXS
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 0
|
|
||||||
opacity: powerMenuVisible ? 1 : 0
|
|
||||||
scale: powerMenuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.powerOptionsText
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.logOutText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.powerActionRequested(
|
|
||||||
"logout", "Log Out",
|
|
||||||
"Are you sure you want to log out?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.suspendText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.powerActionRequested(
|
|
||||||
"suspend", "Suspend",
|
|
||||||
"Are you sure you want to suspend the system?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
|
|
||||||
Theme.warning.g,
|
|
||||||
Theme.warning.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "restart_alt"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.rebootText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.powerActionRequested(
|
|
||||||
"reboot", "Reboot",
|
|
||||||
"Are you sure you want to reboot the system?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b,
|
|
||||||
0.08) : Qt.rgba(
|
|
||||||
Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b,
|
|
||||||
0.08)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power_settings_new"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.powerOffText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: powerOffArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
powerMenuVisible = false
|
|
||||||
root.powerActionRequested(
|
|
||||||
"poweroff", "Power Off",
|
|
||||||
"Are you sure you want to power off the system?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property var barWindow
|
|
||||||
required property var axis
|
|
||||||
required property var appDrawerLoader
|
|
||||||
required property var dankDashPopoutLoader
|
|
||||||
required property var processListPopoutLoader
|
|
||||||
required property var notificationCenterLoader
|
|
||||||
required property var batteryPopoutLoader
|
|
||||||
required property var vpnPopoutLoader
|
|
||||||
required property var controlCenterLoader
|
|
||||||
required property var clipboardHistoryModalPopup
|
|
||||||
required property var systemUpdateLoader
|
|
||||||
required property var notepadInstance
|
|
||||||
|
|
||||||
property alias reveal: core.reveal
|
|
||||||
property alias autoHide: core.autoHide
|
|
||||||
property alias backgroundTransparency: core.backgroundTransparency
|
|
||||||
property alias hasActivePopout: core.hasActivePopout
|
|
||||||
property alias mouseArea: topBarMouseArea
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: inputMask
|
|
||||||
|
|
||||||
readonly property int barThickness: barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing)
|
|
||||||
|
|
||||||
readonly property bool showing: SettingsData.dankBarVisible && (core.reveal
|
|
||||||
|| (CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview)
|
|
||||||
|| !core.autoHide)
|
|
||||||
|
|
||||||
readonly property int maskThickness: showing ? barThickness : 1
|
|
||||||
|
|
||||||
x: {
|
|
||||||
if (!axis.isVertical) {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
switch (SettingsData.dankBarPosition) {
|
|
||||||
case SettingsData.Position.Left: return 0
|
|
||||||
case SettingsData.Position.Right: return parent.width - maskThickness
|
|
||||||
default: return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (axis.isVertical) {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
switch (SettingsData.dankBarPosition) {
|
|
||||||
case SettingsData.Position.Top: return 0
|
|
||||||
case SettingsData.Position.Bottom: return parent.height - maskThickness
|
|
||||||
default: return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width: axis.isVertical ? maskThickness : parent.width
|
|
||||||
height: axis.isVertical ? parent.height : maskThickness
|
|
||||||
}
|
|
||||||
|
|
||||||
Region {
|
|
||||||
id: mask
|
|
||||||
item: inputMask
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias maskRegion: mask
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: core
|
|
||||||
|
|
||||||
property real backgroundTransparency: SettingsData.dankBarTransparency
|
|
||||||
property bool autoHide: SettingsData.dankBarAutoHide
|
|
||||||
property bool revealSticky: false
|
|
||||||
|
|
||||||
property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false
|
|
||||||
|
|
||||||
readonly property bool hasActivePopout: {
|
|
||||||
const loaders = [{
|
|
||||||
"loader": appDrawerLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": dankDashPopoutLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": processListPopoutLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": notificationCenterLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": batteryPopoutLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": vpnPopoutLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": controlCenterLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}, {
|
|
||||||
"loader": clipboardHistoryModalPopup,
|
|
||||||
"prop": "visible"
|
|
||||||
}, {
|
|
||||||
"loader": systemUpdateLoader,
|
|
||||||
"prop": "shouldBeVisible"
|
|
||||||
}]
|
|
||||||
return notepadInstanceVisible || loaders.some(item => {
|
|
||||||
if (item.loader) {
|
|
||||||
return item.loader?.item?.[item.prop]
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool reveal: {
|
|
||||||
if (CompositorService.isNiri && NiriService.inOverview) {
|
|
||||||
return SettingsData.dankBarOpenOnOverview
|
|
||||||
}
|
|
||||||
return SettingsData.dankBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky)
|
|
||||||
}
|
|
||||||
|
|
||||||
onHasActivePopoutChanged: {
|
|
||||||
if (!hasActivePopout && autoHide && !topBarMouseArea.containsMouse) {
|
|
||||||
revealSticky = true
|
|
||||||
revealHold.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: revealHold
|
|
||||||
interval: 250
|
|
||||||
repeat: false
|
|
||||||
onTriggered: core.revealSticky = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onDankBarTransparencyChanged() {
|
|
||||||
core.backgroundTransparency = SettingsData.dankBarTransparency
|
|
||||||
}
|
|
||||||
|
|
||||||
target: SettingsData
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: topBarMouseArea
|
|
||||||
function onContainsMouseChanged() {
|
|
||||||
if (topBarMouseArea.containsMouse) {
|
|
||||||
core.revealSticky = true
|
|
||||||
revealHold.stop()
|
|
||||||
} else {
|
|
||||||
if (core.autoHide && !core.hasActivePopout) {
|
|
||||||
revealHold.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: topBarMouseArea
|
|
||||||
y: !barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? parent.height - height : 0) : 0
|
|
||||||
x: barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.width - width : 0) : 0
|
|
||||||
height: !barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
|
|
||||||
width: barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
|
|
||||||
anchors {
|
|
||||||
left: !barWindow.isVertical ? parent.left : (SettingsData.dankBarPosition === SettingsData.Position.Left ? parent.left : undefined)
|
|
||||||
right: !barWindow.isVertical ? parent.right : (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.right : undefined)
|
|
||||||
top: barWindow.isVertical ? parent.top : undefined
|
|
||||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
|
||||||
}
|
|
||||||
hoverEnabled: SettingsData.dankBarAutoHide && !core.reveal
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
enabled: SettingsData.dankBarAutoHide && !core.reveal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,498 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var widgetsModel: null
|
|
||||||
property var components: null
|
|
||||||
property bool noBackground: false
|
|
||||||
required property var axis
|
|
||||||
property string section: "center"
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
property bool overrideAxisLayout: false
|
|
||||||
property bool forceVerticalLayout: false
|
|
||||||
|
|
||||||
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
|
|
||||||
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
|
|
||||||
|
|
||||||
property var centerWidgets: []
|
|
||||||
property int totalWidgets: 0
|
|
||||||
property real totalSize: 0
|
|
||||||
|
|
||||||
function updateLayout() {
|
|
||||||
const containerSize = isVertical ? height : width
|
|
||||||
if (containerSize <= 0 || !visible) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
centerWidgets = []
|
|
||||||
totalWidgets = 0
|
|
||||||
totalSize = 0
|
|
||||||
|
|
||||||
let configuredWidgets = 0
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
const item = centerRepeater.itemAt(i)
|
|
||||||
if (item && getWidgetVisible(item.widgetId)) {
|
|
||||||
configuredWidgets++
|
|
||||||
if (item.active && item.item) {
|
|
||||||
centerWidgets.push(item.item)
|
|
||||||
totalWidgets++
|
|
||||||
totalSize += isVertical ? item.item.height : item.item.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWidgets > 1) {
|
|
||||||
totalSize += spacing * (totalWidgets - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
positionWidgets(configuredWidgets)
|
|
||||||
}
|
|
||||||
|
|
||||||
function positionWidgets(configuredWidgets) {
|
|
||||||
if (totalWidgets === 0 || (isVertical ? height : width) <= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentCenter = (isVertical ? height : width) / 2
|
|
||||||
const isOdd = configuredWidgets % 2 === 1
|
|
||||||
|
|
||||||
centerWidgets.forEach(widget => {
|
|
||||||
if (isVertical) {
|
|
||||||
widget.anchors.verticalCenter = undefined
|
|
||||||
} else {
|
|
||||||
widget.anchors.horizontalCenter = undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isOdd) {
|
|
||||||
const middleIndex = Math.floor(configuredWidgets / 2)
|
|
||||||
let currentActiveIndex = 0
|
|
||||||
let middleWidget = null
|
|
||||||
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
const item = centerRepeater.itemAt(i)
|
|
||||||
if (item && getWidgetVisible(item.widgetId)) {
|
|
||||||
if (currentActiveIndex === middleIndex && item.active && item.item) {
|
|
||||||
middleWidget = item.item
|
|
||||||
break
|
|
||||||
}
|
|
||||||
currentActiveIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (middleWidget) {
|
|
||||||
const middleSize = isVertical ? middleWidget.height : middleWidget.width
|
|
||||||
if (isVertical) {
|
|
||||||
middleWidget.y = parentCenter - (middleSize / 2)
|
|
||||||
} else {
|
|
||||||
middleWidget.x = parentCenter - (middleSize / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
let leftWidgets = []
|
|
||||||
let rightWidgets = []
|
|
||||||
let foundMiddle = false
|
|
||||||
|
|
||||||
for (var i = 0; i < centerWidgets.length; i++) {
|
|
||||||
if (centerWidgets[i] === middleWidget) {
|
|
||||||
foundMiddle = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!foundMiddle) {
|
|
||||||
leftWidgets.push(centerWidgets[i])
|
|
||||||
} else {
|
|
||||||
rightWidgets.push(centerWidgets[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPos = isVertical ? middleWidget.y : middleWidget.x
|
|
||||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
|
||||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
|
||||||
currentPos -= (spacing + size)
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
leftWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
|
|
||||||
for (var i = 0; i < rightWidgets.length; i++) {
|
|
||||||
currentPos += spacing
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
rightWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let configuredLeftIndex = (configuredWidgets / 2) - 1
|
|
||||||
let configuredRightIndex = configuredWidgets / 2
|
|
||||||
const halfSpacing = spacing / 2
|
|
||||||
|
|
||||||
let leftWidget = null
|
|
||||||
let rightWidget = null
|
|
||||||
let leftWidgets = []
|
|
||||||
let rightWidgets = []
|
|
||||||
|
|
||||||
let currentConfigIndex = 0
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
const item = centerRepeater.itemAt(i)
|
|
||||||
if (item && getWidgetVisible(item.widgetId)) {
|
|
||||||
if (item.active && item.item) {
|
|
||||||
if (currentConfigIndex < configuredLeftIndex) {
|
|
||||||
leftWidgets.push(item.item)
|
|
||||||
} else if (currentConfigIndex === configuredLeftIndex) {
|
|
||||||
leftWidget = item.item
|
|
||||||
} else if (currentConfigIndex === configuredRightIndex) {
|
|
||||||
rightWidget = item.item
|
|
||||||
} else {
|
|
||||||
rightWidgets.push(item.item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentConfigIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftWidget && rightWidget) {
|
|
||||||
const leftSize = isVertical ? leftWidget.height : leftWidget.width
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidget.y = parentCenter - halfSpacing - leftSize
|
|
||||||
rightWidget.y = parentCenter + halfSpacing
|
|
||||||
} else {
|
|
||||||
leftWidget.x = parentCenter - halfSpacing - leftSize
|
|
||||||
rightWidget.x = parentCenter + halfSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPos = isVertical ? leftWidget.y : leftWidget.x
|
|
||||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
|
||||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
|
||||||
currentPos -= (spacing + size)
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
leftWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
|
|
||||||
for (var i = 0; i < rightWidgets.length; i++) {
|
|
||||||
currentPos += spacing
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
rightWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
|
||||||
}
|
|
||||||
} else if (leftWidget && !rightWidget) {
|
|
||||||
const leftSize = isVertical ? leftWidget.height : leftWidget.width
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidget.y = parentCenter - halfSpacing - leftSize
|
|
||||||
} else {
|
|
||||||
leftWidget.x = parentCenter - halfSpacing - leftSize
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPos = isVertical ? leftWidget.y : leftWidget.x
|
|
||||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
|
||||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
|
||||||
currentPos -= (spacing + size)
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
leftWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPos = (isVertical ? leftWidget.y + leftWidget.height : leftWidget.x + leftWidget.width) + spacing
|
|
||||||
for (var i = 0; i < rightWidgets.length; i++) {
|
|
||||||
currentPos += spacing
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
rightWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
|
||||||
}
|
|
||||||
} else if (!leftWidget && rightWidget) {
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidget.y = parentCenter + halfSpacing
|
|
||||||
} else {
|
|
||||||
rightWidget.x = parentCenter + halfSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentPos = (isVertical ? rightWidget.y : rightWidget.x) - spacing
|
|
||||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
|
||||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
|
||||||
currentPos -= size
|
|
||||||
if (isVertical) {
|
|
||||||
leftWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
leftWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos -= spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
|
|
||||||
for (var i = 0; i < rightWidgets.length; i++) {
|
|
||||||
currentPos += spacing
|
|
||||||
if (isVertical) {
|
|
||||||
rightWidgets[i].y = currentPos
|
|
||||||
} else {
|
|
||||||
rightWidgets[i].x = currentPos
|
|
||||||
}
|
|
||||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
|
||||||
}
|
|
||||||
} else if (totalWidgets === 1 && centerWidgets[0]) {
|
|
||||||
const size = isVertical ? centerWidgets[0].height : centerWidgets[0].width
|
|
||||||
if (isVertical) {
|
|
||||||
centerWidgets[0].y = parentCenter - (size / 2)
|
|
||||||
} else {
|
|
||||||
centerWidgets[0].x = parentCenter - (size / 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWidgetVisible(widgetId) {
|
|
||||||
const widgetVisibility = {
|
|
||||||
"cpuUsage": DgopService.dgopAvailable,
|
|
||||||
"memUsage": DgopService.dgopAvailable,
|
|
||||||
"cpuTemp": DgopService.dgopAvailable,
|
|
||||||
"gpuTemp": DgopService.dgopAvailable,
|
|
||||||
"network_speed_monitor": DgopService.dgopAvailable
|
|
||||||
}
|
|
||||||
return widgetVisibility[widgetId] ?? true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWidgetComponent(widgetId) {
|
|
||||||
// Build dynamic component map including plugins
|
|
||||||
let baseMap = {
|
|
||||||
"launcherButton": "launcherButtonComponent",
|
|
||||||
"workspaceSwitcher": "workspaceSwitcherComponent",
|
|
||||||
"focusedWindow": "focusedWindowComponent",
|
|
||||||
"runningApps": "runningAppsComponent",
|
|
||||||
"clock": "clockComponent",
|
|
||||||
"music": "mediaComponent",
|
|
||||||
"weather": "weatherComponent",
|
|
||||||
"systemTray": "systemTrayComponent",
|
|
||||||
"privacyIndicator": "privacyIndicatorComponent",
|
|
||||||
"clipboard": "clipboardComponent",
|
|
||||||
"cpuUsage": "cpuUsageComponent",
|
|
||||||
"memUsage": "memUsageComponent",
|
|
||||||
"diskUsage": "diskUsageComponent",
|
|
||||||
"cpuTemp": "cpuTempComponent",
|
|
||||||
"gpuTemp": "gpuTempComponent",
|
|
||||||
"notificationButton": "notificationButtonComponent",
|
|
||||||
"battery": "batteryComponent",
|
|
||||||
"controlCenterButton": "controlCenterButtonComponent",
|
|
||||||
"idleInhibitor": "idleInhibitorComponent",
|
|
||||||
"spacer": "spacerComponent",
|
|
||||||
"separator": "separatorComponent",
|
|
||||||
"network_speed_monitor": "networkComponent",
|
|
||||||
"keyboard_layout_name": "keyboardLayoutNameComponent",
|
|
||||||
"vpn": "vpnComponent",
|
|
||||||
"notepadButton": "notepadButtonComponent",
|
|
||||||
"colorPicker": "colorPickerComponent",
|
|
||||||
"systemUpdate": "systemUpdateComponent"
|
|
||||||
}
|
|
||||||
|
|
||||||
// For built-in components, get from components property
|
|
||||||
const componentKey = baseMap[widgetId]
|
|
||||||
if (componentKey && root.components[componentKey]) {
|
|
||||||
return root.components[componentKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
// For plugin components, get from PluginService
|
|
||||||
var parts = widgetId.split(":")
|
|
||||||
var pluginId = parts[0]
|
|
||||||
let pluginComponents = PluginService.getWidgetComponents()
|
|
||||||
return pluginComponents[pluginId] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
height: parent.height
|
|
||||||
width: parent.width
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: layoutTimer
|
|
||||||
interval: 0
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.updateLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
onWidthChanged: {
|
|
||||||
if (width > 0) {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHeightChanged: {
|
|
||||||
if (height > 0) {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && (isVertical ? height : width) > 0) {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: centerRepeater
|
|
||||||
model: root.widgetsModel
|
|
||||||
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
property string widgetId: model.widgetId
|
|
||||||
property var widgetData: model
|
|
||||||
property int spacerSize: model.size || 20
|
|
||||||
|
|
||||||
anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined
|
|
||||||
anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined
|
|
||||||
active: root.getWidgetVisible(model.widgetId) && (model.widgetId !== "music" || MprisController.activePlayer !== null)
|
|
||||||
sourceComponent: root.getWidgetComponent(model.widgetId)
|
|
||||||
opacity: (model.enabled !== false) ? 1 : 0
|
|
||||||
asynchronous: false
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (!item) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
item.widthChanged.connect(() => layoutTimer.restart())
|
|
||||||
item.heightChanged.connect(() => layoutTimer.restart())
|
|
||||||
if (root.axis && "axis" in item) {
|
|
||||||
item.axis = Qt.binding(() => root.axis)
|
|
||||||
}
|
|
||||||
if (root.axis && "isVertical" in item) {
|
|
||||||
try {
|
|
||||||
item.isVertical = Qt.binding(() => root.axis.isVertical)
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject properties for plugin widgets
|
|
||||||
if ("section" in item) {
|
|
||||||
item.section = root.section
|
|
||||||
}
|
|
||||||
if ("parentScreen" in item) {
|
|
||||||
item.parentScreen = Qt.binding(() => root.parentScreen)
|
|
||||||
}
|
|
||||||
if ("widgetThickness" in item) {
|
|
||||||
item.widgetThickness = Qt.binding(() => root.widgetThickness)
|
|
||||||
}
|
|
||||||
if ("barThickness" in item) {
|
|
||||||
item.barThickness = Qt.binding(() => root.barThickness)
|
|
||||||
}
|
|
||||||
if ("sectionSpacing" in item) {
|
|
||||||
item.sectionSpacing = Qt.binding(() => root.spacing)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("isFirst" in item) {
|
|
||||||
item.isFirst = Qt.binding(() => {
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
const checkItem = centerRepeater.itemAt(i)
|
|
||||||
if (checkItem && checkItem.active && checkItem.item) {
|
|
||||||
return checkItem.item === item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("isLast" in item) {
|
|
||||||
item.isLast = Qt.binding(() => {
|
|
||||||
for (var i = centerRepeater.count - 1; i >= 0; i--) {
|
|
||||||
const checkItem = centerRepeater.itemAt(i)
|
|
||||||
if (checkItem && checkItem.active && checkItem.item) {
|
|
||||||
return checkItem.item === item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("isLeftBarEdge" in item) {
|
|
||||||
item.isLeftBarEdge = false
|
|
||||||
}
|
|
||||||
if ("isRightBarEdge" in item) {
|
|
||||||
item.isRightBarEdge = false
|
|
||||||
}
|
|
||||||
if ("isTopBarEdge" in item) {
|
|
||||||
item.isTopBarEdge = false
|
|
||||||
}
|
|
||||||
if ("isBottomBarEdge" in item) {
|
|
||||||
item.isBottomBarEdge = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.pluginService !== undefined) {
|
|
||||||
var parts = model.widgetId.split(":")
|
|
||||||
var pluginId = parts[0]
|
|
||||||
var variantId = parts.length > 1 ? parts[1] : null
|
|
||||||
|
|
||||||
if (item.pluginId !== undefined) {
|
|
||||||
item.pluginId = pluginId
|
|
||||||
}
|
|
||||||
if (item.variantId !== undefined) {
|
|
||||||
item.variantId = variantId
|
|
||||||
}
|
|
||||||
if (item.variantData !== undefined && variantId) {
|
|
||||||
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
|
|
||||||
}
|
|
||||||
item.pluginService = PluginService
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.popoutService !== undefined) {
|
|
||||||
item.popoutService = PopoutService
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
onActiveChanged: {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: widgetsModel
|
|
||||||
function onCountChanged() {
|
|
||||||
layoutTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for plugin changes and refresh components
|
|
||||||
Connections {
|
|
||||||
target: PluginService
|
|
||||||
function onPluginLoaded(pluginId) {
|
|
||||||
// Force refresh of component lookups
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
var item = centerRepeater.itemAt(i)
|
|
||||||
if (item && item.widgetId.startsWith(pluginId)) {
|
|
||||||
item.sourceComponent = root.getWidgetComponent(item.widgetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onPluginUnloaded(pluginId) {
|
|
||||||
// Force refresh of component lookups
|
|
||||||
for (var i = 0; i < centerRepeater.count; i++) {
|
|
||||||
var item = centerRepeater.itemAt(i)
|
|
||||||
if (item && item.widgetId.startsWith(pluginId)) {
|
|
||||||
item.sourceComponent = root.getWidgetComponent(item.widgetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,691 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.SystemTray
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isVertical: axis?.isVertical ?? false
|
|
||||||
property var axis: null
|
|
||||||
property var parentWindow: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property real widgetThickness: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
property bool isAtBottom: false
|
|
||||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
|
|
||||||
readonly property var hiddenTrayIds: {
|
|
||||||
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || ""
|
|
||||||
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : []
|
|
||||||
}
|
|
||||||
readonly property var visibleTrayItems: {
|
|
||||||
if (!hiddenTrayIds.length) {
|
|
||||||
return SystemTray.items.values
|
|
||||||
}
|
|
||||||
return SystemTray.items.values.filter(item => {
|
|
||||||
const itemId = item?.id || ""
|
|
||||||
return !hiddenTrayIds.includes(itemId.toLowerCase())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
readonly property int calculatedSize: visibleTrayItems.length > 0 ? visibleTrayItems.length * 24 + horizontalPadding * 2 : 0
|
|
||||||
readonly property real visualWidth: isVertical ? widgetThickness : calculatedSize
|
|
||||||
readonly property real visualHeight: isVertical ? calculatedSize : widgetThickness
|
|
||||||
|
|
||||||
width: isVertical ? barThickness : visualWidth
|
|
||||||
height: isVertical ? visualHeight : barThickness
|
|
||||||
visible: visibleTrayItems.length > 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualBackground
|
|
||||||
width: root.visualWidth
|
|
||||||
height: root.visualHeight
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (visibleTrayItems.length === 0) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsData.dankBarNoBackground) {
|
|
||||||
return "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor;
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: layoutLoader
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: root.isVertical ? columnComp : rowComp
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: rowComp
|
|
||||||
Row {
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.visibleTrayItems
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateRoot
|
|
||||||
property var trayItem: modelData
|
|
||||||
property string iconSource: {
|
|
||||||
let icon = trayItem && trayItem.icon;
|
|
||||||
if (typeof icon === 'string' || icon instanceof String) {
|
|
||||||
if (icon === "") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const split = icon.split("?path=");
|
|
||||||
if (split.length !== 2) {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = split[0];
|
|
||||||
const path = split[1];
|
|
||||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
if (fileName.startsWith("dropboxstatus")) {
|
|
||||||
fileName = `hicolor/16x16/status/${fileName}`;
|
|
||||||
}
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
if (icon.startsWith("/") && !icon.startsWith("file://")) {
|
|
||||||
return `file://${icon}`;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 24
|
|
||||||
height: root.barThickness
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: Theme.barIconSize(root.barThickness)
|
|
||||||
height: Theme.barIconSize(root.barThickness)
|
|
||||||
source: delegateRoot.iconSource
|
|
||||||
asynchronous: true
|
|
||||||
smooth: true
|
|
||||||
mipmap: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: !iconImg.visible
|
|
||||||
text: {
|
|
||||||
const itemId = trayItem?.id || ""
|
|
||||||
if (!itemId) {
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
return itemId.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: trayItemArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
if (!delegateRoot.trayItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton && !delegateRoot.trayItem.onlyMenu) {
|
|
||||||
delegateRoot.trayItem.activate();
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (delegateRoot.trayItem.hasMenu) {
|
|
||||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: columnComp
|
|
||||||
Column {
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.visibleTrayItems
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateRoot
|
|
||||||
property var trayItem: modelData
|
|
||||||
property string iconSource: {
|
|
||||||
let icon = trayItem && trayItem.icon;
|
|
||||||
if (typeof icon === 'string' || icon instanceof String) {
|
|
||||||
if (icon === "") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const split = icon.split("?path=");
|
|
||||||
if (split.length !== 2) {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = split[0];
|
|
||||||
const path = split[1];
|
|
||||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
if (fileName.startsWith("dropboxstatus")) {
|
|
||||||
fileName = `hicolor/16x16/status/${fileName}`;
|
|
||||||
}
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
if (icon.startsWith("/") && !icon.startsWith("file://")) {
|
|
||||||
return `file://${icon}`;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
width: root.barThickness
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: Theme.barIconSize(root.barThickness)
|
|
||||||
height: Theme.barIconSize(root.barThickness)
|
|
||||||
source: delegateRoot.iconSource
|
|
||||||
asynchronous: true
|
|
||||||
smooth: true
|
|
||||||
mipmap: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: !iconImg.visible
|
|
||||||
text: {
|
|
||||||
const itemId = trayItem?.id || ""
|
|
||||||
if (!itemId) {
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
return itemId.charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: trayItemArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
if (!delegateRoot.trayItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton && !delegateRoot.trayItem.onlyMenu) {
|
|
||||||
delegateRoot.trayItem.activate();
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (delegateRoot.trayItem.hasMenu) {
|
|
||||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: trayMenuComponent
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: menuRoot
|
|
||||||
|
|
||||||
property var trayItem: null
|
|
||||||
property var anchorItem: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property bool isAtBottom: false
|
|
||||||
property bool isVertical: false
|
|
||||||
property var axis: null
|
|
||||||
property bool showMenu: false
|
|
||||||
property var menuHandle: null
|
|
||||||
|
|
||||||
ListModel { id: entryStack }
|
|
||||||
function topEntry() {
|
|
||||||
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null
|
|
||||||
}
|
|
||||||
|
|
||||||
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
|
||||||
trayItem = item
|
|
||||||
anchorItem = anchor
|
|
||||||
parentScreen = screen
|
|
||||||
isAtBottom = atBottom
|
|
||||||
isVertical = vertical
|
|
||||||
axis = axisObj
|
|
||||||
menuHandle = item?.menu
|
|
||||||
|
|
||||||
if (parentScreen) {
|
|
||||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
|
||||||
const s = Quickshell.screens[i]
|
|
||||||
if (s === parentScreen) {
|
|
||||||
menuWindow.screen = s
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showMenu = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSubMenu(entry) {
|
|
||||||
if (!entry || !entry.hasChildren) return;
|
|
||||||
|
|
||||||
entryStack.append({ handle: entry });
|
|
||||||
|
|
||||||
const h = entry.menu || entry;
|
|
||||||
if (h && typeof h.updateLayout === "function") h.updateLayout();
|
|
||||||
|
|
||||||
submenuHydrator.menu = h;
|
|
||||||
submenuHydrator.open();
|
|
||||||
Qt.callLater(() => submenuHydrator.close());
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
if (!entryStack.count) return;
|
|
||||||
entryStack.remove(entryStack.count - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 0
|
|
||||||
height: 0
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: menuWindow
|
|
||||||
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
updatePosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePosition() {
|
|
||||||
if (!menuRoot.anchorItem || !menuRoot.trayItem) {
|
|
||||||
anchorPos = Qt.point(screen.width / 2, screen.height / 2)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalPos = menuRoot.anchorItem.mapToGlobal(0, 0)
|
|
||||||
const screenX = screen.x || 0
|
|
||||||
const screenY = screen.y || 0
|
|
||||||
const relativeX = globalPos.x - screenX
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
|
|
||||||
const widgetThickness = Math.max(20, 26 + SettingsData.dankBarInnerPadding * 0.6)
|
|
||||||
const effectiveBarThickness = Math.max(widgetThickness + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding))
|
|
||||||
|
|
||||||
if (menuRoot.isVertical) {
|
|
||||||
const edge = menuRoot.axis?.edge
|
|
||||||
let targetX
|
|
||||||
if (edge === "left") {
|
|
||||||
targetX = effectiveBarThickness + SettingsData.dankBarSpacing + Theme.popupDistance
|
|
||||||
} else {
|
|
||||||
const popupX = effectiveBarThickness + SettingsData.dankBarSpacing + Theme.popupDistance
|
|
||||||
targetX = screen.width - popupX
|
|
||||||
}
|
|
||||||
anchorPos = Qt.point(targetX, relativeY + menuRoot.anchorItem.height / 2)
|
|
||||||
} else {
|
|
||||||
let targetY
|
|
||||||
if (menuRoot.isAtBottom) {
|
|
||||||
const popupY = effectiveBarThickness + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap + Theme.popupDistance
|
|
||||||
targetY = screen.height - popupY
|
|
||||||
} else {
|
|
||||||
targetY = effectiveBarThickness + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap + Theme.popupDistance
|
|
||||||
}
|
|
||||||
anchorPos = Qt.point(relativeX + menuRoot.anchorItem.width / 2, targetY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: menuContainer
|
|
||||||
|
|
||||||
width: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
|
|
||||||
height: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
|
|
||||||
|
|
||||||
x: {
|
|
||||||
if (menuRoot.isVertical) {
|
|
||||||
const edge = menuRoot.axis?.edge
|
|
||||||
if (edge === "left") {
|
|
||||||
const targetX = menuWindow.anchorPos.x
|
|
||||||
return Math.min(menuWindow.screen.width - width - 10, targetX)
|
|
||||||
} else {
|
|
||||||
const targetX = menuWindow.anchorPos.x - width
|
|
||||||
return Math.max(10, targetX)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const left = 10
|
|
||||||
const right = menuWindow.width - width - 10
|
|
||||||
const want = menuWindow.anchorPos.x - width / 2
|
|
||||||
return Math.max(left, Math.min(right, want))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
y: {
|
|
||||||
if (menuRoot.isVertical) {
|
|
||||||
const top = 10
|
|
||||||
const bottom = menuWindow.height - height - 10
|
|
||||||
const want = menuWindow.anchorPos.y - height / 2
|
|
||||||
return Math.max(top, Math.min(bottom, want))
|
|
||||||
} else {
|
|
||||||
if (menuRoot.isAtBottom) {
|
|
||||||
const targetY = menuWindow.anchorPos.y - height
|
|
||||||
return Math.max(10, targetY)
|
|
||||||
} else {
|
|
||||||
const targetY = menuWindow.anchorPos.y
|
|
||||||
return Math.min(menuWindow.screen.height - height - 10, targetY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
opacity: menuRoot.showMenu ? 1 : 0
|
|
||||||
scale: menuRoot.showMenu ? 1 : 0.85
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
QsMenuAnchor {
|
|
||||||
id: submenuHydrator
|
|
||||||
anchor.window: menuWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
QsMenuOpener {
|
|
||||||
id: rootOpener
|
|
||||||
menu: menuRoot.menuHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
QsMenuOpener {
|
|
||||||
id: subOpener
|
|
||||||
menu: {
|
|
||||||
const e = menuRoot.topEntry();
|
|
||||||
return e ? (e.menu || e) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: entryStack.count > 0
|
|
||||||
width: parent.width
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: backArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "arrow_back"
|
|
||||||
size: 16
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Back")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: backArea
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: menuRoot.goBack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: entryStack.count > 0
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: entryStack.count
|
|
||||||
? (subOpener.children ? subOpener.children
|
|
||||||
: (menuRoot.topEntry()?.children || []))
|
|
||||||
: rootOpener.children
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property var menuEntry: modelData
|
|
||||||
|
|
||||||
width: menuColumn.width
|
|
||||||
height: menuEntry?.isSeparator ? 1 : 28
|
|
||||||
radius: menuEntry?.isSeparator ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (menuEntry?.isSeparator) {
|
|
||||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
return itemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: itemArea
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: !menuEntry?.isSeparator && (menuEntry?.enabled !== false)
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (!menuEntry || menuEntry.isSeparator) return;
|
|
||||||
|
|
||||||
if (menuEntry.hasChildren) {
|
|
||||||
menuRoot.showSubMenu(menuEntry);
|
|
||||||
} else {
|
|
||||||
if (typeof menuEntry.activate === "function") {
|
|
||||||
menuEntry.activate();
|
|
||||||
} else if (typeof menuEntry.triggered === "function") {
|
|
||||||
menuEntry.triggered();
|
|
||||||
}
|
|
||||||
Qt.createQmlObject('import QtQuick; Timer { interval: 80; running: true; repeat: false; onTriggered: menuRoot.close() }', menuRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: !menuEntry?.isSeparator
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0
|
|
||||||
radius: menuEntry?.buttonType === 2 ? 8 : 2
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.outline
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - 6
|
|
||||||
height: parent.height - 6
|
|
||||||
radius: parent.radius - 3
|
|
||||||
color: Theme.primary
|
|
||||||
visible: menuEntry?.checkState === 2
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "check"
|
|
||||||
size: 10
|
|
||||||
color: Theme.primaryText
|
|
||||||
visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: menuEntry?.icon && menuEntry.icon !== ""
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: menuEntry?.icon || ""
|
|
||||||
sourceSize.width: 16
|
|
||||||
sourceSize.height: 16
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
smooth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: menuEntry?.text || ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: (menuEntry?.enabled !== false) ? Theme.surfaceText : Theme.surfaceTextMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Math.max(150, parent.width - 64)
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "chevron_right"
|
|
||||||
size: 14
|
|
||||||
color: Theme.surfaceText
|
|
||||||
visible: menuEntry?.hasChildren ?? false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
z: -1
|
|
||||||
onClicked: menuRoot.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var currentTrayMenu: null
|
|
||||||
|
|
||||||
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
|
||||||
if (currentTrayMenu) {
|
|
||||||
currentTrayMenu.destroy()
|
|
||||||
}
|
|
||||||
currentTrayMenu = trayMenuComponent.createObject(null)
|
|
||||||
if (currentTrayMenu) {
|
|
||||||
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Plugins
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
BasePill {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
Ref {
|
|
||||||
service: DMSNetworkService
|
|
||||||
}
|
|
||||||
|
|
||||||
property var popoutTarget: null
|
|
||||||
property bool isHovered: clickArea.containsMouse
|
|
||||||
|
|
||||||
signal toggleVpnPopup()
|
|
||||||
|
|
||||||
content: Component {
|
|
||||||
Item {
|
|
||||||
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
|
|
||||||
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: icon
|
|
||||||
|
|
||||||
name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
|
|
||||||
size: Theme.barIconSize(root.barThickness, -4)
|
|
||||||
color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: tooltipLoader
|
|
||||||
active: false
|
|
||||||
sourceComponent: DankTooltip {}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clickArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton
|
|
||||||
enabled: !DMSNetworkService.isBusy
|
|
||||||
onPressed: {
|
|
||||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
|
||||||
const globalPos = root.visualContent.mapToGlobal(0, 0)
|
|
||||||
const currentScreen = parentScreen || Screen
|
|
||||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
|
|
||||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
|
|
||||||
}
|
|
||||||
root.toggleVpnPopup();
|
|
||||||
}
|
|
||||||
onEntered: {
|
|
||||||
if (root.parentScreen && !(popoutTarget && popoutTarget.shouldBeVisible)) {
|
|
||||||
tooltipLoader.active = true
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
let tooltipText = ""
|
|
||||||
if (!DMSNetworkService.connected) {
|
|
||||||
tooltipText = "VPN Disconnected"
|
|
||||||
} else {
|
|
||||||
const names = DMSNetworkService.activeNames || []
|
|
||||||
if (names.length <= 1) {
|
|
||||||
const name = names[0] || ""
|
|
||||||
const maxLength = 25
|
|
||||||
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
|
|
||||||
tooltipText = "VPN Connected • " + displayName
|
|
||||||
} else {
|
|
||||||
const name = names[0]
|
|
||||||
const maxLength = 20
|
|
||||||
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
|
|
||||||
tooltipText = "VPN Connected • " + displayName + " +" + (names.length - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.isVerticalOrientation) {
|
|
||||||
const globalPos = mapToGlobal(width / 2, height / 2)
|
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0
|
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0
|
|
||||||
const relativeY = globalPos.y - screenY
|
|
||||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
|
|
||||||
const isLeft = root.axis?.edge === "left"
|
|
||||||
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
|
||||||
} else {
|
|
||||||
const globalPos = mapToGlobal(width / 2, height)
|
|
||||||
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
|
|
||||||
tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
if (tooltipLoader.item) {
|
|
||||||
tooltipLoader.item.hide()
|
|
||||||
}
|
|
||||||
tooltipLoader.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,904 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.I3
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isVertical: axis?.isVertical ?? false
|
|
||||||
property var axis: null
|
|
||||||
property string screenName: ""
|
|
||||||
property real widgetHeight: 30
|
|
||||||
property real barThickness: 48
|
|
||||||
property var hyprlandOverviewLoader: null
|
|
||||||
property var parentScreen: null
|
|
||||||
property int _desktopEntriesUpdateTrigger: 0
|
|
||||||
readonly property var sortedToplevels: {
|
|
||||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DesktopEntries
|
|
||||||
function onApplicationsChanged() {
|
|
||||||
_desktopEntriesUpdateTrigger++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property int currentWorkspace: {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
return getNiriActiveWorkspace()
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
return getHyprlandActiveWorkspace()
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
const activeTags = getDwlActiveTags()
|
|
||||||
return activeTags.length > 0 ? activeTags[0] : -1
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
return getSwayActiveWorkspace()
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
property var dwlActiveTags: {
|
|
||||||
if (CompositorService.isDwl) {
|
|
||||||
return getDwlActiveTags()
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
property var workspaceList: {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
const baseList = getNiriWorkspaces()
|
|
||||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
|
||||||
}
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
const baseList = getHyprlandWorkspaces()
|
|
||||||
const filteredList = baseList.filter(ws => ws.id > -1)
|
|
||||||
return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList
|
|
||||||
}
|
|
||||||
if (CompositorService.isDwl) {
|
|
||||||
const baseList = getDwlTags()
|
|
||||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
|
||||||
}
|
|
||||||
if (CompositorService.isSway) {
|
|
||||||
const baseList = getSwayWorkspaces()
|
|
||||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
|
||||||
}
|
|
||||||
return [1]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSwayWorkspaces() {
|
|
||||||
const workspaces = I3.workspaces?.values || []
|
|
||||||
if (workspaces.length === 0) return [{"num": 1}]
|
|
||||||
|
|
||||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
|
||||||
return workspaces.slice().sort((a, b) => a.num - b.num)
|
|
||||||
}
|
|
||||||
|
|
||||||
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
|
|
||||||
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSwayActiveWorkspace() {
|
|
||||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
|
||||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
|
|
||||||
return focusedWs ? focusedWs.num : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true)
|
|
||||||
return focusedWs ? focusedWs.num : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkspaceIcons(ws) {
|
|
||||||
_desktopEntriesUpdateTrigger
|
|
||||||
if (!SettingsData.showWorkspaceApps || !ws) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetWorkspaceId
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
const wsNumber = typeof ws === "number" ? ws : -1
|
|
||||||
if (wsNumber <= 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.screenName)
|
|
||||||
if (!workspace) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
targetWorkspaceId = workspace.id
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
targetWorkspaceId = ws.id !== undefined ? ws.id : ws
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
if (typeof ws !== "object" || ws.tag === undefined) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
targetWorkspaceId = ws.tag
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
targetWorkspaceId = ws.num !== undefined ? ws.num : ws
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels
|
|
||||||
|
|
||||||
const byApp = {}
|
|
||||||
let isActiveWs = false
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active)
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
|
|
||||||
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
const output = DwlService.getOutputState(root.screenName)
|
|
||||||
if (output && output.tags) {
|
|
||||||
const tag = output.tags.find(t => t.tag === targetWorkspaceId)
|
|
||||||
isActiveWs = tag ? (tag.state === 1) : false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isActiveWs = targetWorkspaceId === root.currentWorkspace
|
|
||||||
}
|
|
||||||
|
|
||||||
wins.forEach((w, i) => {
|
|
||||||
if (!w) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let winWs = null
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
winWs = w.workspace_id
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
winWs = w.workspace?.num
|
|
||||||
} else {
|
|
||||||
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || [])
|
|
||||||
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w)
|
|
||||||
winWs = hyprToplevel?.workspace?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown")
|
|
||||||
const key = isActiveWs ? `${keyBase}_${i}` : keyBase
|
|
||||||
|
|
||||||
if (!byApp[key]) {
|
|
||||||
const moddedId = Paths.moddedAppId(keyBase)
|
|
||||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
|
||||||
const icon = isSteamApp ? "" : DesktopService.resolveIconPath(moddedId)
|
|
||||||
byApp[key] = {
|
|
||||||
"type": "icon",
|
|
||||||
"icon": icon,
|
|
||||||
"isSteamApp": isSteamApp,
|
|
||||||
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
|
|
||||||
"count": 1,
|
|
||||||
"windowId": w.address || w.id,
|
|
||||||
"fallbackText": w.appId || w.class || w.title || ""
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
byApp[key].count++
|
|
||||||
if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) {
|
|
||||||
byApp[key].active = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Object.values(byApp)
|
|
||||||
}
|
|
||||||
|
|
||||||
function padWorkspaces(list) {
|
|
||||||
const padded = list.slice()
|
|
||||||
let placeholder
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
placeholder = {"id": -1, "name": ""}
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
placeholder = {"tag": -1}
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
placeholder = {"num": -1}
|
|
||||||
} else {
|
|
||||||
placeholder = -1
|
|
||||||
}
|
|
||||||
while (padded.length < 3) {
|
|
||||||
padded.push(placeholder)
|
|
||||||
}
|
|
||||||
return padded
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNiriWorkspaces() {
|
|
||||||
if (NiriService.allWorkspaces.length === 0) {
|
|
||||||
return [1, 2]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
|
||||||
return NiriService.getCurrentOutputWorkspaceNumbers()
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1)
|
|
||||||
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNiriActiveWorkspace() {
|
|
||||||
if (NiriService.allWorkspaces.length === 0) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
|
||||||
return NiriService.getCurrentWorkspaceNumber()
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active)
|
|
||||||
return activeWs ? activeWs.idx + 1 : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHyprlandWorkspaces() {
|
|
||||||
const workspaces = Hyprland.workspaces?.values || []
|
|
||||||
|
|
||||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
|
||||||
// Show all workspaces on all monitors if per-monitor filtering is disabled
|
|
||||||
const sorted = workspaces.slice().sort((a, b) => a.id - b.id)
|
|
||||||
return sorted.length > 0 ? sorted : [{
|
|
||||||
"id": 1,
|
|
||||||
"name": "1"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter workspaces for this specific monitor using lastIpcObject.monitor
|
|
||||||
// This matches the approach from the original kyle-config
|
|
||||||
const monitorWorkspaces = workspaces.filter(ws => {
|
|
||||||
return ws.lastIpcObject && ws.lastIpcObject.monitor === root.screenName
|
|
||||||
})
|
|
||||||
|
|
||||||
if (monitorWorkspaces.length === 0) {
|
|
||||||
// Fallback if no workspaces exist for this monitor
|
|
||||||
return [{
|
|
||||||
"id": 1,
|
|
||||||
"name": "1"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all workspaces for this monitor, sorted by ID
|
|
||||||
return monitorWorkspaces.sort((a, b) => a.id - b.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHyprlandActiveWorkspace() {
|
|
||||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
|
||||||
return Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const monitors = Hyprland.monitors?.values || []
|
|
||||||
const currentMonitor = monitors.find(monitor => monitor.name === root.screenName)
|
|
||||||
|
|
||||||
if (!currentMonitor) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentMonitor.activeWorkspace?.id ?? 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDwlTags() {
|
|
||||||
if (!DwlService.dwlAvailable) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = DwlService.getOutputState(root.screenName)
|
|
||||||
if (!output || !output.tags || output.tags.length === 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsData.dwlShowAllTags) {
|
|
||||||
return output.tags.map(tag => ({"tag": tag.tag, "state": tag.state, "clients": tag.clients, "focused": tag.focused}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleTagIndices = DwlService.getVisibleTags(root.screenName)
|
|
||||||
return visibleTagIndices.map(tagIndex => {
|
|
||||||
const tagData = output.tags.find(t => t.tag === tagIndex)
|
|
||||||
return {
|
|
||||||
"tag": tagIndex,
|
|
||||||
"state": tagData?.state ?? 0,
|
|
||||||
"clients": tagData?.clients ?? 0,
|
|
||||||
"focused": tagData?.focused ?? false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDwlActiveTags() {
|
|
||||||
if (!DwlService.dwlAvailable) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeTags = DwlService.getActiveTags(root.screenName)
|
|
||||||
return activeTags
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
|
||||||
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
|
|
||||||
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
|
|
||||||
|
|
||||||
function getRealWorkspaces() {
|
|
||||||
return root.workspaceList.filter(ws => {
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return ws && ws.id !== -1
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
return ws && ws.tag !== -1
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
return ws && ws.num !== -1
|
|
||||||
}
|
|
||||||
return ws !== -1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchWorkspace(direction) {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
const realWorkspaces = getRealWorkspaces()
|
|
||||||
if (realWorkspaces.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace)
|
|
||||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
|
||||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
|
||||||
|
|
||||||
if (nextIndex === validIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1)
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
const realWorkspaces = getRealWorkspaces()
|
|
||||||
if (realWorkspaces.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = realWorkspaces.findIndex(ws => ws.id === root.currentWorkspace)
|
|
||||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
|
||||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
|
||||||
|
|
||||||
if (nextIndex === validIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`)
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
const realWorkspaces = getRealWorkspaces()
|
|
||||||
if (realWorkspaces.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace)
|
|
||||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
|
||||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
|
||||||
|
|
||||||
if (nextIndex === validIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag)
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
const realWorkspaces = getRealWorkspaces()
|
|
||||||
if (realWorkspaces.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace)
|
|
||||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
|
||||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
|
||||||
|
|
||||||
if (nextIndex === validIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
width: isVertical ? barThickness : visualWidth
|
|
||||||
height: isVertical ? visualHeight : barThickness
|
|
||||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualBackground
|
|
||||||
width: root.visualWidth
|
|
||||||
height: root.visualHeight
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (SettingsData.dankBarNoBackground)
|
|
||||||
return "transparent"
|
|
||||||
const baseColor = Theme.widgetBaseBackgroundColor
|
|
||||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.button === Qt.RightButton) {
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
NiriService.toggleOverview()
|
|
||||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
id: workspaceRow
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ScriptModel {
|
|
||||||
values: root.workspaceList
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: delegateRoot
|
|
||||||
|
|
||||||
property bool isActive: {
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return modelData && modelData.id === root.currentWorkspace
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
return modelData && root.dwlActiveTags.includes(modelData.tag)
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
return modelData && modelData.num === root.currentWorkspace
|
|
||||||
}
|
|
||||||
return modelData === root.currentWorkspace
|
|
||||||
}
|
|
||||||
property bool isPlaceholder: {
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return modelData && modelData.id === -1
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
return modelData && modelData.tag === -1
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
return modelData && modelData.num === -1
|
|
||||||
}
|
|
||||||
return modelData === -1
|
|
||||||
}
|
|
||||||
property bool isHovered: mouseArea.containsMouse
|
|
||||||
|
|
||||||
property var loadedWorkspaceData: null
|
|
||||||
property bool loadedIsUrgent: false
|
|
||||||
property bool isUrgent: {
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return modelData?.urgent ?? false
|
|
||||||
} else if (CompositorService.isNiri) {
|
|
||||||
return loadedIsUrgent
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
return modelData?.state === 2
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
return loadedIsUrgent
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
property var loadedIconData: null
|
|
||||||
property bool loadedHasIcon: false
|
|
||||||
property var loadedIcons: []
|
|
||||||
|
|
||||||
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
|
|
||||||
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
|
|
||||||
|
|
||||||
readonly property real iconsExtraWidth: {
|
|
||||||
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
|
||||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
|
|
||||||
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
readonly property real iconsExtraHeight: {
|
|
||||||
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
|
|
||||||
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
|
|
||||||
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real visualWidth: baseWidth + iconsExtraWidth
|
|
||||||
readonly property real visualHeight: baseHeight + iconsExtraHeight
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: !isPlaceholder
|
|
||||||
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
|
||||||
enabled: !isPlaceholder
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (isPlaceholder) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRightClick = mouse.button === Qt.RightButton
|
|
||||||
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
if (isRightClick) {
|
|
||||||
NiriService.toggleOverview()
|
|
||||||
} else {
|
|
||||||
NiriService.switchToWorkspace(modelData - 1)
|
|
||||||
}
|
|
||||||
} else if (CompositorService.isHyprland && modelData?.id) {
|
|
||||||
if (isRightClick && root.hyprlandOverviewLoader?.item) {
|
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
|
|
||||||
} else {
|
|
||||||
Hyprland.dispatch(`workspace ${modelData.id}`)
|
|
||||||
}
|
|
||||||
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
|
|
||||||
console.log("DWL click - tag:", modelData.tag, "rightClick:", isRightClick)
|
|
||||||
if (isRightClick) {
|
|
||||||
console.log("Calling toggleTag")
|
|
||||||
DwlService.toggleTag(root.screenName, modelData.tag)
|
|
||||||
} else {
|
|
||||||
console.log("Calling switchToTag")
|
|
||||||
DwlService.switchToTag(root.screenName, modelData.tag)
|
|
||||||
}
|
|
||||||
} else if (CompositorService.isSway && modelData?.num) {
|
|
||||||
try { I3.dispatch(`workspace number ${modelData.num}`) } catch(_){}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: dataUpdateTimer
|
|
||||||
interval: 50
|
|
||||||
onTriggered: {
|
|
||||||
if (isPlaceholder) {
|
|
||||||
delegateRoot.loadedWorkspaceData = null
|
|
||||||
delegateRoot.loadedIconData = null
|
|
||||||
delegateRoot.loadedHasIcon = false
|
|
||||||
delegateRoot.loadedIcons = []
|
|
||||||
delegateRoot.loadedIsUrgent = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var wsData = null;
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null;
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
wsData = modelData;
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
wsData = modelData;
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
wsData = modelData;
|
|
||||||
}
|
|
||||||
delegateRoot.loadedWorkspaceData = wsData;
|
|
||||||
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
|
|
||||||
|
|
||||||
var icData = null;
|
|
||||||
if (wsData?.name) {
|
|
||||||
icData = SettingsData.getWorkspaceNameIcon(wsData.name);
|
|
||||||
}
|
|
||||||
delegateRoot.loadedIconData = icData;
|
|
||||||
delegateRoot.loadedHasIcon = icData !== null;
|
|
||||||
|
|
||||||
if (SettingsData.showWorkspaceApps) {
|
|
||||||
if (CompositorService.isDwl || CompositorService.isSway) {
|
|
||||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
|
|
||||||
} else {
|
|
||||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delegateRoot.loadedIcons = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAllData() {
|
|
||||||
dataUpdateTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
width: root.isVertical ? root.barThickness : visualWidth
|
|
||||||
height: root.isVertical ? visualHeight : root.barThickness
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: delegateRoot.visualWidth
|
|
||||||
height: delegateRoot.visualHeight
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
|
|
||||||
|
|
||||||
border.width: isUrgent && !isActive ? 2 : 0
|
|
||||||
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: appIconsLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
Loader {
|
|
||||||
id: contentRow
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: root.isVertical ? columnLayout : rowLayout
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: rowLayout
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
visible: loadedIcons.length > 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ScriptModel {
|
|
||||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
|
||||||
}
|
|
||||||
delegate: Item {
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: appIcon
|
|
||||||
property var windowId: modelData.windowId
|
|
||||||
anchors.fill: parent
|
|
||||||
source: modelData.icon
|
|
||||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
|
||||||
visible: !modelData.isSteamApp
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
size: 18
|
|
||||||
name: "sports_esports"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
|
||||||
visible: modelData.isSteamApp
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: appMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: isActive
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!appIcon.windowId) return
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
|
|
||||||
} else if (CompositorService.isNiri) {
|
|
||||||
NiriService.focusWindow(appIcon.windowId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: modelData.count > 1 && !isActive
|
|
||||||
width: 12
|
|
||||||
height: 12
|
|
||||||
radius: 6
|
|
||||||
color: "black"
|
|
||||||
border.color: "white"
|
|
||||||
border.width: 1
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
z: 2
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData.count
|
|
||||||
font.pixelSize: 8
|
|
||||||
color: "white"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: columnLayout
|
|
||||||
Column {
|
|
||||||
spacing: 4
|
|
||||||
visible: loadedIcons.length > 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ScriptModel {
|
|
||||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
|
||||||
}
|
|
||||||
delegate: Item {
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: appIcon
|
|
||||||
property var windowId: modelData.windowId
|
|
||||||
anchors.fill: parent
|
|
||||||
source: modelData.icon
|
|
||||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
|
||||||
visible: !modelData.isSteamApp
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
size: 18
|
|
||||||
name: "sports_esports"
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
|
|
||||||
visible: modelData.isSteamApp
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: appMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: isActive
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!appIcon.windowId) return
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
|
|
||||||
} else if (CompositorService.isNiri) {
|
|
||||||
NiriService.focusWindow(appIcon.windowId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: modelData.count > 1 && !isActive
|
|
||||||
width: 12
|
|
||||||
height: 12
|
|
||||||
radius: 6
|
|
||||||
color: "black"
|
|
||||||
border.color: "white"
|
|
||||||
border.width: 1
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
z: 2
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData.count
|
|
||||||
font.pixelSize: 8
|
|
||||||
color: "white"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loader for Custom Name Icon
|
|
||||||
Loader {
|
|
||||||
id: customIconLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "icon" && !SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: loadedIconData ? loadedIconData.value : "" // NULL CHECK
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
|
|
||||||
weight: isActive && !isPlaceholder ? 500 : 400
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loader for Custom Name Text
|
|
||||||
Loader {
|
|
||||||
id: customTextLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "text" && !SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: loadedIconData ? loadedIconData.value : "" // NULL CHECK
|
|
||||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
|
|
||||||
font.pixelSize: Theme.barTextSize(barThickness)
|
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loader for Workspace Index
|
|
||||||
Loader {
|
|
||||||
id: indexLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: !isPlaceholder && SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
let isPlaceholder
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
isPlaceholder = modelData?.id === -1
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
isPlaceholder = modelData?.tag === -1
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
isPlaceholder = modelData?.num === -1
|
|
||||||
} else {
|
|
||||||
isPlaceholder = modelData === -1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaceholder) {
|
|
||||||
return index + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CompositorService.isHyprland) {
|
|
||||||
return modelData?.id || ""
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
return modelData?.num || ""
|
|
||||||
}
|
|
||||||
return modelData - 1
|
|
||||||
}
|
|
||||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
|
||||||
font.pixelSize: Theme.barTextSize(barThickness)
|
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: updateAllData()
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: CompositorService
|
|
||||||
function onSortedToplevelsChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: NiriService
|
|
||||||
enabled: CompositorService.isNiri
|
|
||||||
function onAllWorkspacesChanged() { delegateRoot.updateAllData() }
|
|
||||||
function onWindowUrgentChanged() { delegateRoot.updateAllData() }
|
|
||||||
function onWindowsChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() }
|
|
||||||
function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: DwlService
|
|
||||||
enabled: CompositorService.isDwl
|
|
||||||
function onStateChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: I3.workspaces
|
|
||||||
enabled: CompositorService.isSway
|
|
||||||
function onValuesChanged() { delegateRoot.updateAllData() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var contextMenu: null
|
|
||||||
property bool requestDockShow: false
|
|
||||||
property int pinnedAppCount: 0
|
|
||||||
property bool groupByApp: false
|
|
||||||
property bool isVertical: false
|
|
||||||
property var dockScreen: null
|
|
||||||
property real iconSize: 40
|
|
||||||
|
|
||||||
clip: false
|
|
||||||
implicitWidth: isVertical ? appLayout.height : appLayout.width
|
|
||||||
implicitHeight: isVertical ? appLayout.width : appLayout.height
|
|
||||||
|
|
||||||
function movePinnedApp(fromIndex, toIndex) {
|
|
||||||
if (fromIndex === toIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPinned = [...(SessionData.pinnedApps || [])]
|
|
||||||
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedApp = currentPinned.splice(fromIndex, 1)[0]
|
|
||||||
currentPinned.splice(toIndex, 0, movedApp)
|
|
||||||
|
|
||||||
SessionData.setPinnedApps(currentPinned)
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: appLayout
|
|
||||||
width: layoutFlow.width
|
|
||||||
height: layoutFlow.height
|
|
||||||
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
|
|
||||||
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
|
|
||||||
anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
|
|
||||||
anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
|
|
||||||
anchors.top: root.isVertical ? undefined : parent.top
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
id: layoutFlow
|
|
||||||
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
|
||||||
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: repeater
|
|
||||||
model: ListModel {
|
|
||||||
id: dockModel
|
|
||||||
|
|
||||||
Component.onCompleted: updateModel()
|
|
||||||
|
|
||||||
function updateModel() {
|
|
||||||
clear()
|
|
||||||
|
|
||||||
const items = []
|
|
||||||
const pinnedApps = [...(SessionData.pinnedApps || [])]
|
|
||||||
const sortedToplevels = CompositorService.sortedToplevels
|
|
||||||
|
|
||||||
if (root.groupByApp) {
|
|
||||||
// Group windows by appId
|
|
||||||
const appGroups = new Map()
|
|
||||||
|
|
||||||
// Add pinned apps first (even if they have no windows)
|
|
||||||
pinnedApps.forEach(appId => {
|
|
||||||
appGroups.set(appId, {
|
|
||||||
appId: appId,
|
|
||||||
isPinned: true,
|
|
||||||
windows: []
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Group all running windows by appId
|
|
||||||
sortedToplevels.forEach((toplevel, index) => {
|
|
||||||
const appId = toplevel.appId || "unknown"
|
|
||||||
if (!appGroups.has(appId)) {
|
|
||||||
appGroups.set(appId, {
|
|
||||||
appId: appId,
|
|
||||||
isPinned: false,
|
|
||||||
windows: []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const title = toplevel.title || "(Unnamed)"
|
|
||||||
const truncatedTitle = title.length > 50 ? title.substring(0, 47) + "..." : title
|
|
||||||
const uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index
|
|
||||||
|
|
||||||
appGroups.get(appId).windows.push({
|
|
||||||
windowId: index,
|
|
||||||
windowTitle: truncatedTitle,
|
|
||||||
uniqueId: uniqueId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sort groups: pinned first, then unpinned
|
|
||||||
const pinnedGroups = []
|
|
||||||
const unpinnedGroups = []
|
|
||||||
|
|
||||||
Array.from(appGroups.entries()).forEach(([appId, group]) => {
|
|
||||||
// For grouped apps, just show the first window info but track all windows
|
|
||||||
const firstWindow = group.windows.length > 0 ? group.windows[0] : null
|
|
||||||
|
|
||||||
const item = {
|
|
||||||
"type": "grouped",
|
|
||||||
"appId": appId,
|
|
||||||
"windowId": firstWindow ? firstWindow.windowId : -1,
|
|
||||||
"windowTitle": firstWindow ? firstWindow.windowTitle : "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": group.isPinned,
|
|
||||||
"isRunning": group.windows.length > 0,
|
|
||||||
"windowCount": group.windows.length,
|
|
||||||
"uniqueId": firstWindow ? firstWindow.uniqueId : "",
|
|
||||||
"allWindows": group.windows
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.isPinned) {
|
|
||||||
pinnedGroups.push(item)
|
|
||||||
} else {
|
|
||||||
unpinnedGroups.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add items in order
|
|
||||||
pinnedGroups.forEach(item => items.push(item))
|
|
||||||
|
|
||||||
// Add separator if needed
|
|
||||||
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
|
||||||
items.push({
|
|
||||||
"type": "separator",
|
|
||||||
"appId": "__SEPARATOR__",
|
|
||||||
"windowId": -1,
|
|
||||||
"windowTitle": "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": false,
|
|
||||||
"isRunning": false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
unpinnedGroups.forEach(item => items.push(item))
|
|
||||||
root.pinnedAppCount = pinnedGroups.length
|
|
||||||
} else {
|
|
||||||
pinnedApps.forEach(appId => {
|
|
||||||
items.push({
|
|
||||||
"type": "pinned",
|
|
||||||
"appId": appId,
|
|
||||||
"windowId": -1,
|
|
||||||
"windowTitle": "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": true,
|
|
||||||
"isRunning": false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
root.pinnedAppCount = pinnedApps.length
|
|
||||||
|
|
||||||
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
|
|
||||||
items.push({
|
|
||||||
"type": "separator",
|
|
||||||
"appId": "__SEPARATOR__",
|
|
||||||
"windowId": -1,
|
|
||||||
"windowTitle": "",
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": false,
|
|
||||||
"isRunning": false,
|
|
||||||
"isFocused": false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sortedToplevels.forEach((toplevel, index) => {
|
|
||||||
const title = toplevel.title || "(Unnamed)"
|
|
||||||
const truncatedTitle = title.length > 50 ? title.substring(0, 47) + "..." : title
|
|
||||||
const uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
"type": "window",
|
|
||||||
"appId": toplevel.appId,
|
|
||||||
"windowId": index,
|
|
||||||
"windowTitle": truncatedTitle,
|
|
||||||
"workspaceId": -1,
|
|
||||||
"isPinned": false,
|
|
||||||
"isRunning": true,
|
|
||||||
"uniqueId": uniqueId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
items.forEach(item => append(item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateItem
|
|
||||||
property alias dockButton: button
|
|
||||||
clip: false
|
|
||||||
|
|
||||||
width: model.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
|
|
||||||
height: model.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: model.type === "separator"
|
|
||||||
width: root.isVertical ? root.iconSize * 0.5 : 2
|
|
||||||
height: root.isVertical ? 2 : root.iconSize * 0.5
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
|
||||||
radius: 1
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
DockAppButton {
|
|
||||||
id: button
|
|
||||||
visible: model.type !== "separator"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
width: delegateItem.width
|
|
||||||
height: delegateItem.height
|
|
||||||
actualIconSize: root.iconSize
|
|
||||||
|
|
||||||
appData: model
|
|
||||||
contextMenu: root.contextMenu
|
|
||||||
dockApps: root
|
|
||||||
index: model.index
|
|
||||||
parentDockScreen: root.dockScreen
|
|
||||||
|
|
||||||
showWindowTitle: model.type === "window" || model.type === "grouped"
|
|
||||||
windowTitle: model.windowTitle || ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: CompositorService
|
|
||||||
function onToplevelsChanged() {
|
|
||||||
dockModel.updateModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionData
|
|
||||||
function onPinnedAppsChanged() {
|
|
||||||
dockModel.updateModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onGroupByAppChanged: {
|
|
||||||
dockModel.updateModel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isVisible: false
|
|
||||||
property bool showLogout: true
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property int optionCount: {
|
|
||||||
let count = 0
|
|
||||||
if (showLogout) count++
|
|
||||||
count++
|
|
||||||
if (SessionService.hibernateSupported) count++
|
|
||||||
count += 2
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
signal closed()
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
isVisible = true
|
|
||||||
selectedIndex = 0
|
|
||||||
Qt.callLater(() => {
|
|
||||||
if (powerMenuFocusScope && powerMenuFocusScope.forceActiveFocus) {
|
|
||||||
powerMenuFocusScope.forceActiveFocus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
isVisible = false
|
|
||||||
closed()
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.5)
|
|
||||||
visible: isVisible
|
|
||||||
z: 1000
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: powerMenuFocusScope
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: root.isVisible
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
Qt.callLater(() => forceActiveFocus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onEscapePressed: {
|
|
||||||
root.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Up:
|
|
||||||
case Qt.Key_Backtab:
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Down:
|
|
||||||
case Qt.Key_Tab:
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
const actions = []
|
|
||||||
if (showLogout) actions.push("logout")
|
|
||||||
actions.push("suspend")
|
|
||||||
if (SessionService.hibernateSupported) actions.push("hibernate")
|
|
||||||
actions.push("reboot", "poweroff")
|
|
||||||
if (selectedIndex < actions.length) {
|
|
||||||
const action = actions[selectedIndex]
|
|
||||||
hide()
|
|
||||||
switch (action) {
|
|
||||||
case "logout":
|
|
||||||
SessionService.logout()
|
|
||||||
break
|
|
||||||
case "suspend":
|
|
||||||
SessionService.suspend()
|
|
||||||
break
|
|
||||||
case "hibernate":
|
|
||||||
SessionService.hibernate()
|
|
||||||
break
|
|
||||||
case "reboot":
|
|
||||||
SessionService.reboot()
|
|
||||||
break
|
|
||||||
case "poweroff":
|
|
||||||
SessionService.poweroff()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
break
|
|
||||||
case Qt.Key_N:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_P:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_J:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Qt.Key_K:
|
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
|
||||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 320
|
|
||||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
|
||||||
height: implicitHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingL
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Options")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - 150
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
onClicked: root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
visible: showLogout
|
|
||||||
color: {
|
|
||||||
if (selectedIndex === 0) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (logoutArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === 0 ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "logout"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Log Out")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: logoutArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
const suspendIdx = showLogout ? 1 : 0
|
|
||||||
if (selectedIndex === suspendIdx) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (suspendArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === (showLogout ? 1 : 0) ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === (showLogout ? 1 : 0) ? 1 : 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bedtime"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Suspend")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: suspendArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.suspend()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
const hibernateIdx = showLogout ? 2 : 1
|
|
||||||
if (selectedIndex === hibernateIdx) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (hibernateArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: selectedIndex === (showLogout ? 2 : 1) ? Theme.primary : "transparent"
|
|
||||||
border.width: selectedIndex === (showLogout ? 2 : 1) ? 1 : 0
|
|
||||||
visible: SessionService.hibernateSupported
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "ac_unit"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Hibernate")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: hibernateArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.hibernate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
let rebootIdx = showLogout ? 3 : 2
|
|
||||||
if (!SessionService.hibernateSupported) rebootIdx--
|
|
||||||
if (selectedIndex === rebootIdx) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (rebootArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
let rebootIdx = showLogout ? 3 : 2
|
|
||||||
if (!SessionService.hibernateSupported) rebootIdx--
|
|
||||||
return selectedIndex === rebootIdx ? Theme.primary : "transparent"
|
|
||||||
}
|
|
||||||
border.width: {
|
|
||||||
let rebootIdx = showLogout ? 3 : 2
|
|
||||||
if (!SessionService.hibernateSupported) rebootIdx--
|
|
||||||
return selectedIndex === rebootIdx ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "restart_alt"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Reboot")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rebootArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.reboot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
let powerOffIdx = showLogout ? 4 : 3
|
|
||||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
|
||||||
if (selectedIndex === powerOffIdx) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
} else if (powerOffArea.containsMouse) {
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
|
||||||
} else {
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
let powerOffIdx = showLogout ? 4 : 3
|
|
||||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
|
||||||
return selectedIndex === powerOffIdx ? Theme.primary : "transparent"
|
|
||||||
}
|
|
||||||
border.width: {
|
|
||||||
let powerOffIdx = showLogout ? 4 : 3
|
|
||||||
if (!SessionService.hibernateSupported) powerOffIdx--
|
|
||||||
return selectedIndex === powerOffIdx ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "power_settings_new"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Power Off")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: powerOffArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.hide()
|
|
||||||
SessionService.poweroff()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
height: Theme.spacingS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankOSD {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
|
||||||
osdHeight: 40 + Theme.spacingS * 2
|
|
||||||
autoHideInterval: 3000
|
|
||||||
enableMouseInteraction: true
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DisplayService
|
|
||||||
function onBrightnessChanged(showOsd) {
|
|
||||||
if (showOsd) {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int gap: Theme.spacingS
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Theme.iconSize
|
|
||||||
height: Theme.iconSize
|
|
||||||
radius: Theme.iconSize / 2
|
|
||||||
color: "transparent"
|
|
||||||
x: parent.gap
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: {
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
|
||||||
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
|
|
||||||
return "brightness_medium"
|
|
||||||
} else if (deviceInfo.name.includes("kbd")) {
|
|
||||||
return "keyboard"
|
|
||||||
} else {
|
|
||||||
return "lightbulb"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: brightnessSlider
|
|
||||||
|
|
||||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
|
||||||
height: 40
|
|
||||||
x: parent.gap * 2 + Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
minimum: {
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
|
||||||
if (!deviceInfo) return 1
|
|
||||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
|
||||||
if (isExponential) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0
|
|
||||||
}
|
|
||||||
maximum: {
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
|
||||||
if (!deviceInfo) return 100
|
|
||||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
|
||||||
if (isExponential) {
|
|
||||||
return 100
|
|
||||||
}
|
|
||||||
return deviceInfo.displayMax || 100
|
|
||||||
}
|
|
||||||
enabled: DisplayService.brightnessAvailable
|
|
||||||
showValue: true
|
|
||||||
unit: {
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo()
|
|
||||||
if (!deviceInfo) return "%"
|
|
||||||
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id)
|
|
||||||
if (isExponential) {
|
|
||||||
return "%"
|
|
||||||
}
|
|
||||||
return deviceInfo.class === "ddc" ? "" : "%"
|
|
||||||
}
|
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
|
||||||
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true)
|
|
||||||
resetHideTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onContainsMouseChanged: {
|
|
||||||
setChildHovered(containsMouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderDragFinished: finalValue => {
|
|
||||||
if (DisplayService.brightnessAvailable) {
|
|
||||||
DisplayService.setBrightness(finalValue, DisplayService.lastIpcDevice, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DisplayService
|
|
||||||
|
|
||||||
function onBrightnessChanged(showOsd) {
|
|
||||||
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
|
|
||||||
brightnessSlider.value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeviceSwitched() {
|
|
||||||
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
|
|
||||||
brightnessSlider.value = DisplayService.brightnessLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankOSD {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
osdWidth: Math.min(260, Screen.width - Theme.spacingM * 2)
|
|
||||||
osdHeight: 40 + Theme.spacingS * 2
|
|
||||||
autoHideInterval: 3000
|
|
||||||
enableMouseInteraction: true
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
|
||||||
|
|
||||||
function onVolumeChanged() {
|
|
||||||
if (!AudioService.suppressOSD) {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMutedChanged() {
|
|
||||||
if (!AudioService.suppressOSD) {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService
|
|
||||||
|
|
||||||
function onSinkChanged() {
|
|
||||||
if (root.shouldBeVisible) {
|
|
||||||
root.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int gap: Theme.spacingS
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Theme.iconSize
|
|
||||||
height: Theme.iconSize
|
|
||||||
radius: Theme.iconSize / 2
|
|
||||||
color: "transparent"
|
|
||||||
x: parent.gap
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: muteButton
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
AudioService.toggleMute()
|
|
||||||
}
|
|
||||||
onContainsMouseChanged: {
|
|
||||||
setChildHovered(containsMouse || volumeSlider.containsMouse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: volumeSlider
|
|
||||||
|
|
||||||
readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
|
||||||
readonly property real displayPercent: actualVolumePercent
|
|
||||||
|
|
||||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
|
||||||
height: 40
|
|
||||||
x: parent.gap * 2 + Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
enabled: AudioService.sink && AudioService.sink.audio
|
|
||||||
showValue: true
|
|
||||||
unit: "%"
|
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
|
||||||
valueOverride: displayPercent
|
|
||||||
alwaysShowValue: SettingsData.osdAlwaysShowValue
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
|
||||||
AudioService.suppressOSD = true
|
|
||||||
AudioService.sink.audio.volume = newValue / 100
|
|
||||||
AudioService.suppressOSD = false
|
|
||||||
resetHideTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onContainsMouseChanged: {
|
|
||||||
setChildHovered(containsMouse || muteButton.containsMouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
|
|
||||||
|
|
||||||
function onVolumeChanged() {
|
|
||||||
if (volumeSlider && !volumeSlider.pressed) {
|
|
||||||
volumeSlider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOsdShown: {
|
|
||||||
if (AudioService.sink && AudioService.sink.audio && contentLoader.item) {
|
|
||||||
const slider = contentLoader.item.children[0].children[1]
|
|
||||||
if (slider) {
|
|
||||||
slider.value = Math.min(100, Math.round(AudioService.sink.audio.volume * 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
|||||||
# DankMaterialShell (dms)
|
# DankMaterialShell
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://danklinux.com">
|
<a href="https://danklinux.com">
|
||||||
<img src="assets/danklogo2.svg" alt="DankMaterialShell Logo" width="200">
|
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
### A modern Wayland desktop shell
|
### A modern desktop shell for Wayland
|
||||||
|
|
||||||
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
||||||
|
|
||||||
@@ -19,16 +19,29 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hypr.land), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop - all in one cohesive package with a gorgeous interface.
|
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/), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
||||||
|
|
||||||
## Components
|
## Repository Structure
|
||||||
|
|
||||||
DankMaterialShell combines two main components:
|
This is a monorepo containing both the shell interface and the core backend services:
|
||||||
|
|
||||||
- **[QML/UI Layer](https://github.com/AvengeMedia/DankMaterialShell)** (this repo) - All the visual components, widgets, and shell interface built with Quickshell
|
```
|
||||||
- **[Go Backend](https://github.com/AvengeMedia/danklinux)** - System integration, IPC, process management, and core 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
|
## See it in Action
|
||||||
|
|
||||||
@@ -54,166 +67,126 @@ https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
## Installation
|
||||||
|
|
||||||
## Quick Install
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://install.danklinux.com | sh
|
curl -fsSL https://install.danklinux.com | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
That's it. One command installs dms and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
|
One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
|
||||||
|
|
||||||
**[Manual Installation Guide →](https://danklinux.com/docs/dankmaterialshell/installation)**
|
**[Manual installation guide](https://danklinux.com/docs/dankmaterialshell/installation)**
|
||||||
|
|
||||||
---
|
## Features
|
||||||
|
|
||||||
## What You Get
|
|
||||||
|
|
||||||
**Dynamic Theming**
|
**Dynamic Theming**
|
||||||
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (like vscode, vscodium), and more with [matugen](https://github.com/InioX/matugen) and [dank16](https://github.com/AvengeMedia/danklinux/blob/master/internal/dank16/dank16.go).
|
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
|
||||||
|
|
||||||
**System Monitoring**
|
**System Monitoring**
|
||||||
Real-time CPU, RAM, GPU metrics and temps with [dgop](https://github.com/AvengeMedia/dgop). Full process list with search and management.
|
Real-time CPU, RAM, GPU metrics and temperatures with [dgop](https://github.com/AvengeMedia/dgop). Process list with search and management.
|
||||||
|
|
||||||
**Powerful Launcher**
|
**Powerful Launcher**
|
||||||
Spotlight-style search for apps, files (via [dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, commands - extensible with plugins.
|
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
|
||||||
|
|
||||||
**Control Center**
|
**Control Center**
|
||||||
Network, Bluetooth, audio devices, display settings, night mode - all in one clean interface.
|
Unified interface for network, Bluetooth, audio devices, display settings, and night mode.
|
||||||
|
|
||||||
**Smart Notifications**
|
**Smart Notifications**
|
||||||
Notification center with grouping, rich text support, and keyboard navigation.
|
Notification center with grouping, rich text support, and keyboard navigation.
|
||||||
|
|
||||||
**Media Integration**
|
**Media Integration**
|
||||||
MPRIS player controls, calendar sync, weather widgets, clipboard history with image previews.
|
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
||||||
|
|
||||||
**Complete Session Management**
|
**Session Management**
|
||||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, greeter support.
|
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
||||||
|
|
||||||
**Plugin System**
|
**Plugin System**
|
||||||
Endless customization with the [plugin registry](https://plugins.danklinux.com).
|
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
||||||
|
|
||||||
**TL;DR** - One shell replaces waybar, swaylock, swayidle, mako, fuzzel, polkit and everything else you normally piece together to create a linux desktop.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Supported Compositors
|
## Supported Compositors
|
||||||
|
|
||||||
DankMaterialShell works best with **[niri](https://github.com/YaLTeR/niri)**, **[Hyprland](https://hyprland.org/)**, **[sway](https://swaywm.org/)**, and **[dwl/MangoWC](https://github.com/DreamMaoMao/mangowc)** - with full workspace switching, overview integration, and monitor management.
|
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), and [labwc](https://labwc.github.io/) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
||||||
|
|
||||||
Other Wayland compositors work too, just with a reduced feature set.
|
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
||||||
|
|
||||||
**[Compositor configuration guide →](https://danklinux.com/docs/dankmaterialshell/compositors)**
|
## Command Line Interface
|
||||||
|
|
||||||
---
|
Control the shell from the command line or keybinds:
|
||||||
|
|
||||||
## Keybinds & IPC
|
|
||||||
|
|
||||||
Control everything from the command line or keybinds:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
dms run # Start the shell
|
||||||
dms ipc call spotlight toggle
|
dms ipc call spotlight toggle
|
||||||
dms ipc call audio setvolume 50
|
dms ipc call audio setvolume 50
|
||||||
dms ipc call wallpaper set /path/to/image.jpg
|
dms ipc call wallpaper set /path/to/image.jpg
|
||||||
dms ipc call theme toggle
|
dms brightness list # List available displays
|
||||||
|
dms plugins search # Browse plugin registry
|
||||||
```
|
```
|
||||||
|
|
||||||
**[Full keybind and IPC documentation →](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)**
|
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Theming
|
|
||||||
|
|
||||||
DankMaterialShell automatically generates color schemes from your wallpaper or theme and applies them to GTK, Qt, terminals, and more.
|
|
||||||
|
|
||||||
DMS is not opinionated or forcing these themes - they are created as optional themes you can enable. You can refer to the documentation if you want to use them:
|
|
||||||
|
|
||||||
**Application theming:** [GTK, Qt, Firefox, terminals, vscode+vscodium →](https://danklinux.com/docs/dankmaterialshell/application-themes)
|
|
||||||
|
|
||||||
**Custom themes:** [Create your own color schemes →](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Plugins
|
|
||||||
|
|
||||||
Extend dms with the plugin system. Browse community plugins at [plugins.danklinux.com](https://plugins.danklinux.com).
|
|
||||||
|
|
||||||
**[Plugin development guide →](https://danklinux.com/docs/dankmaterialshell/plugins-overview)**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
**Website:** [danklinux.com](https://danklinux.com)
|
- **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)
|
||||||
|
|
||||||
**Docs:** [danklinux.com/docs](https://danklinux.com/docs)
|
## Development
|
||||||
|
|
||||||
**Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
See component-specific documentation:
|
||||||
|
|
||||||
---
|
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
|
||||||
|
- **[core/](core/)** - Go backend, CLI tools, and system integration
|
||||||
|
- **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
|
**Core + Dankinstall:**
|
||||||
|
```bash
|
||||||
|
cd core
|
||||||
|
make # Build dms CLI
|
||||||
|
make dankinstall # Build installer
|
||||||
|
```
|
||||||
|
|
||||||
|
**Shell:**
|
||||||
|
```bash
|
||||||
|
quickshell -p quickshell/
|
||||||
|
```
|
||||||
|
|
||||||
|
**NixOS:**
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||||
|
|
||||||
|
# Use in home-manager or NixOS configuration
|
||||||
|
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions welcome! Bug fixes, new widgets, theme improvements, or docs - it all helps.
|
Contributions welcome. Bug fixes, widgets, features, documentation, and plugins all help.
|
||||||
|
|
||||||
**Contributing Code:**
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Set up the development environment
|
2. Make your changes
|
||||||
3. Make your changes
|
3. Test thoroughly
|
||||||
4. Open a pull request
|
4. Open a pull request
|
||||||
|
|
||||||
**Contributing Documentation:**
|
For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs).
|
||||||
1. Fork the [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs) repository
|
|
||||||
2. Update files in the `docs/` folder
|
|
||||||
3. Open a pull request
|
|
||||||
|
|
||||||
### Development Setup
|
|
||||||
|
|
||||||
**Requirements:**
|
|
||||||
- `python3` - Translation management
|
|
||||||
|
|
||||||
**Git Hooks:**
|
|
||||||
|
|
||||||
Enable the pre-commit hook to check translation sync status:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git config core.hooksPath .githooks
|
|
||||||
```
|
|
||||||
|
|
||||||
**Translation Workflow**
|
|
||||||
|
|
||||||
Set POEditor credentials:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export POEDITOR_API_TOKEN="your_api_token"
|
|
||||||
export POEDITOR_PROJECT_ID="your_project_id"
|
|
||||||
```
|
|
||||||
|
|
||||||
Sync translations before committing:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 scripts/i18nsync.py sync
|
|
||||||
```
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Extracts strings from QML files
|
|
||||||
- Uploads changed English terms to POEditor
|
|
||||||
- Downloads updated translations from POEditor
|
|
||||||
- Stages all changes for commit
|
|
||||||
|
|
||||||
The pre-commit hook will block commits if translations are out of sync and remind you to run the sync script.
|
|
||||||
|
|
||||||
Without POEditor credentials, the hook is skipped and commits proceed normally.
|
|
||||||
|
|
||||||
Check the [issues](https://github.com/AvengeMedia/DankMaterialShell/issues) or join the community.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
|
- [quickshell](https://quickshell.org/) - Shell framework
|
||||||
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
|
- [niri](https://github.com/YaLTeR/niri) - Scrolling window manager
|
||||||
- [Ly-sec](http://github.com/ly-sec) for awesome wallpaper effects among other things from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
- [Ly-sec](http://github.com/ly-sec) - Wallpaper effects from [Noctalia](https://github.com/noctalia-dev/noctalia-shell)
|
||||||
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
|
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
|
||||||
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
|
- [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,455 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isHyprland: false
|
|
||||||
property bool isNiri: false
|
|
||||||
property bool isDwl: false
|
|
||||||
property bool isSway: false
|
|
||||||
property string compositor: "unknown"
|
|
||||||
|
|
||||||
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
|
|
||||||
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
|
|
||||||
readonly property string swaySocket: Quickshell.env("SWAYSOCK")
|
|
||||||
property bool useNiriSorting: isNiri && NiriService
|
|
||||||
|
|
||||||
property var sortedToplevels: []
|
|
||||||
property bool _sortScheduled: false
|
|
||||||
|
|
||||||
signal toplevelsChanged()
|
|
||||||
|
|
||||||
function getScreenScale(screen) {
|
|
||||||
if (!screen) return 1
|
|
||||||
|
|
||||||
if (Quickshell.env("QT_WAYLAND_FORCE_DPI")) {
|
|
||||||
return screen.devicePixelRatio || 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNiri && screen) {
|
|
||||||
const niriScale = NiriService.displayScales[screen.name]
|
|
||||||
if (niriScale !== undefined) return niriScale
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHyprland && screen) {
|
|
||||||
const hyprlandMonitor = Hyprland.monitors.values.find(m => m.name === screen.name)
|
|
||||||
if (hyprlandMonitor?.scale !== undefined) return hyprlandMonitor.scale
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDwl && screen) {
|
|
||||||
const dwlScale = DwlService.getOutputScale(screen.name)
|
|
||||||
if (dwlScale !== undefined && dwlScale > 0) return dwlScale
|
|
||||||
}
|
|
||||||
|
|
||||||
return screen?.devicePixelRatio || 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: sortDebounceTimer
|
|
||||||
interval: 100
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
_sortScheduled = false
|
|
||||||
sortedToplevels = computeSortedToplevels()
|
|
||||||
toplevelsChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleSort() {
|
|
||||||
if (_sortScheduled) return
|
|
||||||
_sortScheduled = true
|
|
||||||
sortDebounceTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: ToplevelManager.toplevels
|
|
||||||
function onValuesChanged() { root.scheduleSort() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: isHyprland ? Hyprland : null
|
|
||||||
enabled: isHyprland
|
|
||||||
|
|
||||||
function onRawEvent(event) {
|
|
||||||
if (event.name === "openwindow" ||
|
|
||||||
event.name === "closewindow" ||
|
|
||||||
event.name === "movewindow" ||
|
|
||||||
event.name === "movewindowv2" ||
|
|
||||||
event.name === "workspace" ||
|
|
||||||
event.name === "workspacev2" ||
|
|
||||||
event.name === "focusedmon" ||
|
|
||||||
event.name === "focusedmonv2" ||
|
|
||||||
event.name === "activewindow" ||
|
|
||||||
event.name === "activewindowv2" ||
|
|
||||||
event.name === "changefloatingmode" ||
|
|
||||||
event.name === "fullscreen" ||
|
|
||||||
event.name === "moveintogroup" ||
|
|
||||||
event.name === "moveoutofgroup") {
|
|
||||||
try {
|
|
||||||
Hyprland.refreshToplevels()
|
|
||||||
} catch(e) {}
|
|
||||||
root.scheduleSort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: NiriService
|
|
||||||
function onWindowsChanged() { root.scheduleSort() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
detectCompositor()
|
|
||||||
scheduleSort()
|
|
||||||
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DwlService
|
|
||||||
function onStateChanged() {
|
|
||||||
if (isDwl && !isHyprland && !isNiri) {
|
|
||||||
scheduleSort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeSortedToplevels() {
|
|
||||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values)
|
|
||||||
return []
|
|
||||||
|
|
||||||
if (useNiriSorting)
|
|
||||||
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
|
|
||||||
|
|
||||||
if (isHyprland)
|
|
||||||
return sortHyprlandToplevelsSafe()
|
|
||||||
|
|
||||||
return Array.from(ToplevelManager.toplevels.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
function _get(o, path, fallback) {
|
|
||||||
try {
|
|
||||||
let v = o
|
|
||||||
for (let i = 0; i < path.length; i++) {
|
|
||||||
if (v === null || v === undefined) return fallback
|
|
||||||
v = v[path[i]]
|
|
||||||
}
|
|
||||||
return (v === undefined || v === null) ? fallback : v
|
|
||||||
} catch (e) { return fallback }
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortHyprlandToplevelsSafe() {
|
|
||||||
if (!Hyprland.toplevels || !Hyprland.toplevels.values) return []
|
|
||||||
|
|
||||||
const items = Array.from(Hyprland.toplevels.values)
|
|
||||||
|
|
||||||
function _get(o, path, fb) {
|
|
||||||
try {
|
|
||||||
let v = o
|
|
||||||
for (let k of path) { if (v == null) return fb; v = v[k] }
|
|
||||||
return (v == null) ? fb : v
|
|
||||||
} catch(e) { return fb }
|
|
||||||
}
|
|
||||||
|
|
||||||
let snap = []
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const t = items[i]
|
|
||||||
if (!t) continue
|
|
||||||
|
|
||||||
const addr = t.address || ""
|
|
||||||
if (!addr) continue
|
|
||||||
|
|
||||||
const li = t.lastIpcObject || null
|
|
||||||
|
|
||||||
const monName = _get(li, ["monitor"], null) ?? _get(t, ["monitor", "name"], "")
|
|
||||||
const monX = _get(t, ["monitor", "x"], Number.MAX_SAFE_INTEGER)
|
|
||||||
const monY = _get(t, ["monitor", "y"], Number.MAX_SAFE_INTEGER)
|
|
||||||
|
|
||||||
const wsId = _get(li, ["workspace", "id"], null) ?? _get(t, ["workspace", "id"], Number.MAX_SAFE_INTEGER)
|
|
||||||
|
|
||||||
const at = _get(li, ["at"], null)
|
|
||||||
let atX = (at !== null && at !== undefined && typeof at[0] === "number") ? at[0] : 1e9
|
|
||||||
let atY = (at !== null && at !== undefined && typeof at[1] === "number") ? at[1] : 1e9
|
|
||||||
|
|
||||||
const relX = Number.isFinite(monX) ? (atX - monX) : atX
|
|
||||||
const relY = Number.isFinite(monY) ? (atY - monY) : atY
|
|
||||||
|
|
||||||
snap.push({
|
|
||||||
monKey: String(monName),
|
|
||||||
monOrderX: Number.isFinite(monX) ? monX : Number.MAX_SAFE_INTEGER,
|
|
||||||
monOrderY: Number.isFinite(monY) ? monY : Number.MAX_SAFE_INTEGER,
|
|
||||||
wsId: (typeof wsId === "number") ? wsId : Number.MAX_SAFE_INTEGER,
|
|
||||||
x: relX,
|
|
||||||
y: relY,
|
|
||||||
title: t.title || "",
|
|
||||||
address: addr,
|
|
||||||
wayland: t.wayland
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = new Map()
|
|
||||||
for (const it of snap) {
|
|
||||||
const key = it.monKey + "::" + it.wsId
|
|
||||||
if (!groups.has(key)) groups.set(key, [])
|
|
||||||
groups.get(key).push(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
let groupList = []
|
|
||||||
for (const [key, arr] of groups) {
|
|
||||||
const repr = arr[0]
|
|
||||||
groupList.push({
|
|
||||||
key,
|
|
||||||
monKey: repr.monKey,
|
|
||||||
monOrderX: repr.monOrderX,
|
|
||||||
monOrderY: repr.monOrderY,
|
|
||||||
wsId: repr.wsId,
|
|
||||||
items: arr
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
groupList.sort((a, b) => {
|
|
||||||
if (a.monOrderX !== b.monOrderX) return a.monOrderX - b.monOrderX
|
|
||||||
if (a.monOrderY !== b.monOrderY) return a.monOrderY - b.monOrderY
|
|
||||||
if (a.monKey !== b.monKey) return a.monKey.localeCompare(b.monKey)
|
|
||||||
if (a.wsId !== b.wsId) return a.wsId - b.wsId
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const COLUMN_THRESHOLD = 48
|
|
||||||
const JITTER_Y = 6
|
|
||||||
|
|
||||||
let ordered = []
|
|
||||||
for (const g of groupList) {
|
|
||||||
const arr = g.items
|
|
||||||
|
|
||||||
const xs = arr.map(it => it.x).filter(x => Number.isFinite(x)).sort((a, b) => a - b)
|
|
||||||
let colCenters = []
|
|
||||||
if (xs.length > 0) {
|
|
||||||
for (const x of xs) {
|
|
||||||
if (colCenters.length === 0) {
|
|
||||||
colCenters.push(x)
|
|
||||||
} else {
|
|
||||||
const last = colCenters[colCenters.length - 1]
|
|
||||||
if (x - last >= COLUMN_THRESHOLD) {
|
|
||||||
colCenters.push(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
colCenters = [0]
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const it of arr) {
|
|
||||||
let bestCol = 0
|
|
||||||
let bestDist = Number.POSITIVE_INFINITY
|
|
||||||
for (let ci = 0; ci < colCenters.length; ci++) {
|
|
||||||
const d = Math.abs(it.x - colCenters[ci])
|
|
||||||
if (d < bestDist) {
|
|
||||||
bestDist = d
|
|
||||||
bestCol = ci
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it._col = bestCol
|
|
||||||
}
|
|
||||||
|
|
||||||
arr.sort((a, b) => {
|
|
||||||
if (a._col !== b._col) return a._col - b._col
|
|
||||||
|
|
||||||
const dy = a.y - b.y
|
|
||||||
if (Math.abs(dy) > JITTER_Y) return dy
|
|
||||||
|
|
||||||
if (a.title !== b.title) return a.title.localeCompare(b.title)
|
|
||||||
if (a.address !== b.address) return a.address.localeCompare(b.address)
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
ordered.push.apply(ordered, arr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ordered.map(x => x.wayland).filter(w => w !== null && w !== undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterCurrentWorkspace(toplevels, screen) {
|
|
||||||
if (useNiriSorting) return NiriService.filterCurrentWorkspace(toplevels, screen)
|
|
||||||
if (isHyprland) return filterHyprlandCurrentWorkspaceSafe(toplevels, screen)
|
|
||||||
return toplevels
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterHyprlandCurrentWorkspaceSafe(toplevels, screenName) {
|
|
||||||
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) return toplevels
|
|
||||||
|
|
||||||
let currentWorkspaceId = null
|
|
||||||
try {
|
|
||||||
const hy = Array.from(Hyprland.toplevels.values)
|
|
||||||
for (const t of hy) {
|
|
||||||
const mon = _get(t, ["monitor", "name"], "")
|
|
||||||
const wsId = _get(t, ["workspace", "id"], null)
|
|
||||||
const active = !!_get(t, ["activated"], false)
|
|
||||||
if (mon === screenName && wsId !== null) {
|
|
||||||
if (active) { currentWorkspaceId = wsId; break }
|
|
||||||
if (currentWorkspaceId === null) currentWorkspaceId = wsId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentWorkspaceId === null && Hyprland.workspaces) {
|
|
||||||
const wss = Array.from(Hyprland.workspaces.values)
|
|
||||||
const focusedId = _get(Hyprland, ["focusedWorkspace", "id"], null)
|
|
||||||
for (const ws of wss) {
|
|
||||||
const monName = _get(ws, ["monitor"], "")
|
|
||||||
const wsId = _get(ws, ["id"], null)
|
|
||||||
if (monName === screenName && wsId !== null) {
|
|
||||||
if (focusedId !== null && wsId === focusedId) { currentWorkspaceId = wsId; break }
|
|
||||||
if (currentWorkspaceId === null) currentWorkspaceId = wsId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("CompositorService: workspace snapshot failed:", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentWorkspaceId === null) return toplevels
|
|
||||||
|
|
||||||
// Map wayland → wsId snapshot
|
|
||||||
let map = new Map()
|
|
||||||
try {
|
|
||||||
const hy = Array.from(Hyprland.toplevels.values)
|
|
||||||
for (const t of hy) {
|
|
||||||
const wsId = _get(t, ["workspace", "id"], null)
|
|
||||||
if (t && t.wayland && wsId !== null) map.set(t.wayland, wsId)
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
return toplevels.filter(w => map.get(w) === currentWorkspaceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: compositorInitTimer
|
|
||||||
interval: 100
|
|
||||||
running: true
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
detectCompositor()
|
|
||||||
Qt.callLater(() => NiriService.generateNiriLayoutConfig())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCompositor() {
|
|
||||||
if (hyprlandSignature && hyprlandSignature.length > 0) {
|
|
||||||
isHyprland = true
|
|
||||||
isNiri = false
|
|
||||||
isDwl = false
|
|
||||||
isSway = false
|
|
||||||
compositor = "hyprland"
|
|
||||||
console.info("CompositorService: Detected Hyprland")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (niriSocket && niriSocket.length > 0) {
|
|
||||||
Proc.runCommand("niriSocketCheck", ["test", "-S", niriSocket], (output, exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
isNiri = true
|
|
||||||
isHyprland = false
|
|
||||||
isDwl = false
|
|
||||||
isSway = false
|
|
||||||
compositor = "niri"
|
|
||||||
console.info("CompositorService: Detected Niri with socket:", niriSocket)
|
|
||||||
NiriService.generateNiriBinds()
|
|
||||||
NiriService.generateNiriBlurrule()
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (swaySocket && swaySocket.length > 0) {
|
|
||||||
Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
isNiri = false
|
|
||||||
isHyprland = false
|
|
||||||
isDwl = false
|
|
||||||
isSway = true
|
|
||||||
compositor = "sway"
|
|
||||||
console.info("CompositorService: Detected Sway with socket:", swaySocket)
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DMSService.dmsAvailable) {
|
|
||||||
Qt.callLater(checkForDwl)
|
|
||||||
} else {
|
|
||||||
isHyprland = false
|
|
||||||
isNiri = false
|
|
||||||
isDwl = false
|
|
||||||
isSway = false
|
|
||||||
compositor = "unknown"
|
|
||||||
console.warn("CompositorService: No compositor detected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DMSService
|
|
||||||
function onCapabilitiesReceived() {
|
|
||||||
if (!isHyprland && !isNiri && !isDwl) {
|
|
||||||
checkForDwl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForDwl() {
|
|
||||||
if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) {
|
|
||||||
isHyprland = false
|
|
||||||
isNiri = false
|
|
||||||
isDwl = true
|
|
||||||
compositor = "dwl"
|
|
||||||
console.info("CompositorService: Detected DWL via DMS capability")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function powerOffMonitors() {
|
|
||||||
if (isNiri) return NiriService.powerOffMonitors()
|
|
||||||
if (isHyprland) return Hyprland.dispatch("dpms off")
|
|
||||||
if (isDwl) return _dwlPowerOffMonitors()
|
|
||||||
if (isSway) { try { I3.dispatch("output * dpms off") } catch(_){} return }
|
|
||||||
console.warn("CompositorService: Cannot power off monitors, unknown compositor")
|
|
||||||
}
|
|
||||||
|
|
||||||
function powerOnMonitors() {
|
|
||||||
if (isNiri) return NiriService.powerOnMonitors()
|
|
||||||
if (isHyprland) return Hyprland.dispatch("dpms on")
|
|
||||||
if (isDwl) return _dwlPowerOnMonitors()
|
|
||||||
if (isSway) { try { I3.dispatch("output * dpms on") } catch(_){} return }
|
|
||||||
console.warn("CompositorService: Cannot power on monitors, unknown compositor")
|
|
||||||
}
|
|
||||||
|
|
||||||
function _dwlPowerOffMonitors() {
|
|
||||||
if (!Quickshell.screens || Quickshell.screens.length === 0) {
|
|
||||||
console.warn("CompositorService: No screens available for DWL power off")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
|
||||||
const screen = Quickshell.screens[i]
|
|
||||||
if (screen && screen.name) {
|
|
||||||
Quickshell.execDetached(["mmsg", "-d", "disable_monitor," + screen.name])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _dwlPowerOnMonitors() {
|
|
||||||
if (!Quickshell.screens || Quickshell.screens.length === 0) {
|
|
||||||
console.warn("CompositorService: No screens available for DWL power on")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
|
||||||
const screen = Quickshell.screens[i]
|
|
||||||
if (screen && screen.name) {
|
|
||||||
Quickshell.execDetached(["mmsg", "-d", "enable_monitor," + screen.name])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string blurNamespace: "dms:osd"
|
|
||||||
WlrLayershell.namespace: blurNamespace
|
|
||||||
|
|
||||||
property alias content: contentLoader.sourceComponent
|
|
||||||
property alias contentLoader: contentLoader
|
|
||||||
property var modelData
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property int autoHideInterval: 2000
|
|
||||||
property bool enableMouseInteraction: false
|
|
||||||
property real osdWidth: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
property real osdHeight: Theme.iconSize + Theme.spacingS * 2
|
|
||||||
property int animationDuration: Theme.mediumDuration
|
|
||||||
property var animationEasing: Theme.emphasizedEasing
|
|
||||||
|
|
||||||
signal osdShown
|
|
||||||
signal osdHidden
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
hideTimer.restart()
|
|
||||||
osdShown()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
shouldBeVisible = false
|
|
||||||
closeTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetHideTimer() {
|
|
||||||
if (shouldBeVisible) {
|
|
||||||
hideTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHoverState() {
|
|
||||||
let isHovered = (enableMouseInteraction && mouseArea.containsMouse) || osdContainer.childHovered
|
|
||||||
if (enableMouseInteraction) {
|
|
||||||
if (isHovered) {
|
|
||||||
hideTimer.stop()
|
|
||||||
} else if (shouldBeVisible) {
|
|
||||||
hideTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setChildHovered(hovered) {
|
|
||||||
osdContainer.childHovered = hovered
|
|
||||||
updateHoverState()
|
|
||||||
}
|
|
||||||
|
|
||||||
screen: modelData
|
|
||||||
visible: false
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
|
||||||
readonly property real screenWidth: screen.width
|
|
||||||
readonly property real screenHeight: screen.height
|
|
||||||
readonly property real alignedWidth: Theme.px(osdWidth, dpr)
|
|
||||||
readonly property real alignedHeight: Theme.px(osdHeight, dpr)
|
|
||||||
readonly property real alignedX: Theme.snap((screenWidth - alignedWidth) / 2, dpr)
|
|
||||||
readonly property real alignedY: Theme.snap(screenHeight - alignedHeight - Theme.spacingM, dpr)
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: hideTimer
|
|
||||||
|
|
||||||
interval: autoHideInterval
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
if (!enableMouseInteraction || !mouseArea.containsMouse) {
|
|
||||||
hide()
|
|
||||||
} else {
|
|
||||||
hideTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
interval: animationDuration + 50
|
|
||||||
onTriggered: {
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
visible = false
|
|
||||||
osdHidden()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: osdContainer
|
|
||||||
x: alignedX
|
|
||||||
y: alignedY
|
|
||||||
width: alignedWidth
|
|
||||||
height: alignedHeight
|
|
||||||
opacity: shouldBeVisible ? 1 : 0
|
|
||||||
scale: shouldBeVisible ? 1 : 0.9
|
|
||||||
|
|
||||||
property bool childHovered: false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: osdBackground
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: enableMouseInteraction
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
z: -1
|
|
||||||
onContainsMouseChanged: updateHoverState()
|
|
||||||
}
|
|
||||||
|
|
||||||
onChildHoveredChanged: updateHoverState()
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root.visible
|
|
||||||
asynchronous: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: animationDuration
|
|
||||||
easing.type: animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: animationDuration
|
|
||||||
easing.type: animationEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mask: Region {
|
|
||||||
item: osdBackground
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string layerNamespace: "dms:popout"
|
|
||||||
WlrLayershell.namespace: layerNamespace
|
|
||||||
|
|
||||||
property alias content: contentLoader.sourceComponent
|
|
||||||
property alias contentLoader: contentLoader
|
|
||||||
property real popupWidth: 400
|
|
||||||
property real popupHeight: 300
|
|
||||||
property real triggerX: 0
|
|
||||||
property real triggerY: 0
|
|
||||||
property real triggerWidth: 40
|
|
||||||
property string triggerSection: ""
|
|
||||||
property string positioning: "center"
|
|
||||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
|
||||||
property real animationScaleCollapsed: 0.96
|
|
||||||
property real animationOffset: Theme.spacingL
|
|
||||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
|
||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
|
||||||
property bool shouldBeVisible: false
|
|
||||||
property int keyboardFocusMode: WlrKeyboardFocus.OnDemand
|
|
||||||
|
|
||||||
signal opened
|
|
||||||
signal popoutClosed
|
|
||||||
signal backgroundClicked
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
closeTimer.stop()
|
|
||||||
shouldBeVisible = true
|
|
||||||
visible = true
|
|
||||||
opened()
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
shouldBeVisible = false
|
|
||||||
closeTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (shouldBeVisible)
|
|
||||||
close()
|
|
||||||
else
|
|
||||||
open()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: closeTimer
|
|
||||||
interval: animationDuration
|
|
||||||
onTriggered: {
|
|
||||||
if (!shouldBeVisible) {
|
|
||||||
visible = false
|
|
||||||
popoutClosed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: shouldBeVisible ? keyboardFocusMode : WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real screenWidth: root.screen.width
|
|
||||||
readonly property real screenHeight: root.screen.height
|
|
||||||
readonly property real dpr: CompositorService.getScreenScale(root.screen)
|
|
||||||
|
|
||||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
|
||||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
|
||||||
readonly property real alignedX: Theme.snap((() => {
|
|
||||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left) {
|
|
||||||
return triggerY + SettingsData.dankBarBottomGap
|
|
||||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
|
||||||
return screenWidth - triggerY - SettingsData.dankBarBottomGap - popupWidth
|
|
||||||
} else {
|
|
||||||
const centerX = triggerX + (triggerWidth / 2) - (popupWidth / 2)
|
|
||||||
return Math.max(Theme.popupDistance, Math.min(screenWidth - popupWidth - Theme.popupDistance, centerX))
|
|
||||||
}
|
|
||||||
})(), dpr)
|
|
||||||
readonly property real alignedY: Theme.snap((() => {
|
|
||||||
if (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right) {
|
|
||||||
const centerY = triggerX + (triggerWidth / 2) - (popupHeight / 2)
|
|
||||||
return Math.max(Theme.popupDistance, Math.min(screenHeight - popupHeight - Theme.popupDistance, centerY))
|
|
||||||
} else if (SettingsData.dankBarPosition === SettingsData.Position.Bottom) {
|
|
||||||
return Math.max(Theme.popupDistance, screenHeight - triggerY - popupHeight)
|
|
||||||
} else {
|
|
||||||
return Math.min(screenHeight - popupHeight - Theme.popupDistance, triggerY)
|
|
||||||
}
|
|
||||||
})(), dpr)
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: shouldBeVisible && contentLoader.opacity > 0.1
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (mouse.x < alignedX || mouse.x > alignedX + alignedWidth ||
|
|
||||||
mouse.y < alignedY || mouse.y > alignedY + alignedHeight) {
|
|
||||||
backgroundClicked()
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: contentContainer
|
|
||||||
x: alignedX
|
|
||||||
y: alignedY
|
|
||||||
width: alignedWidth
|
|
||||||
height: alignedHeight
|
|
||||||
|
|
||||||
readonly property bool barTop: SettingsData.dankBarPosition === SettingsData.Position.Top
|
|
||||||
readonly property bool barBottom: SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
|
||||||
readonly property bool barLeft: SettingsData.dankBarPosition === SettingsData.Position.Left
|
|
||||||
readonly property bool barRight: SettingsData.dankBarPosition === SettingsData.Position.Right
|
|
||||||
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
|
||||||
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
|
||||||
|
|
||||||
property real animX: 0
|
|
||||||
property real animY: 0
|
|
||||||
property real scaleValue: root.animationScaleCollapsed
|
|
||||||
|
|
||||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
|
||||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onShouldBeVisibleChanged() {
|
|
||||||
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr)
|
|
||||||
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr)
|
|
||||||
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on animX {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on animY {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scaleValue {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
active: root.visible
|
|
||||||
asynchronous: false
|
|
||||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
|
||||||
layer.smooth: false
|
|
||||||
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
|
|
||||||
layer.textureMirroring: ShaderEffectSource.NoMirroring
|
|
||||||
opacity: shouldBeVisible ? 1 : 0
|
|
||||||
visible: opacity > 0
|
|
||||||
scale: contentContainer.scaleValue
|
|
||||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
|
||||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: animationDuration
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
parent: contentContainer
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
close()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component.onCompleted: forceActiveFocus()
|
|
||||||
onVisibleChanged: if (visible) forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+25
-17
@@ -4,15 +4,23 @@
|
|||||||
<svg
|
<svg
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
width="524.44849"
|
width="482.90668"
|
||||||
height="524.5979"
|
height="558.15088"
|
||||||
viewBox="0 0 524.44848 524.5979"
|
viewBox="0 0 482.90667 558.15088"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
shape-rendering="geometricPrecision">
|
shape-rendering="auto"
|
||||||
|
style="image-rendering: auto; filter: url(#smoothing);">
|
||||||
<defs
|
<defs
|
||||||
id="defs1">
|
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
|
<clipPath
|
||||||
clipPathUnits="userSpaceOnUse"
|
clipPathUnits="userSpaceOnUse"
|
||||||
id="clipPath21">
|
id="clipPath21">
|
||||||
@@ -64,42 +72,42 @@
|
|||||||
</defs>
|
</defs>
|
||||||
<g
|
<g
|
||||||
id="layer-MC0"
|
id="layer-MC0"
|
||||||
transform="translate(-475.12476,-548.5802)">
|
transform="translate(-486.31024,-515.02722)">
|
||||||
<path
|
<path
|
||||||
id="path20"
|
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"
|
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:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
transform="matrix(1.4480327,0,0,-1.2531805,922.77315,685.7674)"
|
transform="matrix(1.3333333,0,0,-1.3333333,898.49907,660.9888)"
|
||||||
clip-path="url(#clipPath21)" />
|
clip-path="url(#clipPath21)" />
|
||||||
<path
|
<path
|
||||||
id="path24"
|
id="path24"
|
||||||
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
|
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
|
||||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
transform="matrix(1.4480327,0,0,-1.2531805,622.20527,686.59525)"
|
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
|
||||||
clip-path="url(#clipPath25)" />
|
clip-path="url(#clipPath25)" />
|
||||||
<path
|
<path
|
||||||
id="path26"
|
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"
|
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:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
transform="matrix(1.4480327,0,0,-1.2531805,953.76928,974.41373)"
|
transform="matrix(1.3333333,0,0,-1.3333333,927.04,968.0968)"
|
||||||
clip-path="url(#clipPath27)" />
|
clip-path="url(#clipPath27)" />
|
||||||
<path
|
<path
|
||||||
id="path28"
|
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"
|
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:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
transform="matrix(1.4480327,0,0,-1.2531805,610.09146,775.07882)"
|
transform="matrix(1.3333333,0,0,-1.3333333,610.58507,756.01253)"
|
||||||
clip-path="url(#clipPath29)" />
|
clip-path="url(#clipPath29)" />
|
||||||
<path
|
<path
|
||||||
id="path30"
|
id="path30"
|
||||||
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
|
d="M 0,0 -0.169,-0.292 C 1.43,-0.2 3.091,-0.108 4.798,0 Z"
|
||||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
transform="matrix(1.4480327,0,0,-1.2531805,622.20527,686.59525)"
|
transform="matrix(1.3333333,0,0,-1.3333333,621.73933,661.8696)"
|
||||||
clip-path="url(#clipPath31)" />
|
clip-path="url(#clipPath31)" />
|
||||||
<path
|
<path
|
||||||
id="path32"
|
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"
|
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:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
style="fill:#D0BCFF;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
transform="matrix(1.4480327,0,0,-1.2531805,836.82211,767.63192)"
|
transform="matrix(1.3333333,0,0,-1.3333333,819.35627,748.08933)"
|
||||||
clip-path="url(#clipPath33)" />
|
clip-path="url(#clipPath33)" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
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/danklinux/internal/server/network:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/network"
|
||||||
|
outpkg: mocks_network
|
||||||
|
interfaces:
|
||||||
|
Backend:
|
||||||
|
github.com/AvengeMedia/danklinux/internal/server/cups:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/cups"
|
||||||
|
outpkg: mocks_cups
|
||||||
|
interfaces:
|
||||||
|
CUPSClientInterface:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/evdev"
|
||||||
|
outpkg: mocks_evdev
|
||||||
|
interfaces:
|
||||||
|
EvdevDevice:
|
||||||
+157
@@ -0,0 +1,157 @@
|
|||||||
|
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
|
||||||
|
VERSION=$(shell git describe --tags --always 2>/dev/null || echo "dev")
|
||||||
|
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
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 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)"
|
||||||
|
|
||||||
|
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"
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
# DMS Backend & CLI
|
||||||
|
|
||||||
|
Go-based backend for DankMaterialShell providing system integration, IPC, and installation tools.
|
||||||
|
|
||||||
|
**See [root README](../README.md) for project overview and installation.**
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
**dms CLI**
|
||||||
|
Command-line interface and daemon for shell management and system control.
|
||||||
|
|
||||||
|
**dankinstall**
|
||||||
|
Distribution-aware installer with TUI for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo.
|
||||||
|
|
||||||
|
## System Integration
|
||||||
|
|
||||||
|
**Wayland Protocols**
|
||||||
|
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
||||||
|
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
||||||
|
- `ext-workspace-v1` - Workspace protocol support
|
||||||
|
- `wlr-output-management-unstable-v1` - Display configuration
|
||||||
|
|
||||||
|
**DBus Interfaces**
|
||||||
|
- NetworkManager/iwd - Network management
|
||||||
|
- logind - Session control and inhibit locks
|
||||||
|
- accountsservice - User account information
|
||||||
|
- CUPS - Printer management
|
||||||
|
- Custom IPC via unix socket (JSON API)
|
||||||
|
|
||||||
|
**Hardware Control**
|
||||||
|
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
|
||||||
|
- Backlight control - Internal display brightness via `login1` or sysfs
|
||||||
|
- LED control - Keyboard/device LED management
|
||||||
|
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
|
||||||
|
|
||||||
|
**Plugin System**
|
||||||
|
- Plugin registry integration
|
||||||
|
- Plugin lifecycle management
|
||||||
|
- Settings persistence
|
||||||
|
|
||||||
|
## CLI Commands
|
||||||
|
- `dms run [-d]` - Start shell (optionally as daemon)
|
||||||
|
- `dms restart` / `dms kill` - Manage running processes
|
||||||
|
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
||||||
|
- `dms plugins [install|browse|search]` - Plugin management
|
||||||
|
- `dms brightness [list|set]` - Control display/monitor brightness
|
||||||
|
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
||||||
|
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requires Go 1.24+
|
||||||
|
|
||||||
|
**Development build:**
|
||||||
|
```bash
|
||||||
|
make # Build dms CLI
|
||||||
|
make dankinstall # Build installer
|
||||||
|
make test # Run tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Distribution build:**
|
||||||
|
```bash
|
||||||
|
make dist # Build without update/greeter features
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
```bash
|
||||||
|
sudo make install # Install to /usr/local/bin/dms
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
**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.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,113 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/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-*
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
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),
|
||||||
|
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),
|
||||||
|
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 runBrightnessList(cmd *cobra.Command, args []string) {
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allDevices) == 0 {
|
||||||
|
fmt.Println("No brightness devices found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxIDLen := len("Device")
|
||||||
|
maxNameLen := len("Name")
|
||||||
|
for _, dev := range allDevices {
|
||||||
|
if len(dev.ID) > maxIDLen {
|
||||||
|
maxIDLen = len(dev.ID)
|
||||||
|
}
|
||||||
|
if len(dev.Name) > maxNameLen {
|
||||||
|
maxNameLen = len(dev.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idPad := maxIDLen + 2
|
||||||
|
namePad := maxNameLen + 2
|
||||||
|
|
||||||
|
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
|
||||||
|
|
||||||
|
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
|
||||||
|
for i := 0; i < sepLen; i++ {
|
||||||
|
fmt.Print("─")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
for _, device := range allDevices {
|
||||||
|
fmt.Printf("%-*s %-12s %-*s %3d%%\n",
|
||||||
|
idPad,
|
||||||
|
device.ID,
|
||||||
|
device.Class,
|
||||||
|
namePad,
|
||||||
|
device.Name,
|
||||||
|
device.CurrentPercent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBrightnessSet(cmd *cobra.Command, args []string) {
|
||||||
|
deviceID := args[0]
|
||||||
|
var percent int
|
||||||
|
if _, err := fmt.Sscanf(args[1], "%d", &percent); err != nil {
|
||||||
|
log.Fatalf("Invalid percent value: %s", args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if percent < 0 || percent > 100 {
|
||||||
|
log.Fatalf("Percent must be between 0 and 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
exponential, _ := cmd.Flags().GetBool("exponential")
|
||||||
|
exponent, _ := cmd.Flags().GetFloat64("exponent")
|
||||||
|
|
||||||
|
// For backlight/leds devices, try logind backend first (requires D-Bus connection)
|
||||||
|
parts := strings.SplitN(deviceID, ":", 2)
|
||||||
|
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
|
||||||
|
subsystem := parts[0]
|
||||||
|
name := parts[1]
|
||||||
|
|
||||||
|
// Initialize backends needed for logind approach
|
||||||
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("NewSysfsBackend failed: %v", err)
|
||||||
|
} else {
|
||||||
|
logind, err := brightness.NewLogindBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("NewLogindBackend failed: %v", err)
|
||||||
|
} else {
|
||||||
|
defer logind.Close()
|
||||||
|
|
||||||
|
// Get device info to convert percent to value
|
||||||
|
dev, err := sysfs.GetDevice(deviceID)
|
||||||
|
if err == nil {
|
||||||
|
// Calculate hardware value using the same logic as Manager.setViaSysfsWithLogind
|
||||||
|
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
|
||||||
|
|
||||||
|
// Call logind with hardware value
|
||||||
|
if err := logind.SetBrightness(subsystem, name, uint32(value)); err == nil {
|
||||||
|
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
|
||||||
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Debugf("logind.SetBrightness failed: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to direct sysfs (requires write permissions)
|
||||||
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
|
if err == nil {
|
||||||
|
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
|
||||||
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("sysfs.SetBrightness failed: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("NewSysfsBackend failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try DDC if requested
|
||||||
|
if includeDDC {
|
||||||
|
ddc, err := brightness.NewDDCBackend()
|
||||||
|
if err == nil {
|
||||||
|
defer ddc.Close()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
||||||
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("ddc.SetBrightness failed: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("NewDDCBackend failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatalf("Failed to set brightness for device: %s", deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBrightnessGet(cmd *cobra.Command, args []string) {
|
||||||
|
deviceID := args[0]
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
|
||||||
|
allDevices := []brightness.Device{}
|
||||||
|
|
||||||
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
|
if err == nil {
|
||||||
|
devices, err := sysfs.GetDevices()
|
||||||
|
if err == nil {
|
||||||
|
allDevices = append(allDevices, devices...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeDDC {
|
||||||
|
ddc, err := brightness.NewDDCBackend()
|
||||||
|
if err == nil {
|
||||||
|
defer ddc.Close()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
devices, err := ddc.GetDevices()
|
||||||
|
if err == nil {
|
||||||
|
allDevices = append(allDevices, devices...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, device := range allDevices {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Show version information",
|
||||||
|
Run: runVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
var runCmd = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "Launch quickshell with DMS configuration",
|
||||||
|
Long: "Launch quickshell with DMS configuration (qs -c dms)",
|
||||||
|
PreRunE: findConfig,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
daemon, _ := cmd.Flags().GetBool("daemon")
|
||||||
|
session, _ := cmd.Flags().GetBool("session")
|
||||||
|
if daemon {
|
||||||
|
runShellDaemon(session)
|
||||||
|
} else {
|
||||||
|
runShellInteractive(session)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var restartCmd = &cobra.Command{
|
||||||
|
Use: "restart",
|
||||||
|
Short: "Restart quickshell with DMS configuration",
|
||||||
|
Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration",
|
||||||
|
PreRunE: findConfig,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
restartShell()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var restartDetachedCmd = &cobra.Command{
|
||||||
|
Use: "restart-detached <pid>",
|
||||||
|
Hidden: true,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
PreRunE: findConfig,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
runDetachedRestart(args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var killCmd = &cobra.Command{
|
||||||
|
Use: "kill",
|
||||||
|
Short: "Kill running DMS shell processes",
|
||||||
|
Long: "Kill all running quickshell processes with DMS configuration",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
killShell()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipcCmd = &cobra.Command{
|
||||||
|
Use: "ipc",
|
||||||
|
Short: "Send IPC commands to running DMS shell",
|
||||||
|
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
||||||
|
PreRunE: findConfig,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
runShellIPCCommand(args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugSrvCmd = &cobra.Command{
|
||||||
|
Use: "debug-srv",
|
||||||
|
Short: "Start the debug server",
|
||||||
|
Long: "Start the Unix socket debug server for DMS",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := startDebugServer(); err != nil {
|
||||||
|
log.Fatalf("Error starting debug server: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsCmd = &cobra.Command{
|
||||||
|
Use: "plugins",
|
||||||
|
Short: "Manage DMS plugins",
|
||||||
|
Long: "Browse and manage DMS plugins from the registry",
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsBrowseCmd = &cobra.Command{
|
||||||
|
Use: "browse",
|
||||||
|
Short: "Browse available plugins",
|
||||||
|
Long: "Browse available plugins from the DMS plugin registry",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := browsePlugins(); err != nil {
|
||||||
|
log.Fatalf("Error browsing plugins: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List installed plugins",
|
||||||
|
Long: "List all installed DMS plugins",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := listInstalledPlugins(); err != nil {
|
||||||
|
log.Fatalf("Error listing plugins: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsInstallCmd = &cobra.Command{
|
||||||
|
Use: "install <plugin-id>",
|
||||||
|
Short: "Install a plugin by ID",
|
||||||
|
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
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),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := uninstallPluginCLI(args[0]); err != nil {
|
||||||
|
log.Fatalf("Error uninstalling plugin: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVersion(cmd *cobra.Command, args []string) {
|
||||||
|
printASCII()
|
||||||
|
fmt.Printf("%s\n", Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startDebugServer() error {
|
||||||
|
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 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, 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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCommonCommands returns the commands available in all builds
|
||||||
|
func getCommonCommands() []*cobra.Command {
|
||||||
|
return []*cobra.Command{
|
||||||
|
versionCmd,
|
||||||
|
runCmd,
|
||||||
|
restartCmd,
|
||||||
|
restartDetachedCmd,
|
||||||
|
killCmd,
|
||||||
|
ipcCmd,
|
||||||
|
debugSrvCmd,
|
||||||
|
pluginsCmd,
|
||||||
|
dank16Cmd,
|
||||||
|
brightnessCmd,
|
||||||
|
dpmsCmd,
|
||||||
|
keybindsCmd,
|
||||||
|
greeterCmd,
|
||||||
|
setupCmd,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"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.ExactArgs(1),
|
||||||
|
Run: runDank16,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant")
|
||||||
|
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
||||||
|
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
||||||
|
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
||||||
|
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
||||||
|
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
||||||
|
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
||||||
|
dank16Cmd.Flags().String("vscode-enrich", "", "Enrich existing VSCode theme file with terminal colors")
|
||||||
|
dank16Cmd.Flags().String("background", "", "Custom background color")
|
||||||
|
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDank16(cmd *cobra.Command, args []string) {
|
||||||
|
primaryColor := args[0]
|
||||||
|
if !strings.HasPrefix(primaryColor, "#") {
|
||||||
|
primaryColor = "#" + primaryColor
|
||||||
|
}
|
||||||
|
|
||||||
|
isLight, _ := cmd.Flags().GetBool("light")
|
||||||
|
isJson, _ := cmd.Flags().GetBool("json")
|
||||||
|
isKitty, _ := cmd.Flags().GetBool("kitty")
|
||||||
|
isFoot, _ := cmd.Flags().GetBool("foot")
|
||||||
|
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
||||||
|
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
||||||
|
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
||||||
|
vscodeEnrich, _ := cmd.Flags().GetString("vscode-enrich")
|
||||||
|
background, _ := cmd.Flags().GetString("background")
|
||||||
|
contrastAlgo, _ := cmd.Flags().GetString("contrast")
|
||||||
|
|
||||||
|
if background != "" && !strings.HasPrefix(background, "#") {
|
||||||
|
background = "#" + background
|
||||||
|
}
|
||||||
|
|
||||||
|
contrastAlgo = strings.ToLower(contrastAlgo)
|
||||||
|
if contrastAlgo != "dps" && contrastAlgo != "wcag" {
|
||||||
|
log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := dank16.PaletteOptions{
|
||||||
|
IsLight: isLight,
|
||||||
|
Background: background,
|
||||||
|
UseDPS: contrastAlgo == "dps",
|
||||||
|
}
|
||||||
|
|
||||||
|
colors := dank16.GeneratePalette(primaryColor, opts)
|
||||||
|
|
||||||
|
if vscodeEnrich != "" {
|
||||||
|
data, err := os.ReadFile(vscodeEnrich)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error reading file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enriched, err := dank16.EnrichVSCodeTheme(data, colors)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error enriching theme: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(enriched))
|
||||||
|
} else if isJson {
|
||||||
|
fmt.Print(dank16.GenerateJSON(colors))
|
||||||
|
} else if isKitty {
|
||||||
|
fmt.Print(dank16.GenerateKittyTheme(colors))
|
||||||
|
} else if isFoot {
|
||||||
|
fmt.Print(dank16.GenerateFootTheme(colors))
|
||||||
|
} else if isAlacritty {
|
||||||
|
fmt.Print(dank16.GenerateAlacrittyTheme(colors))
|
||||||
|
} else if isGhostty {
|
||||||
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
|
} else if isWezterm {
|
||||||
|
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
||||||
|
} else {
|
||||||
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
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),
|
||||||
|
Run: runDPMSOn,
|
||||||
|
}
|
||||||
|
|
||||||
|
var dpmsOffCmd = &cobra.Command{
|
||||||
|
Use: "off [output]",
|
||||||
|
Short: "Turn display(s) off",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,488 @@
|
|||||||
|
//go:build !distro_binary
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/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.FamilyNix:
|
||||||
|
updateErr = updateNixOS()
|
||||||
|
case distros.FamilySUSE:
|
||||||
|
updateErr = updateOtherDistros()
|
||||||
|
default:
|
||||||
|
updateErr = updateOtherDistros()
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateErr != nil {
|
||||||
|
if errors.Is(updateErr, errdefs.ErrUpdateCancelled) {
|
||||||
|
log.Info("Update cancelled.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(updateErr, errdefs.ErrNoUpdateNeeded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Fatalf("Error updating DMS: %v", updateErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Update complete! Restarting DMS...")
|
||||||
|
restartShell()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateArchLinux() error {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err == nil {
|
||||||
|
dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms")
|
||||||
|
if _, err := os.Stat(dmsPath); err == nil {
|
||||||
|
return updateOtherDistros()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var packageName string
|
||||||
|
if isArchPackageInstalled("dms-shell-bin") {
|
||||||
|
packageName = "dms-shell-bin"
|
||||||
|
} else if isArchPackageInstalled("dms-shell-git") {
|
||||||
|
packageName = "dms-shell-git"
|
||||||
|
} else {
|
||||||
|
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
||||||
|
fmt.Println("Info: Falling back to git-based update method...")
|
||||||
|
return updateOtherDistros()
|
||||||
|
}
|
||||||
|
|
||||||
|
var helper string
|
||||||
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
|
if commandExists("yay") {
|
||||||
|
helper = "yay"
|
||||||
|
updateCmd = exec.Command("yay", "-S", packageName)
|
||||||
|
} else if 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 updateNixOS() error {
|
||||||
|
fmt.Println("This will update DankMaterialShell using nix profile.")
|
||||||
|
if !confirmUpdate() {
|
||||||
|
return errdefs.ErrUpdateCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nRunning: nix profile upgrade github:AvengeMedia/DankMaterialShell")
|
||||||
|
updateCmd := exec.Command("nix", "profile", "upgrade", "github:AvengeMedia/DankMaterialShell")
|
||||||
|
updateCmd.Stdout = os.Stdout
|
||||||
|
updateCmd.Stderr = os.Stderr
|
||||||
|
err := updateCmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: Failed to update using nix profile: %v\n", err)
|
||||||
|
fmt.Println("Falling back to git-based update method...")
|
||||||
|
return updateOtherDistros()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("dms successfully updated")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateOtherDistros() error {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms")
|
||||||
|
|
||||||
|
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("DMS configuration directory not found at %s", dmsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Found DMS configuration at %s\n", dmsPath)
|
||||||
|
|
||||||
|
versionInfo, err := version.GetDMSVersionInfo()
|
||||||
|
if err == nil && !versionInfo.HasUpdate {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("Current version: %s\n", versionInfo.Current)
|
||||||
|
fmt.Printf("Latest version: %s\n", versionInfo.Latest)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("✓ You are already running the latest version.")
|
||||||
|
return errdefs.ErrNoUpdateNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nThis will update:")
|
||||||
|
fmt.Println(" 1. The dms binary from GitHub releases")
|
||||||
|
fmt.Println(" 2. DankMaterialShell configuration using git")
|
||||||
|
if !confirmUpdate() {
|
||||||
|
return errdefs.ErrUpdateCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Updating dms binary ===")
|
||||||
|
if err := updateDMSBinary(); err != nil {
|
||||||
|
fmt.Printf("Warning: Failed to update dms binary: %v\n", err)
|
||||||
|
fmt.Println("Continuing with shell configuration update...")
|
||||||
|
} else {
|
||||||
|
fmt.Println("dms binary successfully updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Updating DMS shell configuration ===")
|
||||||
|
|
||||||
|
if err := os.Chdir(dmsPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to change to DMS directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCmd := exec.Command("git", "status", "--porcelain")
|
||||||
|
statusOutput, _ := statusCmd.Output()
|
||||||
|
hasLocalChanges := len(strings.TrimSpace(string(statusOutput))) > 0
|
||||||
|
|
||||||
|
currentRefCmd := exec.Command("git", "symbolic-ref", "-q", "HEAD")
|
||||||
|
currentRefOutput, _ := currentRefCmd.Output()
|
||||||
|
onBranch := len(currentRefOutput) > 0
|
||||||
|
|
||||||
|
var currentTag string
|
||||||
|
var currentBranch string
|
||||||
|
|
||||||
|
if !onBranch {
|
||||||
|
tagCmd := exec.Command("git", "describe", "--exact-match", "--tags", "HEAD")
|
||||||
|
if tagOutput, err := tagCmd.Output(); err == nil {
|
||||||
|
currentTag = strings.TrimSpace(string(tagOutput))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
branchCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
if branchOutput, err := branchCmd.Output(); err == nil {
|
||||||
|
currentBranch = strings.TrimSpace(string(branchOutput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Fetching latest changes...")
|
||||||
|
fetchCmd := exec.Command("git", "fetch", "origin", "--tags", "--force")
|
||||||
|
fetchCmd.Stdout = os.Stdout
|
||||||
|
fetchCmd.Stderr = os.Stderr
|
||||||
|
if err := fetchCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch changes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentTag != "" {
|
||||||
|
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
|
||||||
|
latestTagOutput, err := latestTagCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get latest tag: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := strings.Split(strings.TrimSpace(string(latestTagOutput)), "\n")
|
||||||
|
if len(tags) == 0 || tags[0] == "" {
|
||||||
|
return fmt.Errorf("no version tags found")
|
||||||
|
}
|
||||||
|
latestTag := tags[0]
|
||||||
|
|
||||||
|
if latestTag == currentTag {
|
||||||
|
fmt.Printf("Already on latest tag: %s\n", currentTag)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Current tag: %s\n", currentTag)
|
||||||
|
fmt.Printf("Latest tag: %s\n", latestTag)
|
||||||
|
|
||||||
|
if hasLocalChanges {
|
||||||
|
fmt.Println("\nWarning: You have local changes in your DMS configuration.")
|
||||||
|
if offerReclone(dmsPath) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errdefs.ErrUpdateCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Updating to %s...\n", latestTag)
|
||||||
|
checkoutCmd := exec.Command("git", "checkout", latestTag)
|
||||||
|
checkoutCmd.Stdout = os.Stdout
|
||||||
|
checkoutCmd.Stderr = os.Stderr
|
||||||
|
if err := checkoutCmd.Run(); err != nil {
|
||||||
|
fmt.Printf("Error: Failed to checkout %s: %v\n", latestTag, err)
|
||||||
|
if offerReclone(dmsPath) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("update cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentBranch == "" {
|
||||||
|
currentBranch = "master"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Current branch: %s\n", currentBranch)
|
||||||
|
|
||||||
|
if hasLocalChanges {
|
||||||
|
fmt.Println("\nWarning: You have local changes in your DMS configuration.")
|
||||||
|
if offerReclone(dmsPath) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errdefs.ErrUpdateCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
pullCmd := exec.Command("git", "pull", "origin", currentBranch)
|
||||||
|
pullCmd.Stdout = os.Stdout
|
||||||
|
pullCmd.Stderr = os.Stderr
|
||||||
|
if err := pullCmd.Run(); err != nil {
|
||||||
|
fmt.Printf("Error: Failed to pull latest changes: %v\n", err)
|
||||||
|
if offerReclone(dmsPath) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("update cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nUpdate complete!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func offerReclone(dmsPath string) bool {
|
||||||
|
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
response, err := reader.ReadString('\n')
|
||||||
|
if err != nil || !strings.HasPrefix(strings.ToLower(strings.TrimSpace(response)), "y") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
backupPath := fmt.Sprintf("%s.backup-%d", dmsPath, timestamp)
|
||||||
|
|
||||||
|
fmt.Printf("Backing up current directory to %s...\n", backupPath)
|
||||||
|
if err := os.Rename(dmsPath, backupPath); err != nil {
|
||||||
|
fmt.Printf("Error: Failed to backup directory: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Cloning fresh copy...")
|
||||||
|
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||||
|
cloneCmd.Stdout = os.Stdout
|
||||||
|
cloneCmd.Stderr = os.Stderr
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
fmt.Printf("Error: Failed to clone repository: %v\n", err)
|
||||||
|
fmt.Printf("Restoring backup...\n")
|
||||||
|
os.Rename(backupPath, dmsPath)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully re-cloned repository (backup at %s)\n", backupPath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmUpdate() bool {
|
||||||
|
fmt.Print("Do you want to proceed with the update? (y/N): ")
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
response, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading input: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
response = strings.TrimSpace(strings.ToLower(response))
|
||||||
|
return response == "y" || response == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDMSBinary() error {
|
||||||
|
arch := ""
|
||||||
|
switch strings.ToLower(os.Getenv("HOSTTYPE")) {
|
||||||
|
case "x86_64", "amd64":
|
||||||
|
arch = "amd64"
|
||||||
|
case "aarch64", "arm64":
|
||||||
|
arch = "arm64"
|
||||||
|
default:
|
||||||
|
cmd := exec.Command("uname", "-m")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to detect architecture: %w", err)
|
||||||
|
}
|
||||||
|
archStr := strings.TrimSpace(string(output))
|
||||||
|
switch archStr {
|
||||||
|
case "x86_64":
|
||||||
|
arch = "amd64"
|
||||||
|
case "aarch64":
|
||||||
|
arch = "arm64"
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported architecture: %s", archStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Fetching latest release version...")
|
||||||
|
cmd := exec.Command("curl", "-s", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch latest release: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
version := ""
|
||||||
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
|
if strings.Contains(line, "\"tag_name\"") {
|
||||||
|
parts := strings.Split(line, "\"")
|
||||||
|
if len(parts) >= 4 {
|
||||||
|
version = parts[3]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
return fmt.Errorf("could not determine latest version")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Latest version: %s\n", version)
|
||||||
|
|
||||||
|
tempDir, err := os.MkdirTemp("", "dms-update-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
binaryURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch)
|
||||||
|
checksumURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz.sha256", version, arch)
|
||||||
|
|
||||||
|
binaryPath := filepath.Join(tempDir, "dms.gz")
|
||||||
|
checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
|
||||||
|
|
||||||
|
fmt.Println("Downloading dms binary...")
|
||||||
|
downloadCmd := exec.Command("curl", "-L", binaryURL, "-o", binaryPath)
|
||||||
|
if err := downloadCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to download binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Downloading checksum...")
|
||||||
|
downloadCmd = exec.Command("curl", "-L", checksumURL, "-o", checksumPath)
|
||||||
|
if err := downloadCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to download checksum: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Verifying checksum...")
|
||||||
|
checksumData, err := os.ReadFile(checksumPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read checksum file: %w", err)
|
||||||
|
}
|
||||||
|
expectedChecksum := strings.Fields(string(checksumData))[0]
|
||||||
|
|
||||||
|
actualCmd := exec.Command("sha256sum", binaryPath)
|
||||||
|
actualOutput, err := actualCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to calculate checksum: %w", err)
|
||||||
|
}
|
||||||
|
actualChecksum := strings.Fields(string(actualOutput))[0]
|
||||||
|
|
||||||
|
if expectedChecksum != actualChecksum {
|
||||||
|
return fmt.Errorf("checksum verification failed\nExpected: %s\nGot: %s", expectedChecksum, actualChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Decompressing binary...")
|
||||||
|
decompressCmd := exec.Command("gunzip", binaryPath)
|
||||||
|
if err := decompressCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to decompress binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decompressedPath := filepath.Join(tempDir, "dms")
|
||||||
|
|
||||||
|
if err := os.Chmod(decompressedPath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPath, err := exec.LookPath("dms")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not find current dms binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Installing to %s...\n", currentPath)
|
||||||
|
|
||||||
|
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
|
||||||
|
replaceCmd.Stdin = os.Stdin
|
||||||
|
replaceCmd.Stdout = os.Stdout
|
||||||
|
replaceCmd.Stderr = os.Stderr
|
||||||
|
if err := replaceCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to replace binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var greeterCmd = &cobra.Command{
|
||||||
|
Use: "greeter",
|
||||||
|
Short: "Manage DMS greeter",
|
||||||
|
Long: "Manage DMS greeter (greetd)",
|
||||||
|
}
|
||||||
|
|
||||||
|
var greeterInstallCmd = &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Short: "Install and configure DMS greeter",
|
||||||
|
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := installGreeter(); err != nil {
|
||||||
|
log.Fatalf("Error installing greeter: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var greeterSyncCmd = &cobra.Command{
|
||||||
|
Use: "sync",
|
||||||
|
Short: "Sync DMS theme and settings with greeter",
|
||||||
|
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := syncGreeter(); err != nil {
|
||||||
|
log.Fatalf("Error syncing greeter: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var greeterEnableCmd = &cobra.Command{
|
||||||
|
Use: "enable",
|
||||||
|
Short: "Enable DMS greeter in greetd config",
|
||||||
|
Long: "Configure greetd to use DMS as the greeter",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := enableGreeter(); err != nil {
|
||||||
|
log.Fatalf("Error enabling greeter: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var greeterStatusCmd = &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "Check greeter sync status",
|
||||||
|
Long: "Check the status of greeter installation and configuration sync",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := checkGreeterStatus(); err != nil {
|
||||||
|
log.Fatalf("Error checking greeter status: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func installGreeter() error {
|
||||||
|
fmt.Println("=== DMS Greeter Installation ===")
|
||||||
|
|
||||||
|
logFunc := func(msg string) {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := greeter.EnsureGreetdInstalled(logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nDetecting DMS installation...")
|
||||||
|
dmsPath, err := greeter.DetectDMSPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||||
|
|
||||||
|
fmt.Println("\nDetecting installed compositors...")
|
||||||
|
compositors := greeter.DetectCompositors()
|
||||||
|
if len(compositors) == 0 {
|
||||||
|
return fmt.Errorf("no supported compositors found (niri or Hyprland required)")
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedCompositor string
|
||||||
|
if len(compositors) == 1 {
|
||||||
|
selectedCompositor = compositors[0]
|
||||||
|
fmt.Printf("✓ Found compositor: %s\n", selectedCompositor)
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
selectedCompositor, err = greeter.PromptCompositorChoice(compositors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nSetting up dms-greeter group and permissions...")
|
||||||
|
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nCopying greeter files...")
|
||||||
|
if err := greeter.CopyGreeterFiles(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nConfiguring greetd...")
|
||||||
|
if err := greeter.ConfigureGreetd(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
|
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Installation Complete ===")
|
||||||
|
fmt.Println("\nTo test the greeter, run:")
|
||||||
|
fmt.Println(" sudo systemctl start greetd")
|
||||||
|
fmt.Println("\nTo enable on boot, run:")
|
||||||
|
fmt.Println(" sudo systemctl enable --now greetd")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGreeter() error {
|
||||||
|
fmt.Println("=== DMS Greeter Theme Sync ===")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
logFunc := func(msg string) {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Detecting DMS installation...")
|
||||||
|
dmsPath, err := greeter.DetectDMSPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||||
|
|
||||||
|
cacheDir := "/var/cache/dms-greeter"
|
||||||
|
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
greeterGroupExists := checkGroupExists("greeter")
|
||||||
|
if greeterGroupExists {
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get current user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupsCmd := exec.Command("groups", currentUser.Username)
|
||||||
|
groupsOutput, err := groupsCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check groups: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
||||||
|
if !inGreeterGroup {
|
||||||
|
fmt.Println("\n⚠ Warning: You are not in the greeter group.")
|
||||||
|
fmt.Print("Would you like to add your user to the greeter group? (y/N): ")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
|
||||||
|
if response == "y" || response == "yes" {
|
||||||
|
fmt.Println("\nAdding user to greeter group...")
|
||||||
|
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username)
|
||||||
|
addUserCmd.Stdout = os.Stdout
|
||||||
|
addUserCmd.Stderr = os.Stderr
|
||||||
|
if err := addUserCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to add user to greeter group: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("✓ User added to greeter group")
|
||||||
|
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nSetting up permissions and ACLs...")
|
||||||
|
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
|
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Sync Complete ===")
|
||||||
|
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
|
||||||
|
fmt.Println("The changes will be visible on the next login screen.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkGroupExists(groupName string) bool {
|
||||||
|
data, err := os.ReadFile("/etc/group")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, groupName+":") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableGreeter() error {
|
||||||
|
fmt.Println("=== DMS Greeter Enable ===")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
configPath := "/etc/greetd/config.toml"
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read greetd config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configContent := string(data)
|
||||||
|
if strings.Contains(configContent, "dms-greeter") {
|
||||||
|
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Detecting installed compositors...")
|
||||||
|
compositors := greeter.DetectCompositors()
|
||||||
|
|
||||||
|
if commandExists("sway") {
|
||||||
|
compositors = append(compositors, "sway")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(compositors) == 0 {
|
||||||
|
return fmt.Errorf("no supported compositors found (niri, Hyprland, or sway required)")
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedCompositor string
|
||||||
|
if len(compositors) == 1 {
|
||||||
|
selectedCompositor = compositors[0]
|
||||||
|
fmt.Printf("✓ Found compositor: %s\n", selectedCompositor)
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
selectedCompositor, err = promptCompositorChoice(compositors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupPath := configPath + ".backup"
|
||||||
|
backupCmd := exec.Command("sudo", "cp", configPath, backupPath)
|
||||||
|
if err := backupCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to backup config: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Backed up config to %s\n", backupPath)
|
||||||
|
|
||||||
|
lines := strings.Split(configContent, "\n")
|
||||||
|
var newLines []string
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
||||||
|
newLines = append(newLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapperCmd, err := findCommandPath("dms-greeter")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dms-greeter not found in PATH. Please ensure it is installed and accessible")
|
||||||
|
}
|
||||||
|
|
||||||
|
compositorLower := strings.ToLower(selectedCompositor)
|
||||||
|
commandLine := fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
|
||||||
|
|
||||||
|
var finalLines []string
|
||||||
|
inDefaultSession := false
|
||||||
|
commandAdded := false
|
||||||
|
|
||||||
|
for _, line := range newLines {
|
||||||
|
finalLines = append(finalLines, line)
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if trimmed == "[default_session]" {
|
||||||
|
inDefaultSession = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if inDefaultSession && !commandAdded {
|
||||||
|
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
||||||
|
finalLines = append(finalLines, commandLine)
|
||||||
|
commandAdded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !commandAdded {
|
||||||
|
finalLines = append(finalLines, commandLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := strings.Join(finalLines, "\n")
|
||||||
|
|
||||||
|
tmpFile := "/tmp/greetd-config.toml"
|
||||||
|
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write temp config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
moveCmd := exec.Command("sudo", "mv", tmpFile, configPath)
|
||||||
|
if err := moveCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
|
||||||
|
fmt.Println("\n=== Enable Complete ===")
|
||||||
|
fmt.Println("\nTo start the greeter, run:")
|
||||||
|
fmt.Println(" sudo systemctl start greetd")
|
||||||
|
fmt.Println("\nTo enable on boot, run:")
|
||||||
|
fmt.Println(" sudo systemctl enable --now greetd")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptCompositorChoice(compositors []string) (string, error) {
|
||||||
|
fmt.Println("\nMultiple compositors detected:")
|
||||||
|
for i, comp := range compositors {
|
||||||
|
fmt.Printf("%d) %s\n", i+1, comp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Print("Choose compositor for greeter: ")
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.TrimSpace(response)
|
||||||
|
|
||||||
|
choice := 0
|
||||||
|
fmt.Sscanf(response, "%d", &choice)
|
||||||
|
|
||||||
|
if choice < 1 || choice > len(compositors) {
|
||||||
|
return "", fmt.Errorf("invalid choice")
|
||||||
|
}
|
||||||
|
|
||||||
|
return compositors[choice-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkGreeterStatus() error {
|
||||||
|
fmt.Println("=== DMS Greeter Status ===")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get current user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := "/etc/greetd/config.toml"
|
||||||
|
fmt.Println("Greeter Configuration:")
|
||||||
|
if data, err := os.ReadFile(configPath); err == nil {
|
||||||
|
configContent := string(data)
|
||||||
|
if strings.Contains(configContent, "dms-greeter") {
|
||||||
|
lines := strings.Split(configContent, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||||||
|
fmt.Println(" ✓ Greeter is enabled")
|
||||||
|
|
||||||
|
if strings.Contains(command, "--command niri") {
|
||||||
|
fmt.Println(" Compositor: niri")
|
||||||
|
} else if strings.Contains(command, "--command hyprland") {
|
||||||
|
fmt.Println(" Compositor: Hyprland")
|
||||||
|
} else if strings.Contains(command, "--command sway") {
|
||||||
|
fmt.Println(" Compositor: sway")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" Compositor: unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(" ✗ Greeter is NOT enabled")
|
||||||
|
fmt.Println(" Run 'dms greeter enable' to enable it")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(" ✗ Greeter config not found")
|
||||||
|
fmt.Println(" Run 'dms greeter install' to install greeter")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nGroup Membership:")
|
||||||
|
groupsCmd := exec.Command("groups", currentUser.Username)
|
||||||
|
groupsOutput, err := groupsCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check groups: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
||||||
|
if inGreeterGroup {
|
||||||
|
fmt.Println(" ✓ User is in greeter group")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" ✗ User is NOT in greeter group")
|
||||||
|
fmt.Println(" Run 'dms greeter install' to add user to greeter group")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := "/var/cache/dms-greeter"
|
||||||
|
fmt.Println("\nGreeter Cache Directory:")
|
||||||
|
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
||||||
|
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
||||||
|
fmt.Println(" Run 'dms greeter install' to create cache directory")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nConfiguration Symlinks:")
|
||||||
|
symlinks := []struct {
|
||||||
|
source string
|
||||||
|
target string
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"),
|
||||||
|
target: filepath.Join(cacheDir, "settings.json"),
|
||||||
|
desc: "Settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"),
|
||||||
|
target: filepath.Join(cacheDir, "session.json"),
|
||||||
|
desc: "Session state",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
target: filepath.Join(cacheDir, "colors.json"),
|
||||||
|
desc: "Color theme",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
allGood := true
|
||||||
|
for _, link := range symlinks {
|
||||||
|
targetInfo, err := os.Lstat(link.target)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" ✗ %s: symlink not found at %s\n", link.desc, link.target)
|
||||||
|
allGood = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetInfo.Mode()&os.ModeSymlink == 0 {
|
||||||
|
fmt.Printf(" ✗ %s: %s is not a symlink\n", link.desc, link.target)
|
||||||
|
allGood = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
linkDest, err := os.Readlink(link.target)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" ✗ %s: failed to read symlink\n", link.desc)
|
||||||
|
allGood = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if linkDest != link.source {
|
||||||
|
fmt.Printf(" ✗ %s: symlink points to wrong location\n", link.desc)
|
||||||
|
fmt.Printf(" Expected: %s\n", link.source)
|
||||||
|
fmt.Printf(" Got: %s\n", linkDest)
|
||||||
|
allGood = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
||||||
|
fmt.Printf(" ⚠ %s: symlink OK, but source file doesn't exist yet\n", link.desc)
|
||||||
|
fmt.Printf(" Will be created when you run DMS\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if allGood && inGreeterGroup {
|
||||||
|
fmt.Println("✓ All checks passed! Greeter is properly configured.")
|
||||||
|
} else if !allGood {
|
||||||
|
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to fix symlinks.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var keybindsCmd = &cobra.Command{
|
||||||
|
Use: "keybinds",
|
||||||
|
Aliases: []string{"cheatsheet", "chsht"},
|
||||||
|
Short: "Manage keybinds and cheatsheets",
|
||||||
|
Long: "Display and manage keybinds and cheatsheets for various applications",
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List available providers",
|
||||||
|
Long: "List all available keybind/cheatsheet providers",
|
||||||
|
Run: runKeybindsList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsShowCmd = &cobra.Command{
|
||||||
|
Use: "show <provider>",
|
||||||
|
Short: "Show keybinds for a provider",
|
||||||
|
Long: "Display keybinds/cheatsheet for the specified provider",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: runKeybindsShow,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||||
|
|
||||||
|
keybindsCmd.AddCommand(keybindsListCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
|
|
||||||
|
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
||||||
|
return providers.NewJSONFileProvider(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
initializeProviders()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeProviders() {
|
||||||
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
|
||||||
|
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
|
||||||
|
if err := registry.Register(hyprlandProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Hyprland provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
|
||||||
|
if err := registry.Register(mangowcProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register MangoWC provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
|
||||||
|
if err := registry.Register(swayProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Sway 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, args []string) {
|
||||||
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
providers := registry.List()
|
||||||
|
|
||||||
|
if len(providers) == 0 {
|
||||||
|
fmt.Fprintln(os.Stdout, "No providers available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stdout, "Available providers:")
|
||||||
|
for _, name := range providers {
|
||||||
|
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsShow(cmd *cobra.Command, args []string) {
|
||||||
|
providerName := args[0]
|
||||||
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
|
||||||
|
customPath, _ := cmd.Flags().GetString("path")
|
||||||
|
if customPath != "" {
|
||||||
|
var provider keybinds.Provider
|
||||||
|
switch providerName {
|
||||||
|
case "hyprland":
|
||||||
|
provider = providers.NewHyprlandProvider(customPath)
|
||||||
|
case "mangowc":
|
||||||
|
provider = providers.NewMangoWCProvider(customPath)
|
||||||
|
case "sway":
|
||||||
|
provider = providers.NewSwayProvider(customPath)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Provider %s does not support custom path", providerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet, err := provider.GetCheatSheet()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting cheatsheet: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := json.MarshalIndent(sheet, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error generating JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := registry.Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var customConfigPath string
|
||||||
|
var configPath string
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "dms",
|
||||||
|
Short: "dms CLI",
|
||||||
|
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
||||||
|
Run: runInteractiveMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Add the -c flag
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func findConfig(cmd *cobra.Command, args []string) error {
|
||||||
|
if customConfigPath != "" {
|
||||||
|
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
|
||||||
|
shellPath := filepath.Join(customConfigPath, "shell.qml")
|
||||||
|
|
||||||
|
info, statErr := os.Stat(shellPath)
|
||||||
|
|
||||||
|
if statErr == nil && !info.IsDir() {
|
||||||
|
configPath = customConfigPath
|
||||||
|
log.Debug("Using config from: %s", configPath)
|
||||||
|
return nil // <-- Guard statement
|
||||||
|
}
|
||||||
|
|
||||||
|
if statErr != nil {
|
||||||
|
return fmt.Errorf("custom config path error: %w", statErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
|
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
||||||
|
statePath := strings.TrimSpace(string(data))
|
||||||
|
shellPath := filepath.Join(statePath, "shell.qml")
|
||||||
|
|
||||||
|
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
||||||
|
log.Debug("Using config from active session state file: %s", statePath)
|
||||||
|
configPath = statePath
|
||||||
|
log.Debug("Using config from: %s", configPath)
|
||||||
|
return nil // <-- Guard statement
|
||||||
|
} else {
|
||||||
|
os.Remove(configStateFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("No custom path or active session, searching default XDG locations...")
|
||||||
|
var err error
|
||||||
|
configPath, err = config.LocateDMSConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Using config from: %s", configPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
||||||
|
detector, err := dms.NewDetector()
|
||||||
|
if err != nil && !errors.Is(err, &distros.UnsupportedDistributionError{}) {
|
||||||
|
log.Fatalf("Error initializing DMS detector: %v", err)
|
||||||
|
} else if errors.Is(err, &distros.UnsupportedDistributionError{}) {
|
||||||
|
log.Error("Interactive mode is not supported on this distribution.")
|
||||||
|
log.Info("Please run 'dms --help' for available commands.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !detector.IsDMSInstalled() {
|
||||||
|
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
||||||
|
log.Info("Please install DMS using dankinstall before using this management interface.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
model := dms.NewModel(Version)
|
||||||
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
log.Fatalf("Error running program: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var setupCmd = &cobra.Command{
|
||||||
|
Use: "setup",
|
||||||
|
Short: "Deploy DMS configurations",
|
||||||
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runSetup(); err != nil {
|
||||||
|
log.Fatalf("Error during setup: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSetup() error {
|
||||||
|
fmt.Println("=== DMS Configuration Setup ===")
|
||||||
|
|
||||||
|
wm, wmSelected := promptCompositor()
|
||||||
|
terminal, terminalSelected := promptTerminal()
|
||||||
|
|
||||||
|
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.DeployConfigurationsWithTerminal(ctx, wm, terminal)
|
||||||
|
} else if wmSelected {
|
||||||
|
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
if len(results) > 1 {
|
||||||
|
results = results[:1]
|
||||||
|
}
|
||||||
|
} else if terminalSelected {
|
||||||
|
results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal)
|
||||||
|
if len(results) > 0 && results[0].ConfigType == "Niri" {
|
||||||
|
results = results[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(logChan)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("deployment failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Deployment Complete ===")
|
||||||
|
for _, result := range results {
|
||||||
|
if result.Deployed {
|
||||||
|
fmt.Printf("✓ %s: %s\n", result.ConfigType, result.Path)
|
||||||
|
if result.BackupPath != "" {
|
||||||
|
fmt.Printf(" Backup: %s\n", result.BackupPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptCompositor() (deps.WindowManager, bool) {
|
||||||
|
fmt.Println("Select compositor:")
|
||||||
|
fmt.Println("1) Niri")
|
||||||
|
fmt.Println("2) Hyprland")
|
||||||
|
fmt.Println("3) None")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Print("\nChoice (1-3): ")
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.TrimSpace(response)
|
||||||
|
|
||||||
|
switch response {
|
||||||
|
case "1":
|
||||||
|
return deps.WindowManagerNiri, true
|
||||||
|
case "2":
|
||||||
|
return deps.WindowManagerHyprland, true
|
||||||
|
default:
|
||||||
|
return deps.WindowManagerNiri, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptTerminal() (deps.Terminal, bool) {
|
||||||
|
fmt.Println("\nSelect terminal:")
|
||||||
|
fmt.Println("1) Ghostty")
|
||||||
|
fmt.Println("2) Kitty")
|
||||||
|
fmt.Println("3) Alacritty")
|
||||||
|
fmt.Println("4) None")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Print("\nChoice (1-4): ")
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.TrimSpace(response)
|
||||||
|
|
||||||
|
switch response {
|
||||||
|
case "1":
|
||||||
|
return deps.TerminalGhostty, true
|
||||||
|
case "2":
|
||||||
|
return deps.TerminalKitty, true
|
||||||
|
case "3":
|
||||||
|
return deps.TerminalAlacritty, true
|
||||||
|
default:
|
||||||
|
return deps.TerminalGhostty, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power"
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cmd struct {
|
||||||
|
fn func()
|
||||||
|
done chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
type dpmsClient struct {
|
||||||
|
display *wlclient.Display
|
||||||
|
ctx *wlclient.Context
|
||||||
|
powerMgr *wlr_output_power.ZwlrOutputPowerManagerV1
|
||||||
|
outputs map[string]*outputState
|
||||||
|
mu sync.Mutex
|
||||||
|
syncRound int
|
||||||
|
done bool
|
||||||
|
err error
|
||||||
|
cmdq chan cmd
|
||||||
|
stopChan chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
type outputState struct {
|
||||||
|
wlOutput *wlclient.Output
|
||||||
|
powerCtrl *wlr_output_power.ZwlrOutputPowerV1
|
||||||
|
name string
|
||||||
|
mode uint32
|
||||||
|
failed bool
|
||||||
|
waitCh chan struct{}
|
||||||
|
wantMode *uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dpmsClient) post(fn func()) {
|
||||||
|
done := make(chan error, 1)
|
||||||
|
select {
|
||||||
|
case c.cmdq <- cmd{fn: fn, done: done}:
|
||||||
|
<-done
|
||||||
|
case <-c.stopChan:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dpmsClient) waylandActor() {
|
||||||
|
defer c.wg.Done()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.stopChan:
|
||||||
|
return
|
||||||
|
case cmd := <-c.cmdq:
|
||||||
|
cmd.fn()
|
||||||
|
close(cmd.done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDPMSClient() (*dpmsClient, error) {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &dpmsClient{
|
||||||
|
display: display,
|
||||||
|
ctx: display.Context(),
|
||||||
|
outputs: make(map[string]*outputState),
|
||||||
|
cmdq: make(chan cmd, 128),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.wg.Add(1)
|
||||||
|
go c.waylandActor()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
display.Context().Close()
|
||||||
|
return nil, fmt.Errorf("failed to get registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
|
||||||
|
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 1 {
|
||||||
|
version = 1
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
|
||||||
|
c.powerMgr = powerMgr
|
||||||
|
}
|
||||||
|
|
||||||
|
case "wl_output":
|
||||||
|
output := wlclient.NewOutput(c.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
|
outputID := fmt.Sprintf("output-%d", output.ID())
|
||||||
|
state := &outputState{
|
||||||
|
wlOutput: output,
|
||||||
|
name: outputID,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
c.outputs[outputID] = state
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
|
||||||
|
c.mu.Lock()
|
||||||
|
delete(c.outputs, state.name)
|
||||||
|
state.name = ev.Name
|
||||||
|
c.outputs[ev.Name] = state
|
||||||
|
c.mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
syncCallback, err := display.Sync()
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, fmt.Errorf("failed to sync display: %w", err)
|
||||||
|
}
|
||||||
|
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
||||||
|
c.handleSync()
|
||||||
|
})
|
||||||
|
|
||||||
|
for !c.done {
|
||||||
|
if err := c.ctx.Dispatch(); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, fmt.Errorf("dispatch error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dpmsClient) handleSync() {
|
||||||
|
c.syncRound++
|
||||||
|
|
||||||
|
switch c.syncRound {
|
||||||
|
case 1:
|
||||||
|
if c.powerMgr == nil {
|
||||||
|
c.err = fmt.Errorf("wlr-output-power-management protocol not supported by compositor")
|
||||||
|
c.done = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
for _, state := range c.outputs {
|
||||||
|
powerCtrl, err := c.powerMgr.GetOutputPower(state.wlOutput)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state.powerCtrl = powerCtrl
|
||||||
|
|
||||||
|
powerCtrl.SetModeHandler(func(e wlr_output_power.ZwlrOutputPowerV1ModeEvent) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if state.powerCtrl == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.mode = e.Mode
|
||||||
|
if state.wantMode != nil && e.Mode == *state.wantMode && state.waitCh != nil {
|
||||||
|
close(state.waitCh)
|
||||||
|
state.wantMode = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
powerCtrl.SetFailedHandler(func(e wlr_output_power.ZwlrOutputPowerV1FailedEvent) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if state.powerCtrl == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.failed = true
|
||||||
|
if state.waitCh != nil {
|
||||||
|
close(state.waitCh)
|
||||||
|
state.wantMode = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
syncCallback, err := c.display.Sync()
|
||||||
|
if err != nil {
|
||||||
|
c.err = fmt.Errorf("failed to sync display: %w", err)
|
||||||
|
c.done = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
||||||
|
c.handleSync()
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
c.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dpmsClient) ListOutputs() []string {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
names := make([]string, 0, len(c.outputs))
|
||||||
|
for name := range c.outputs {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dpmsClient) SetDPMS(outputName string, on bool) error {
|
||||||
|
var mode uint32
|
||||||
|
if on {
|
||||||
|
mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOn)
|
||||||
|
} else {
|
||||||
|
mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOff)
|
||||||
|
}
|
||||||
|
|
||||||
|
var setErr error
|
||||||
|
c.post(func() {
|
||||||
|
c.mu.Lock()
|
||||||
|
var waitStates []*outputState
|
||||||
|
|
||||||
|
if outputName == "" || outputName == "all" {
|
||||||
|
if len(c.outputs) == 0 {
|
||||||
|
c.mu.Unlock()
|
||||||
|
setErr = fmt.Errorf("no outputs found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range c.outputs {
|
||||||
|
if state.powerCtrl == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state.wantMode = &mode
|
||||||
|
state.waitCh = make(chan struct{})
|
||||||
|
state.failed = false
|
||||||
|
waitStates = append(waitStates, state)
|
||||||
|
state.powerCtrl.SetMode(mode)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state, ok := c.outputs[outputName]
|
||||||
|
if !ok {
|
||||||
|
c.mu.Unlock()
|
||||||
|
setErr = fmt.Errorf("output not found: %s", outputName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.powerCtrl == nil {
|
||||||
|
c.mu.Unlock()
|
||||||
|
setErr = fmt.Errorf("output %s has nil powerCtrl", outputName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.wantMode = &mode
|
||||||
|
state.waitCh = make(chan struct{})
|
||||||
|
state.failed = false
|
||||||
|
waitStates = append(waitStates, state)
|
||||||
|
state.powerCtrl.SetMode(mode)
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
|
||||||
|
for _, state := range waitStates {
|
||||||
|
c.mu.Lock()
|
||||||
|
ch := state.waitCh
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
done := false
|
||||||
|
for !done {
|
||||||
|
if err := c.ctx.Dispatch(); err != nil {
|
||||||
|
setErr = fmt.Errorf("dispatch error: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
c.mu.Lock()
|
||||||
|
if state.failed {
|
||||||
|
setErr = fmt.Errorf("compositor reported failed for %s", state.name)
|
||||||
|
c.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
done = true
|
||||||
|
default:
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
setErr = fmt.Errorf("timeout waiting for mode change on %s", state.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
for _, state := range waitStates {
|
||||||
|
if state.powerCtrl != nil {
|
||||||
|
state.powerCtrl.Destroy()
|
||||||
|
state.powerCtrl = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
c.display.Roundtrip()
|
||||||
|
})
|
||||||
|
|
||||||
|
return setErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dpmsClient) Close() {
|
||||||
|
close(c.stopChan)
|
||||||
|
c.wg.Wait()
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, state := range c.outputs {
|
||||||
|
if state.powerCtrl != nil {
|
||||||
|
state.powerCtrl.Destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.outputs = nil
|
||||||
|
|
||||||
|
if c.powerMgr != nil {
|
||||||
|
c.powerMgr.Destroy()
|
||||||
|
c.powerMgr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.display != nil {
|
||||||
|
c.ctx.Close()
|
||||||
|
c.display = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//go:build !distro_binary
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
|
// Add subcommands to greeter
|
||||||
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
|
// Add subcommands to update
|
||||||
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
|
// Add subcommands to plugins
|
||||||
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
|
||||||
|
|
||||||
|
// Add common commands to root
|
||||||
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|
||||||
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build distro_binary
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Add flags
|
||||||
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
|
// Add subcommands to greeter
|
||||||
|
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
|
// Add subcommands to plugins
|
||||||
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
|
||||||
|
|
||||||
|
// Add common commands to root
|
||||||
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Block root
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the child process is still alive
|
||||||
|
proc, err := os.FindProcess(childPID)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(pidFile)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||||
|
// Process is dead, remove stale PID file
|
||||||
|
os.Remove(pidFile)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pids = append(pids, childPID)
|
||||||
|
|
||||||
|
// Also get the parent PID from the filename
|
||||||
|
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
|
||||||
|
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
|
||||||
|
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
|
||||||
|
// Check if parent is still alive
|
||||||
|
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), 0644); 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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := server.Start(false); err != nil {
|
||||||
|
errChan <- fmt.Errorf("server error: %w", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Infof("Spawning quickshell with -p %s", configPath)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||||
|
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||||
|
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSessionManaged && hasSystemdRun() {
|
||||||
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
||||||
|
if !strings.HasPrefix(configPath, homeDir) {
|
||||||
|
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Fatalf("Error starting quickshell: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write PID file for the quickshell child process
|
||||||
|
if err := writePIDFile(cmd.Process.Pid); err != nil {
|
||||||
|
log.Warnf("Failed to write PID file: %v", err)
|
||||||
|
}
|
||||||
|
defer removePIDFile()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if cmd.Process != nil {
|
||||||
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
errChan <- fmt.Errorf("quickshell exited: %w", err)
|
||||||
|
} else {
|
||||||
|
errChan <- fmt.Errorf("quickshell exited")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sig := <-sigChan:
|
||||||
|
// Handle SIGUSR1 restart for non-session managed processes
|
||||||
|
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
||||||
|
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||||
|
execDetachedRestart(os.Getpid())
|
||||||
|
// Exit immediately to avoid race conditions with detached restart
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other signals: clean shutdown
|
||||||
|
log.Infof("\nReceived signal %v, shutting down...", sig)
|
||||||
|
cancel()
|
||||||
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
os.Remove(socketPath)
|
||||||
|
return
|
||||||
|
|
||||||
|
case err := <-errChan:
|
||||||
|
log.Error(err)
|
||||||
|
cancel()
|
||||||
|
if cmd.Process != nil {
|
||||||
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
}
|
||||||
|
os.Remove(socketPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartShell() {
|
||||||
|
pids := getAllDMSPIDs()
|
||||||
|
|
||||||
|
if len(pids) == 0 {
|
||||||
|
log.Info("No running DMS shell instances found. Starting daemon...")
|
||||||
|
runShellDaemon(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPid := os.Getpid()
|
||||||
|
uniquePids := make(map[int]bool)
|
||||||
|
|
||||||
|
for _, pid := range pids {
|
||||||
|
if pid != currentPid {
|
||||||
|
uniquePids[pid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for pid := range uniquePids {
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error finding process %d: %v", pid, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proc.Signal(syscall.SIGUSR1); err != nil {
|
||||||
|
log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
|
||||||
|
} else {
|
||||||
|
log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func killShell() {
|
||||||
|
// Get all tracked DMS PIDs from PID files
|
||||||
|
pids := getAllDMSPIDs()
|
||||||
|
|
||||||
|
if len(pids) == 0 {
|
||||||
|
log.Info("No running DMS shell instances found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPid := os.Getpid()
|
||||||
|
uniquePids := make(map[int]bool)
|
||||||
|
|
||||||
|
// Deduplicate and filter out current process
|
||||||
|
for _, pid := range pids {
|
||||||
|
if pid != currentPid {
|
||||||
|
uniquePids[pid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill all tracked processes
|
||||||
|
for pid := range uniquePids {
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error finding process %d: %v", pid, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if process is still alive before killing
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any remaining PID files
|
||||||
|
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
|
||||||
|
// Check if this is the daemon child process by looking for the hidden flag
|
||||||
|
isDaemonChild := false
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if arg == "--daemon-child" {
|
||||||
|
isDaemonChild = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDaemonChild {
|
||||||
|
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||||
|
|
||||||
|
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setsid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Fatalf("Error starting daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
socketPath := server.GetSocketPath()
|
||||||
|
|
||||||
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
|
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); 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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := server.Start(false); err != nil {
|
||||||
|
errChan <- fmt.Errorf("server error: %w", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Infof("Spawning quickshell with -p %s", configPath)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||||
|
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||||
|
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSessionManaged && hasSystemdRun() {
|
||||||
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
||||||
|
if !strings.HasPrefix(configPath, homeDir) {
|
||||||
|
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error opening /dev/null: %v", err)
|
||||||
|
}
|
||||||
|
defer devNull.Close()
|
||||||
|
|
||||||
|
cmd.Stdin = devNull
|
||||||
|
cmd.Stdout = devNull
|
||||||
|
cmd.Stderr = devNull
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Fatalf("Error starting daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write PID file for the quickshell child process
|
||||||
|
if err := writePIDFile(cmd.Process.Pid); err != nil {
|
||||||
|
log.Warnf("Failed to write PID file: %v", err)
|
||||||
|
}
|
||||||
|
defer removePIDFile()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if cmd.Process != nil {
|
||||||
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
errChan <- fmt.Errorf("quickshell exited: %w", err)
|
||||||
|
} else {
|
||||||
|
errChan <- fmt.Errorf("quickshell exited")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sig := <-sigChan:
|
||||||
|
// Handle SIGUSR1 restart for non-session managed processes
|
||||||
|
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
||||||
|
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||||
|
execDetachedRestart(os.Getpid())
|
||||||
|
// Exit immediately to avoid race conditions with detached restart
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other signals: clean shutdown
|
||||||
|
cancel()
|
||||||
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
os.Remove(socketPath)
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-errChan:
|
||||||
|
cancel()
|
||||||
|
if cmd.Process != nil {
|
||||||
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
}
|
||||||
|
os.Remove(socketPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runShellIPCCommand(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
log.Error("IPC command requires arguments")
|
||||||
|
log.Info("Usage: dms ipc <command> [args...]")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if args[0] != "call" {
|
||||||
|
args = append([]string{"call"}, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdArgs := append([]string{"-p", configPath, "ipc"}, args...)
|
||||||
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Fatalf("Error running IPC command: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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}}
|
||||||
|
`
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandExists(cmd string) bool {
|
||||||
|
_, err := exec.LookPath(cmd)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findCommandPath returns the absolute path to a command in PATH
|
||||||
|
func findCommandPath(cmd string) (string, error) {
|
||||||
|
path, err := exec.LookPath(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
|
go 1.24.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
github.com/charmbracelet/log v0.4.2
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||||
|
github.com/spf13/cobra v1.10.1
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.5.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.6.0 // 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-20251111123000-fb5ff8f3f0b0 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
|
golang.org/x/crypto v0.44.0 // indirect
|
||||||
|
golang.org/x/net v0.47.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.0 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sys v0.38.0
|
||||||
|
golang.org/x/text v0.31.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||||
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||||
|
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
|
||||||
|
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
|
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 h1:EC9n6hr6yKDoVJ6g7Ko523LbbceJfR0ohbOp809Fyf4=
|
||||||
|
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0/go.mod h1:E3VhlS+AKkrq6ZNn1axE2/nDRJ87l1FJk9r5HT2vPX0=
|
||||||
|
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
||||||
|
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
||||||
|
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9 h1:SOFrnF9LCssC6q6Rb0084Bzg2aBYbe8QXv9xKGXmt/w=
|
||||||
|
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9/go.mod h1:0wtvm/JfPC9RFVEAP3ks0ec5h64/qmZkTTUE3pjz7Hc=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||||
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||||
|
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/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/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||||
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Check for root privileges
|
||||||
|
if [ "$(id -u)" == "0" ]; then
|
||||||
|
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if running on Linux
|
||||||
|
if [ "$(uname)" != "Linux" ]; then
|
||||||
|
printf "%bError: This installer only supports Linux systems%b\n" "$RED" "$NC"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64)
|
||||||
|
ARCH="amd64"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
ARCH="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
|
||||||
|
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Get the latest release version
|
||||||
|
LATEST_VERSION=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
|
||||||
|
if [ -z "$LATEST_VERSION" ]; then
|
||||||
|
printf "%bError: Could not fetch latest version%b\n" "$RED" "$NC"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%bInstalling Dankinstall %s for %s...%b\n" "$GREEN" "$LATEST_VERSION" "$ARCH" "$NC"
|
||||||
|
|
||||||
|
# Download and install
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
cd "$TEMP_DIR" || exit 1
|
||||||
|
|
||||||
|
# Download the gzipped binary and its checksum
|
||||||
|
printf "%bDownloading installer...%b\n" "$GREEN" "$NC"
|
||||||
|
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz" -o "installer.gz"
|
||||||
|
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
|
||||||
|
|
||||||
|
# Get the expected checksum
|
||||||
|
EXPECTED_CHECKSUM=$(cat expected.sha256 | awk '{print $1}')
|
||||||
|
|
||||||
|
# Calculate actual checksum
|
||||||
|
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
|
||||||
|
ACTUAL_CHECKSUM=$(sha256sum installer.gz | awk '{print $1}')
|
||||||
|
|
||||||
|
# Compare checksums
|
||||||
|
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
|
||||||
|
printf "%bError: Checksum verification failed%b\n" "$RED" "$NC"
|
||||||
|
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
|
||||||
|
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
|
||||||
|
printf "The downloaded file may be corrupted or tampered with\n"
|
||||||
|
cd - > /dev/null
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Decompress the binary
|
||||||
|
printf "%bDecompressing installer...%b\n" "$GREEN" "$NC"
|
||||||
|
gunzip installer.gz
|
||||||
|
chmod +x installer
|
||||||
|
|
||||||
|
# Execute the installer
|
||||||
|
printf "%bRunning installer...%b\n" "$GREEN" "$NC"
|
||||||
|
./installer
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
cd - > /dev/null
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
@@ -0,0 +1,574 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// deployNiriConfig handles Niri configuration deployment with backup and merging
|
||||||
|
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
||||||
|
result := DeploymentResult{
|
||||||
|
ConfigType: "Niri",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Dir(result.Path)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingConfig string
|
||||||
|
if _, err := os.Stat(result.Path); err == nil {
|
||||||
|
cd.log("Found existing Niri configuration")
|
||||||
|
|
||||||
|
existingData, err := os.ReadFile(result.Path)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
existingConfig = string(existingData)
|
||||||
|
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
|
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect polkit agent path
|
||||||
|
polkitPath, err := cd.detectPolkitAgent()
|
||||||
|
if err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
||||||
|
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine terminal command based on choice
|
||||||
|
var terminalCommand string
|
||||||
|
switch terminal {
|
||||||
|
case deps.TerminalGhostty:
|
||||||
|
terminalCommand = "ghostty"
|
||||||
|
case deps.TerminalKitty:
|
||||||
|
terminalCommand = "kitty"
|
||||||
|
case deps.TerminalAlacritty:
|
||||||
|
terminalCommand = "alacritty"
|
||||||
|
default:
|
||||||
|
terminalCommand = "ghostty" // fallback to ghostty
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
||||||
|
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
|
|
||||||
|
// If there was an existing config, merge the output sections
|
||||||
|
if existingConfig != "" {
|
||||||
|
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
||||||
|
if err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
|
||||||
|
} else {
|
||||||
|
newConfig = mergedConfig
|
||||||
|
cd.log("Successfully merged existing output sections")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Deployed = true
|
||||||
|
cd.log("Successfully deployed Niri configuration")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||||
|
var results []DeploymentResult
|
||||||
|
|
||||||
|
mainResult := DeploymentResult{
|
||||||
|
ConfigType: "Ghostty",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||||
|
cd.log("Found existing Ghostty configuration")
|
||||||
|
|
||||||
|
existingData, err := os.ReadFile(mainResult.Path)
|
||||||
|
if err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
mainResult.Deployed = true
|
||||||
|
cd.log("Successfully deployed Ghostty configuration")
|
||||||
|
results = append(results, mainResult)
|
||||||
|
|
||||||
|
colorResult := DeploymentResult{
|
||||||
|
ConfigType: "Ghostty Colors",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
||||||
|
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
||||||
|
return results, colorResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
colorResult.Deployed = true
|
||||||
|
cd.log("Successfully deployed Ghostty color configuration")
|
||||||
|
results = append(results, colorResult)
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
||||||
|
var results []DeploymentResult
|
||||||
|
|
||||||
|
mainResult := DeploymentResult{
|
||||||
|
ConfigType: "Kitty",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||||
|
cd.log("Found existing Kitty configuration")
|
||||||
|
|
||||||
|
existingData, err := os.ReadFile(mainResult.Path)
|
||||||
|
if err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
mainResult.Deployed = true
|
||||||
|
cd.log("Successfully deployed Kitty configuration")
|
||||||
|
results = append(results, mainResult)
|
||||||
|
|
||||||
|
themeResult := DeploymentResult{
|
||||||
|
ConfigType: "Kitty Theme",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil {
|
||||||
|
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||||
|
return results, themeResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
themeResult.Deployed = true
|
||||||
|
cd.log("Successfully deployed Kitty theme configuration")
|
||||||
|
results = append(results, themeResult)
|
||||||
|
|
||||||
|
tabsResult := DeploymentResult{
|
||||||
|
ConfigType: "Kitty Tabs",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil {
|
||||||
|
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
||||||
|
return results, tabsResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
tabsResult.Deployed = true
|
||||||
|
cd.log("Successfully deployed Kitty tabs configuration")
|
||||||
|
results = append(results, tabsResult)
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
||||||
|
var results []DeploymentResult
|
||||||
|
|
||||||
|
mainResult := DeploymentResult{
|
||||||
|
ConfigType: "Alacritty",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||||
|
cd.log("Found existing Alacritty configuration")
|
||||||
|
|
||||||
|
existingData, err := os.ReadFile(mainResult.Path)
|
||||||
|
if err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
mainResult.Deployed = true
|
||||||
|
cd.log("Successfully deployed Alacritty configuration")
|
||||||
|
results = append(results, mainResult)
|
||||||
|
|
||||||
|
themeResult := DeploymentResult{
|
||||||
|
ConfigType: "Alacritty Theme",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil {
|
||||||
|
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||||
|
return results, themeResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
themeResult.Deployed = true
|
||||||
|
cd.log("Successfully deployed Alacritty theme configuration")
|
||||||
|
results = append(results, themeResult)
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectPolkitAgent tries to find the polkit authentication agent on the system
|
||||||
|
// Prioritizes mate-polkit paths since that's what we install
|
||||||
|
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
|
||||||
|
// Prioritize mate-polkit paths first
|
||||||
|
matePaths := []string{
|
||||||
|
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
|
||||||
|
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
|
||||||
|
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
|
||||||
|
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
|
||||||
|
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range matePaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to other polkit agents if mate-polkit is not found
|
||||||
|
fallbackPaths := []string{
|
||||||
|
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
||||||
|
"/usr/libexec/polkit-gnome-authentication-agent-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range fallbackPaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no polkit agent found in common locations")
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
|
||||||
|
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
|
||||||
|
// Regular expression to match output sections (including commented ones)
|
||||||
|
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||||
|
|
||||||
|
// Find all output sections in the existing config
|
||||||
|
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
||||||
|
|
||||||
|
if len(existingOutputs) == 0 {
|
||||||
|
// No output sections to merge
|
||||||
|
return newConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the example output section from the new config
|
||||||
|
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||||
|
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
|
||||||
|
|
||||||
|
// Find where to insert the output sections (after the input section)
|
||||||
|
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
|
||||||
|
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
|
||||||
|
|
||||||
|
if len(inputMatches) < 1 {
|
||||||
|
return "", fmt.Errorf("could not find insertion point for output sections")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert after the first closing brace (end of input section)
|
||||||
|
insertPos := inputMatches[0][1]
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(mergedConfig[:insertPos])
|
||||||
|
builder.WriteString("\n// Outputs from existing configuration\n")
|
||||||
|
|
||||||
|
for _, output := range existingOutputs {
|
||||||
|
builder.WriteString(output)
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString(mergedConfig[insertPos:])
|
||||||
|
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
|
||||||
|
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
||||||
|
result := DeploymentResult{
|
||||||
|
ConfigType: "Hyprland",
|
||||||
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Dir(result.Path)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingConfig string
|
||||||
|
if _, err := os.Stat(result.Path); err == nil {
|
||||||
|
cd.log("Found existing Hyprland configuration")
|
||||||
|
|
||||||
|
existingData, err := os.ReadFile(result.Path)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
existingConfig = string(existingData)
|
||||||
|
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
|
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect polkit agent path
|
||||||
|
polkitPath, err := cd.detectPolkitAgent()
|
||||||
|
if err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
||||||
|
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine terminal command based on choice
|
||||||
|
var terminalCommand string
|
||||||
|
switch terminal {
|
||||||
|
case deps.TerminalGhostty:
|
||||||
|
terminalCommand = "ghostty"
|
||||||
|
case deps.TerminalKitty:
|
||||||
|
terminalCommand = "kitty"
|
||||||
|
case deps.TerminalAlacritty:
|
||||||
|
terminalCommand = "alacritty"
|
||||||
|
default:
|
||||||
|
terminalCommand = "ghostty" // fallback to ghostty
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
||||||
|
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
|
|
||||||
|
// If there was an existing config, merge the monitor sections
|
||||||
|
if existingConfig != "" {
|
||||||
|
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
|
||||||
|
if err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
|
||||||
|
} else {
|
||||||
|
newConfig = mergedConfig
|
||||||
|
cd.log("Successfully merged existing monitor sections")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Deployed = true
|
||||||
|
cd.log("Successfully deployed Hyprland configuration")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
|
||||||
|
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
|
||||||
|
// Regular expression to match monitor lines (including commented ones)
|
||||||
|
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
|
||||||
|
// Also matches commented versions: # monitor = ...
|
||||||
|
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
||||||
|
|
||||||
|
// Find all monitor lines in the existing config
|
||||||
|
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
||||||
|
|
||||||
|
if len(existingMonitors) == 0 {
|
||||||
|
// No monitor sections to merge
|
||||||
|
return newConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the example monitor line from the new config
|
||||||
|
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
||||||
|
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
||||||
|
|
||||||
|
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
|
||||||
|
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
||||||
|
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
||||||
|
|
||||||
|
if headerMatch == nil {
|
||||||
|
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert after the header
|
||||||
|
insertPos := headerMatch[1] + 1 // +1 for the newline
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,660 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDetectPolkitAgent(t *testing.T) {
|
||||||
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
|
// This test depends on the system having a polkit agent installed
|
||||||
|
// We'll just test that the function doesn't crash and returns some path or error
|
||||||
|
path, err := cd.detectPolkitAgent()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// If no polkit agent is found, that's okay for testing
|
||||||
|
assert.Contains(t, err.Error(), "no polkit agent found")
|
||||||
|
} else {
|
||||||
|
// If found, it should be a valid path
|
||||||
|
assert.NotEmpty(t, path)
|
||||||
|
assert.True(t, strings.Contains(path, "polkit"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeNiriOutputSections(t *testing.T) {
|
||||||
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
newConfig string
|
||||||
|
existingConfig string
|
||||||
|
wantError bool
|
||||||
|
wantContains []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no existing outputs",
|
||||||
|
newConfig: `input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 5
|
||||||
|
}`,
|
||||||
|
existingConfig: `input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 10
|
||||||
|
}`,
|
||||||
|
wantError: false,
|
||||||
|
wantContains: []string{"gaps 5"}, // Should keep new config
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "merge single output",
|
||||||
|
newConfig: `input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/-output "eDP-2" {
|
||||||
|
mode "2560x1600@239.998993"
|
||||||
|
position x=2560 y=0
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 5
|
||||||
|
}`,
|
||||||
|
existingConfig: `input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output "eDP-1" {
|
||||||
|
mode "1920x1080@60.000000"
|
||||||
|
position x=0 y=0
|
||||||
|
scale 1.0
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 10
|
||||||
|
}`,
|
||||||
|
wantError: false,
|
||||||
|
wantContains: []string{
|
||||||
|
"gaps 5", // New config preserved
|
||||||
|
`output "eDP-1"`, // Existing output merged
|
||||||
|
"1920x1080@60.000000", // Existing output details
|
||||||
|
"Outputs from existing configuration", // Comment added
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "merge multiple outputs",
|
||||||
|
newConfig: `input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/-output "eDP-2" {
|
||||||
|
mode "2560x1600@239.998993"
|
||||||
|
position x=2560 y=0
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 5
|
||||||
|
}`,
|
||||||
|
existingConfig: `input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output "eDP-1" {
|
||||||
|
mode "1920x1080@60.000000"
|
||||||
|
position x=0 y=0
|
||||||
|
scale 1.0
|
||||||
|
}
|
||||||
|
/-output "HDMI-1" {
|
||||||
|
mode "1920x1080@60.000000"
|
||||||
|
position x=1920 y=0
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 10
|
||||||
|
}`,
|
||||||
|
wantError: false,
|
||||||
|
wantContains: []string{
|
||||||
|
"gaps 5", // New config preserved
|
||||||
|
`output "eDP-1"`, // First existing output
|
||||||
|
`/-output "HDMI-1"`, // Second existing output (commented)
|
||||||
|
"1920x1080@60.000000", // Output details
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "merge commented outputs",
|
||||||
|
newConfig: `input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/-output "eDP-2" {
|
||||||
|
mode "2560x1600@239.998993"
|
||||||
|
position x=2560 y=0
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 5
|
||||||
|
}`,
|
||||||
|
existingConfig: `input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/-output "eDP-1" {
|
||||||
|
mode "1920x1080@60.000000"
|
||||||
|
position x=0 y=0
|
||||||
|
scale 1.0
|
||||||
|
}
|
||||||
|
layout {
|
||||||
|
gaps 10
|
||||||
|
}`,
|
||||||
|
wantError: false,
|
||||||
|
wantContains: []string{
|
||||||
|
"gaps 5", // New config preserved
|
||||||
|
`/-output "eDP-1"`, // Commented output preserved
|
||||||
|
"1920x1080@60.000000", // Output details
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig)
|
||||||
|
|
||||||
|
if tt.wantError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, want := range tt.wantContains {
|
||||||
|
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotContains(t, result, `/-output "eDP-2"`, "example output should be removed")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigDeploymentFlow(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
t.Run("deploy ghostty config to empty directory", func(t *testing.T) {
|
||||||
|
results, err := cd.deployGhosttyConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, results, 2)
|
||||||
|
|
||||||
|
mainResult := results[0]
|
||||||
|
assert.Equal(t, "Ghostty", mainResult.ConfigType)
|
||||||
|
assert.True(t, mainResult.Deployed)
|
||||||
|
assert.Empty(t, mainResult.BackupPath)
|
||||||
|
assert.FileExists(t, mainResult.Path)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(mainResult.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(content), "window-decoration = false")
|
||||||
|
|
||||||
|
colorResult := results[1]
|
||||||
|
assert.Equal(t, "Ghostty Colors", colorResult.ConfigType)
|
||||||
|
assert.True(t, colorResult.Deployed)
|
||||||
|
assert.FileExists(t, colorResult.Path)
|
||||||
|
|
||||||
|
colorContent, err := os.ReadFile(colorResult.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(colorContent), "background = #101418")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
||||||
|
existingContent := "# Old config\nfont-size = 14\n"
|
||||||
|
ghosttyPath := getGhosttyPath()
|
||||||
|
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
results, err := cd.deployGhosttyConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, results, 2)
|
||||||
|
|
||||||
|
mainResult := results[0]
|
||||||
|
assert.Equal(t, "Ghostty", mainResult.ConfigType)
|
||||||
|
assert.True(t, mainResult.Deployed)
|
||||||
|
assert.NotEmpty(t, mainResult.BackupPath)
|
||||||
|
assert.FileExists(t, mainResult.Path)
|
||||||
|
assert.FileExists(t, mainResult.BackupPath)
|
||||||
|
|
||||||
|
backupContent, err := os.ReadFile(mainResult.BackupPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, existingContent, string(backupContent))
|
||||||
|
|
||||||
|
newContent, err := os.ReadFile(mainResult.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, string(newContent), "# Old config")
|
||||||
|
|
||||||
|
colorResult := results[1]
|
||||||
|
assert.Equal(t, "Ghostty Colors", colorResult.ConfigType)
|
||||||
|
assert.True(t, colorResult.Deployed)
|
||||||
|
assert.FileExists(t, colorResult.Path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGhosttyPath() string {
|
||||||
|
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolkitPathInjection(t *testing.T) {
|
||||||
|
|
||||||
|
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
||||||
|
other content`
|
||||||
|
|
||||||
|
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
|
||||||
|
|
||||||
|
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
|
||||||
|
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||||
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
newConfig string
|
||||||
|
existingConfig string
|
||||||
|
wantError bool
|
||||||
|
wantContains []string
|
||||||
|
wantNotContains []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no existing monitors",
|
||||||
|
newConfig: `# ==================
|
||||||
|
# MONITOR CONFIG
|
||||||
|
# ==================
|
||||||
|
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# ENVIRONMENT VARS
|
||||||
|
# ==================
|
||||||
|
env = XDG_CURRENT_DESKTOP,niri`,
|
||||||
|
existingConfig: `# Some other config
|
||||||
|
input {
|
||||||
|
kb_layout = us
|
||||||
|
}`,
|
||||||
|
wantError: false,
|
||||||
|
wantContains: []string{"MONITOR CONFIG", "ENVIRONMENT VARS"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "merge single monitor",
|
||||||
|
newConfig: `# ==================
|
||||||
|
# MONITOR CONFIG
|
||||||
|
# ==================
|
||||||
|
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# ENVIRONMENT VARS
|
||||||
|
# ==================`,
|
||||||
|
existingConfig: `# My config
|
||||||
|
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||||
|
input {
|
||||||
|
kb_layout = us
|
||||||
|
}`,
|
||||||
|
wantError: false,
|
||||||
|
wantContains: []string{
|
||||||
|
"MONITOR CONFIG",
|
||||||
|
"monitor = DP-1, 1920x1080@144, 0x0, 1",
|
||||||
|
"Monitors from existing configuration",
|
||||||
|
},
|
||||||
|
wantNotContains: []string{
|
||||||
|
"monitor = eDP-2", // Example monitor should be removed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "merge multiple monitors",
|
||||||
|
newConfig: `# ==================
|
||||||
|
# MONITOR CONFIG
|
||||||
|
# ==================
|
||||||
|
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# ENVIRONMENT VARS
|
||||||
|
# ==================`,
|
||||||
|
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||||
|
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
||||||
|
monitor = eDP-1, 2560x1440@165, auto, 1.25`,
|
||||||
|
wantError: false,
|
||||||
|
wantContains: []string{
|
||||||
|
"monitor = DP-1",
|
||||||
|
"# monitor = HDMI-A-1", // Commented monitor preserved
|
||||||
|
"monitor = eDP-1",
|
||||||
|
"Monitors from existing configuration",
|
||||||
|
},
|
||||||
|
wantNotContains: []string{
|
||||||
|
"monitor = eDP-2", // Example monitor should be removed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preserve commented monitors",
|
||||||
|
newConfig: `# ==================
|
||||||
|
# MONITOR CONFIG
|
||||||
|
# ==================
|
||||||
|
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||||
|
|
||||||
|
# ==================`,
|
||||||
|
existingConfig: `# monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||||
|
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1`,
|
||||||
|
wantError: false,
|
||||||
|
wantContains: []string{
|
||||||
|
"# monitor = DP-1",
|
||||||
|
"# monitor = HDMI-A-1",
|
||||||
|
"Monitors from existing configuration",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no monitor config section",
|
||||||
|
newConfig: `# Some config without monitor section
|
||||||
|
input {
|
||||||
|
kb_layout = us
|
||||||
|
}`,
|
||||||
|
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1`,
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig)
|
||||||
|
|
||||||
|
if tt.wantError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, want := range tt.wantContains {
|
||||||
|
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, notWant := range tt.wantNotContains {
|
||||||
|
assert.NotContains(t, result, notWant, "merged config should NOT contain: %s", notWant)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandConfigDeployment(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-hyprland-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
||||||
|
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||||
|
assert.True(t, result.Deployed)
|
||||||
|
assert.Empty(t, result.BackupPath)
|
||||||
|
assert.FileExists(t, result.Path)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(result.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||||
|
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
|
||||||
|
assert.Contains(t, string(content), "exec-once = ")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
|
||||||
|
existingContent := `# My existing Hyprland config
|
||||||
|
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||||
|
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
||||||
|
|
||||||
|
general {
|
||||||
|
gaps_in = 10
|
||||||
|
}
|
||||||
|
`
|
||||||
|
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||||
|
err := os.MkdirAll(filepath.Dir(hyprPath), 0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||||
|
assert.True(t, result.Deployed)
|
||||||
|
assert.NotEmpty(t, result.BackupPath)
|
||||||
|
assert.FileExists(t, result.Path)
|
||||||
|
assert.FileExists(t, result.BackupPath)
|
||||||
|
|
||||||
|
backupContent, err := os.ReadFile(result.BackupPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, existingContent, string(backupContent))
|
||||||
|
|
||||||
|
newContent, err := os.ReadFile(result.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||||
|
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||||
|
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
|
||||||
|
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, NiriConfig, "input {")
|
||||||
|
assert.Contains(t, NiriConfig, "layout {")
|
||||||
|
assert.Contains(t, NiriConfig, "binds {")
|
||||||
|
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
|
||||||
|
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||||
|
assert.Contains(t, HyprlandConfig, "# ENVIRONMENT VARS")
|
||||||
|
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||||
|
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||||
|
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
||||||
|
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
|
||||||
|
assert.Contains(t, HyprlandConfig, "{{TERMINAL_COMMAND}}")
|
||||||
|
assert.Contains(t, HyprlandConfig, "exec-once = dms run")
|
||||||
|
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec,")
|
||||||
|
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
||||||
|
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||||
|
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||||
|
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGhosttyColorConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, GhosttyColorConfig, "background = #101418")
|
||||||
|
assert.Contains(t, GhosttyColorConfig, "foreground = #e0e2e8")
|
||||||
|
assert.Contains(t, GhosttyColorConfig, "cursor-color = #9dcbfb")
|
||||||
|
assert.Contains(t, GhosttyColorConfig, "palette = 0=#101418")
|
||||||
|
assert.Contains(t, GhosttyColorConfig, "palette = 15=#ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKittyConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, KittyConfig, "font_size 12.0")
|
||||||
|
assert.Contains(t, KittyConfig, "window_padding_width 12")
|
||||||
|
assert.Contains(t, KittyConfig, "background_opacity 1.0")
|
||||||
|
assert.Contains(t, KittyConfig, "include dank-tabs.conf")
|
||||||
|
assert.Contains(t, KittyConfig, "include dank-theme.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKittyThemeConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, KittyThemeConfig, "foreground #e0e2e8")
|
||||||
|
assert.Contains(t, KittyThemeConfig, "background #101418")
|
||||||
|
assert.Contains(t, KittyThemeConfig, "cursor #e0e2e8")
|
||||||
|
assert.Contains(t, KittyThemeConfig, "color0 #101418")
|
||||||
|
assert.Contains(t, KittyThemeConfig, "color15 #ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKittyTabsConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, KittyTabsConfig, "tab_bar_style powerline")
|
||||||
|
assert.Contains(t, KittyTabsConfig, "tab_powerline_style slanted")
|
||||||
|
assert.Contains(t, KittyTabsConfig, "active_tab_background #124a73")
|
||||||
|
assert.Contains(t, KittyTabsConfig, "inactive_tab_background #101418")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlacrittyConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, AlacrittyConfig, "[general]")
|
||||||
|
assert.Contains(t, AlacrittyConfig, "~/.config/alacritty/dank-theme.toml")
|
||||||
|
assert.Contains(t, AlacrittyConfig, "[window]")
|
||||||
|
assert.Contains(t, AlacrittyConfig, "decorations = \"None\"")
|
||||||
|
assert.Contains(t, AlacrittyConfig, "padding = { x = 12, y = 12 }")
|
||||||
|
assert.Contains(t, AlacrittyConfig, "[cursor]")
|
||||||
|
assert.Contains(t, AlacrittyConfig, "[keyboard]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlacrittyThemeConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, AlacrittyThemeConfig, "[colors.primary]")
|
||||||
|
assert.Contains(t, AlacrittyThemeConfig, "background = '#101418'")
|
||||||
|
assert.Contains(t, AlacrittyThemeConfig, "foreground = '#e0e2e8'")
|
||||||
|
assert.Contains(t, AlacrittyThemeConfig, "[colors.cursor]")
|
||||||
|
assert.Contains(t, AlacrittyThemeConfig, "cursor = '#9dcbfb'")
|
||||||
|
assert.Contains(t, AlacrittyThemeConfig, "[colors.normal]")
|
||||||
|
assert.Contains(t, AlacrittyThemeConfig, "[colors.bright]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKittyConfigDeployment(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-kitty-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
t.Run("deploy kitty config to empty directory", func(t *testing.T) {
|
||||||
|
results, err := cd.deployKittyConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, results, 3)
|
||||||
|
|
||||||
|
mainResult := results[0]
|
||||||
|
assert.Equal(t, "Kitty", mainResult.ConfigType)
|
||||||
|
assert.True(t, mainResult.Deployed)
|
||||||
|
assert.FileExists(t, mainResult.Path)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(mainResult.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(content), "include dank-theme.conf")
|
||||||
|
|
||||||
|
themeResult := results[1]
|
||||||
|
assert.Equal(t, "Kitty Theme", themeResult.ConfigType)
|
||||||
|
assert.True(t, themeResult.Deployed)
|
||||||
|
assert.FileExists(t, themeResult.Path)
|
||||||
|
|
||||||
|
tabsResult := results[2]
|
||||||
|
assert.Equal(t, "Kitty Tabs", tabsResult.ConfigType)
|
||||||
|
assert.True(t, tabsResult.Deployed)
|
||||||
|
assert.FileExists(t, tabsResult.Path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlacrittyConfigDeployment(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-alacritty-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
t.Run("deploy alacritty config to empty directory", func(t *testing.T) {
|
||||||
|
results, err := cd.deployAlacrittyConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, results, 2)
|
||||||
|
|
||||||
|
mainResult := results[0]
|
||||||
|
assert.Equal(t, "Alacritty", mainResult.ConfigType)
|
||||||
|
assert.True(t, mainResult.Deployed)
|
||||||
|
assert.FileExists(t, mainResult.Path)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(mainResult.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(content), "~/.config/alacritty/dank-theme.toml")
|
||||||
|
assert.Contains(t, string(content), "[window]")
|
||||||
|
|
||||||
|
themeResult := results[1]
|
||||||
|
assert.Equal(t, "Alacritty Theme", themeResult.ConfigType)
|
||||||
|
assert.True(t, themeResult.Deployed)
|
||||||
|
assert.FileExists(t, themeResult.Path)
|
||||||
|
|
||||||
|
themeContent, err := os.ReadFile(themeResult.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(themeContent), "[colors.primary]")
|
||||||
|
assert.Contains(t, string(themeContent), "background = '#101418'")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
||||||
|
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
||||||
|
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
||||||
|
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
results, err := cd.deployAlacrittyConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, results, 2)
|
||||||
|
|
||||||
|
mainResult := results[0]
|
||||||
|
assert.True(t, mainResult.Deployed)
|
||||||
|
assert.NotEmpty(t, mainResult.BackupPath)
|
||||||
|
assert.FileExists(t, mainResult.BackupPath)
|
||||||
|
|
||||||
|
backupContent, err := os.ReadFile(mainResult.BackupPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, existingContent, string(backupContent))
|
||||||
|
|
||||||
|
newContent, err := os.ReadFile(mainResult.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, string(newContent), "# Old alacritty config")
|
||||||
|
assert.Contains(t, string(newContent), "decorations = \"None\"")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
|
||||||
|
func LocateDMSConfig() (string, error) {
|
||||||
|
var primaryPaths []string
|
||||||
|
|
||||||
|
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
if configHome == "" {
|
||||||
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||||
|
configHome = filepath.Join(homeDir, ".config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if configHome != "" {
|
||||||
|
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryPaths = append(primaryPaths, "/usr/share/quickshell/dms")
|
||||||
|
|
||||||
|
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||||
|
if configDirs == "" {
|
||||||
|
configDirs = "/etc/xdg"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range strings.Split(configDirs, ":") {
|
||||||
|
if dir != "" {
|
||||||
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build search paths with secondary (monorepo) paths interleaved
|
||||||
|
var searchPaths []string
|
||||||
|
for _, path := range primaryPaths {
|
||||||
|
searchPaths = append(searchPaths, path)
|
||||||
|
searchPaths = append(searchPaths, filepath.Join(path, "quickshell"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range searchPaths {
|
||||||
|
shellPath := filepath.Join(path, "shell.qml")
|
||||||
|
if info, err := os.Stat(shellPath); err == nil && !info.IsDir() {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not find DMS config (shell.qml) in any valid config path")
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[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'
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[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" },
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Font Configuration
|
||||||
|
font-size = 12
|
||||||
|
|
||||||
|
# Window Configuration
|
||||||
|
window-decoration = false
|
||||||
|
window-padding-x = 12
|
||||||
|
window-padding-y = 12
|
||||||
|
background-opacity = 1.0
|
||||||
|
background-blur-radius = 32
|
||||||
|
|
||||||
|
# Cursor Configuration
|
||||||
|
cursor-style = block
|
||||||
|
cursor-style-blink = true
|
||||||
|
|
||||||
|
# Scrollback
|
||||||
|
scrollback-limit = 3023
|
||||||
|
|
||||||
|
# Terminal features
|
||||||
|
mouse-hide-while-typing = true
|
||||||
|
copy-on-select = false
|
||||||
|
confirm-close-surface = false
|
||||||
|
|
||||||
|
# Disable annoying copied to clipboard
|
||||||
|
app-notifications = no-clipboard-copy,no-config-reload
|
||||||
|
|
||||||
|
# Key bindings for common actions
|
||||||
|
#keybind = ctrl+c=copy_to_clipboard
|
||||||
|
#keybind = ctrl+v=paste_from_clipboard
|
||||||
|
keybind = ctrl+shift+n=new_window
|
||||||
|
keybind = ctrl+t=new_tab
|
||||||
|
keybind = ctrl+plus=increase_font_size:1
|
||||||
|
keybind = ctrl+minus=decrease_font_size:1
|
||||||
|
keybind = ctrl+zero=reset_font_size
|
||||||
|
|
||||||
|
# Material 3 UI elements
|
||||||
|
unfocused-split-opacity = 0.7
|
||||||
|
unfocused-split-fill = #44464f
|
||||||
|
|
||||||
|
# Tab configuration
|
||||||
|
gtk-titlebar = false
|
||||||
|
|
||||||
|
# Shell integration
|
||||||
|
shell-integration = detect
|
||||||
|
shell-integration-features = cursor,sudo,title,no-cursor
|
||||||
|
keybind = shift+enter=text:\n
|
||||||
|
|
||||||
|
# Rando stuff
|
||||||
|
gtk-single-instance = true
|
||||||
|
|
||||||
|
# Dank color generation
|
||||||
|
config-file = ./config-dankcolors
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
# Hyprland Configuration
|
||||||
|
# https://wiki.hypr.land/Configuring/
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# MONITOR CONFIG
|
||||||
|
# ==================
|
||||||
|
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||||
|
monitor = , preferred,auto,auto
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# ENVIRONMENT VARS
|
||||||
|
# ==================
|
||||||
|
env = QT_QPA_PLATFORM,wayland
|
||||||
|
env = ELECTRON_OZONE_PLATFORM_HINT,auto
|
||||||
|
env = QT_QPA_PLATFORMTHEME,gtk3
|
||||||
|
env = QT_QPA_PLATFORMTHEME_QT6,gtk3
|
||||||
|
env = TERMINAL,{{TERMINAL_COMMAND}}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# STARTUP APPS
|
||||||
|
# ==================
|
||||||
|
exec-once = bash -c "wl-paste --watch cliphist store &"
|
||||||
|
exec-once = dms run
|
||||||
|
exec-once = {{POLKIT_AGENT_PATH}}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# INPUT CONFIG
|
||||||
|
# ==================
|
||||||
|
input {
|
||||||
|
kb_layout = us
|
||||||
|
numlock_by_default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# GENERAL LAYOUT
|
||||||
|
# ==================
|
||||||
|
general {
|
||||||
|
gaps_in = 5
|
||||||
|
gaps_out = 5
|
||||||
|
border_size = 0 # off in niri
|
||||||
|
|
||||||
|
col.active_border = rgba(707070ff)
|
||||||
|
col.inactive_border = rgba(d0d0d0ff)
|
||||||
|
|
||||||
|
layout = dwindle
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# DECORATION
|
||||||
|
# ==================
|
||||||
|
decoration {
|
||||||
|
rounding = 12
|
||||||
|
|
||||||
|
active_opacity = 1.0
|
||||||
|
inactive_opacity = 0.9
|
||||||
|
|
||||||
|
shadow {
|
||||||
|
enabled = true
|
||||||
|
range = 30
|
||||||
|
render_power = 5
|
||||||
|
offset = 0 5
|
||||||
|
color = rgba(00000070)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# ANIMATIONS
|
||||||
|
# ==================
|
||||||
|
animations {
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
animation = windowsIn, 1, 3, default
|
||||||
|
animation = windowsOut, 1, 3, default
|
||||||
|
animation = workspaces, 1, 5, default
|
||||||
|
animation = windowsMove, 1, 4, default
|
||||||
|
animation = fade, 1, 3, default
|
||||||
|
animation = border, 1, 3, default
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# LAYOUTS
|
||||||
|
# ==================
|
||||||
|
dwindle {
|
||||||
|
preserve_split = true
|
||||||
|
}
|
||||||
|
|
||||||
|
master {
|
||||||
|
mfact = 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# MISC
|
||||||
|
# ==================
|
||||||
|
misc {
|
||||||
|
disable_hyprland_logo = true
|
||||||
|
disable_splash_rendering = true
|
||||||
|
vrr = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# WINDOW RULES
|
||||||
|
# ==================
|
||||||
|
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
|
||||||
|
|
||||||
|
windowrulev2 = rounding 12, class:^(org\.gnome\.)
|
||||||
|
windowrulev2 = noborder, class:^(org\.gnome\.)
|
||||||
|
|
||||||
|
windowrulev2 = tile, class:^(gnome-control-center)$
|
||||||
|
windowrulev2 = tile, class:^(pavucontrol)$
|
||||||
|
windowrulev2 = tile, class:^(nm-connection-editor)$
|
||||||
|
|
||||||
|
windowrulev2 = float, class:^(gnome-calculator)$
|
||||||
|
windowrulev2 = float, class:^(galculator)$
|
||||||
|
windowrulev2 = float, class:^(blueman-manager)$
|
||||||
|
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
|
||||||
|
windowrulev2 = float, class:^(steam)$
|
||||||
|
windowrulev2 = float, class:^(xdg-desktop-portal)$
|
||||||
|
|
||||||
|
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
|
||||||
|
windowrulev2 = noborder, class:^(Alacritty)$
|
||||||
|
windowrulev2 = noborder, class:^(zen)$
|
||||||
|
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
|
||||||
|
windowrulev2 = noborder, class:^(kitty)$
|
||||||
|
|
||||||
|
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
|
||||||
|
windowrulev2 = float, class:^(zoom)$
|
||||||
|
|
||||||
|
# DMS windows floating by default
|
||||||
|
windowrulev2 = float, class:^(org.quickshell)$
|
||||||
|
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
|
||||||
|
|
||||||
|
layerrule = noanim, ^(quickshell)$
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# KEYBINDINGS
|
||||||
|
# ==================
|
||||||
|
$mod = SUPER
|
||||||
|
|
||||||
|
# === Application Launchers ===
|
||||||
|
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
||||||
|
bind = $mod, space, exec, dms ipc call spotlight toggle
|
||||||
|
bind = $mod, V, exec, dms ipc call clipboard toggle
|
||||||
|
bind = $mod, M, exec, dms ipc call processlist toggle
|
||||||
|
bind = $mod, comma, exec, dms ipc call settings toggle
|
||||||
|
bind = $mod, N, exec, dms ipc call notifications toggle
|
||||||
|
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
||||||
|
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
||||||
|
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
|
||||||
|
|
||||||
|
# === Cheat sheet
|
||||||
|
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
||||||
|
|
||||||
|
# === Security ===
|
||||||
|
bind = $mod ALT, L, exec, dms ipc call lock lock
|
||||||
|
bind = $mod SHIFT, E, exit
|
||||||
|
bind = CTRL ALT, Delete, exec, dms ipc call processlist toggle
|
||||||
|
|
||||||
|
# === Audio Controls ===
|
||||||
|
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
||||||
|
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
||||||
|
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
||||||
|
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
||||||
|
|
||||||
|
# === Brightness Controls ===
|
||||||
|
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||||
|
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
||||||
|
|
||||||
|
# === Window Management ===
|
||||||
|
bind = $mod, Q, killactive
|
||||||
|
bind = $mod, F, fullscreen, 1
|
||||||
|
bind = $mod SHIFT, F, fullscreen, 0
|
||||||
|
bind = $mod SHIFT, T, togglefloating
|
||||||
|
bind = $mod, W, togglegroup
|
||||||
|
|
||||||
|
# === Focus Navigation ===
|
||||||
|
bind = $mod, left, movefocus, l
|
||||||
|
bind = $mod, down, movefocus, d
|
||||||
|
bind = $mod, up, movefocus, u
|
||||||
|
bind = $mod, right, movefocus, r
|
||||||
|
bind = $mod, H, movefocus, l
|
||||||
|
bind = $mod, J, movefocus, d
|
||||||
|
bind = $mod, K, movefocus, u
|
||||||
|
bind = $mod, L, movefocus, r
|
||||||
|
|
||||||
|
# === Window Movement ===
|
||||||
|
bind = $mod SHIFT, left, movewindow, l
|
||||||
|
bind = $mod SHIFT, down, movewindow, d
|
||||||
|
bind = $mod SHIFT, up, movewindow, u
|
||||||
|
bind = $mod SHIFT, right, movewindow, r
|
||||||
|
bind = $mod SHIFT, H, movewindow, l
|
||||||
|
bind = $mod SHIFT, J, movewindow, d
|
||||||
|
bind = $mod SHIFT, K, movewindow, u
|
||||||
|
bind = $mod SHIFT, L, movewindow, r
|
||||||
|
|
||||||
|
# === Column Navigation ===
|
||||||
|
bind = $mod, Home, focuswindow, first
|
||||||
|
bind = $mod, End, focuswindow, last
|
||||||
|
|
||||||
|
# === Monitor Navigation ===
|
||||||
|
bind = $mod CTRL, left, focusmonitor, l
|
||||||
|
bind = $mod CTRL, right, focusmonitor, r
|
||||||
|
bind = $mod CTRL, H, focusmonitor, l
|
||||||
|
bind = $mod CTRL, J, focusmonitor, d
|
||||||
|
bind = $mod CTRL, K, focusmonitor, u
|
||||||
|
bind = $mod CTRL, L, focusmonitor, r
|
||||||
|
|
||||||
|
# === Move to Monitor ===
|
||||||
|
bind = $mod SHIFT CTRL, left, movewindow, mon:l
|
||||||
|
bind = $mod SHIFT CTRL, down, movewindow, mon:d
|
||||||
|
bind = $mod SHIFT CTRL, up, movewindow, mon:u
|
||||||
|
bind = $mod SHIFT CTRL, right, movewindow, mon:r
|
||||||
|
bind = $mod SHIFT CTRL, H, movewindow, mon:l
|
||||||
|
bind = $mod SHIFT CTRL, J, movewindow, mon:d
|
||||||
|
bind = $mod SHIFT CTRL, K, movewindow, mon:u
|
||||||
|
bind = $mod SHIFT CTRL, L, movewindow, mon:r
|
||||||
|
|
||||||
|
# === Workspace Navigation ===
|
||||||
|
bind = $mod, Page_Down, workspace, e+1
|
||||||
|
bind = $mod, Page_Up, workspace, e-1
|
||||||
|
bind = $mod, U, workspace, e+1
|
||||||
|
bind = $mod, I, workspace, e-1
|
||||||
|
bind = $mod CTRL, down, movetoworkspace, e+1
|
||||||
|
bind = $mod CTRL, up, movetoworkspace, e-1
|
||||||
|
bind = $mod CTRL, U, movetoworkspace, e+1
|
||||||
|
bind = $mod CTRL, I, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Move Workspaces ===
|
||||||
|
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
|
||||||
|
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
|
||||||
|
bind = $mod SHIFT, U, movetoworkspace, e+1
|
||||||
|
bind = $mod SHIFT, I, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Mouse Wheel Navigation ===
|
||||||
|
bind = $mod, mouse_down, workspace, e+1
|
||||||
|
bind = $mod, mouse_up, workspace, e-1
|
||||||
|
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
|
||||||
|
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Numbered Workspaces ===
|
||||||
|
bind = $mod, 1, workspace, 1
|
||||||
|
bind = $mod, 2, workspace, 2
|
||||||
|
bind = $mod, 3, workspace, 3
|
||||||
|
bind = $mod, 4, workspace, 4
|
||||||
|
bind = $mod, 5, workspace, 5
|
||||||
|
bind = $mod, 6, workspace, 6
|
||||||
|
bind = $mod, 7, workspace, 7
|
||||||
|
bind = $mod, 8, workspace, 8
|
||||||
|
bind = $mod, 9, workspace, 9
|
||||||
|
|
||||||
|
# === Move to Numbered Workspaces ===
|
||||||
|
bind = $mod SHIFT, 1, movetoworkspace, 1
|
||||||
|
bind = $mod SHIFT, 2, movetoworkspace, 2
|
||||||
|
bind = $mod SHIFT, 3, movetoworkspace, 3
|
||||||
|
bind = $mod SHIFT, 4, movetoworkspace, 4
|
||||||
|
bind = $mod SHIFT, 5, movetoworkspace, 5
|
||||||
|
bind = $mod SHIFT, 6, movetoworkspace, 6
|
||||||
|
bind = $mod SHIFT, 7, movetoworkspace, 7
|
||||||
|
bind = $mod SHIFT, 8, movetoworkspace, 8
|
||||||
|
bind = $mod SHIFT, 9, movetoworkspace, 9
|
||||||
|
|
||||||
|
# === Column Management ===
|
||||||
|
bind = $mod, bracketleft, layoutmsg, preselect l
|
||||||
|
bind = $mod, bracketright, layoutmsg, preselect r
|
||||||
|
|
||||||
|
# === Sizing & Layout ===
|
||||||
|
bind = $mod, R, layoutmsg, togglesplit
|
||||||
|
bind = $mod CTRL, F, resizeactive, exact 100%
|
||||||
|
|
||||||
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
|
bindmd = $mod, mouse:272, Move window, movewindow
|
||||||
|
bindmd = $mod, mouse:273, Resize window, resizewindow
|
||||||
|
|
||||||
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
|
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
|
||||||
|
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
|
||||||
|
|
||||||
|
# === Manual Sizing ===
|
||||||
|
binde = $mod, minus, resizeactive, -10% 0
|
||||||
|
binde = $mod, equal, resizeactive, 10% 0
|
||||||
|
binde = $mod SHIFT, minus, resizeactive, 0 -10%
|
||||||
|
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
||||||
|
|
||||||
|
# === Screenshots ===
|
||||||
|
bind = , XF86Launch1, exec, grimblast copy area
|
||||||
|
bind = CTRL, XF86Launch1, exec, grimblast copy screen
|
||||||
|
bind = ALT, XF86Launch1, exec, grimblast copy active
|
||||||
|
bind = , Print, exec, grimblast copy area
|
||||||
|
bind = CTRL, Print, exec, grimblast copy screen
|
||||||
|
bind = ALT, Print, exec, grimblast copy active
|
||||||
|
|
||||||
|
# === System Controls ===
|
||||||
|
bind = $mod SHIFT, P, dpms, off
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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}]"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
// This config is in the KDL format: https://kdl.dev
|
||||||
|
// "/-" comments out the following node.
|
||||||
|
// Check the wiki for a full description of the configuration:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
|
||||||
|
config-notification {
|
||||||
|
disable-failed
|
||||||
|
}
|
||||||
|
|
||||||
|
gestures {
|
||||||
|
hot-corners {
|
||||||
|
off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input device configuration.
|
||||||
|
// Find the full list of options on the wiki:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
}
|
||||||
|
numlock
|
||||||
|
}
|
||||||
|
touchpad {
|
||||||
|
}
|
||||||
|
mouse {
|
||||||
|
}
|
||||||
|
trackpoint {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// You can configure outputs by their name, which you can find
|
||||||
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
|
// The built-in laptop monitor is usually called "eDP-1".
|
||||||
|
// Find more information on the wiki:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
|
||||||
|
// Remember to uncomment the node by removing "/-"!
|
||||||
|
/-output "eDP-2" {
|
||||||
|
mode "2560x1600@239.998993"
|
||||||
|
position x=2560 y=0
|
||||||
|
variable-refresh-rate
|
||||||
|
}
|
||||||
|
// Settings that influence how windows are positioned and sized.
|
||||||
|
// Find more information on the wiki:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
||||||
|
layout {
|
||||||
|
// Set gaps around windows in logical pixels.
|
||||||
|
gaps 5
|
||||||
|
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
|
||||||
|
}
|
||||||
|
focus-ring {
|
||||||
|
width 2
|
||||||
|
active-color "#808080" // Medium gray
|
||||||
|
inactive-color "#505050" // Dark gray
|
||||||
|
}
|
||||||
|
shadow {
|
||||||
|
softness 30
|
||||||
|
spread 5
|
||||||
|
offset x=0 y=5
|
||||||
|
color "#0007"
|
||||||
|
}
|
||||||
|
struts {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layer-rule {
|
||||||
|
match namespace="^quickshell$"
|
||||||
|
place-within-backdrop true
|
||||||
|
}
|
||||||
|
overview {
|
||||||
|
workspace-shadow {
|
||||||
|
off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add lines like this to spawn processes at startup.
|
||||||
|
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||||
|
// which may be more convenient to use.
|
||||||
|
// See the binds section below for more spawn examples.
|
||||||
|
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||||
|
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
||||||
|
spawn-at-startup "dms" "run"
|
||||||
|
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
||||||
|
environment {
|
||||||
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
|
QT_QPA_PLATFORM "wayland"
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||||
|
QT_QPA_PLATFORMTHEME "gtk3"
|
||||||
|
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
||||||
|
TERMINAL "{{TERMINAL_COMMAND}}"
|
||||||
|
}
|
||||||
|
hotkey-overlay {
|
||||||
|
skip-at-startup
|
||||||
|
}
|
||||||
|
prefer-no-csd
|
||||||
|
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
||||||
|
animations {
|
||||||
|
workspace-switch {
|
||||||
|
spring damping-ratio=0.80 stiffness=523 epsilon=0.0001
|
||||||
|
}
|
||||||
|
window-open {
|
||||||
|
duration-ms 150
|
||||||
|
curve "ease-out-expo"
|
||||||
|
}
|
||||||
|
window-close {
|
||||||
|
duration-ms 150
|
||||||
|
curve "ease-out-quad"
|
||||||
|
}
|
||||||
|
horizontal-view-movement {
|
||||||
|
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
|
||||||
|
}
|
||||||
|
window-movement {
|
||||||
|
spring damping-ratio=0.75 stiffness=323 epsilon=0.0001
|
||||||
|
}
|
||||||
|
window-resize {
|
||||||
|
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
|
||||||
|
}
|
||||||
|
config-notification-open-close {
|
||||||
|
spring damping-ratio=0.65 stiffness=923 epsilon=0.001
|
||||||
|
}
|
||||||
|
screenshot-ui-open {
|
||||||
|
duration-ms 200
|
||||||
|
curve "ease-out-quad"
|
||||||
|
}
|
||||||
|
overview-open-close {
|
||||||
|
spring damping-ratio=0.85 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Window rules let you adjust behavior for individual windows.
|
||||||
|
// Find more information on the wiki:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
|
||||||
|
// Work around WezTerm's initial configure bug
|
||||||
|
// by setting an empty default-column-width.
|
||||||
|
window-rule {
|
||||||
|
// This regular expression is intentionally made as specific as possible,
|
||||||
|
// since this is the default config, and we want no false positives.
|
||||||
|
// You can get away with just app-id="wezterm" if you want.
|
||||||
|
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||||
|
default-column-width {}
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^org\.gnome\."#
|
||||||
|
draw-border-with-background false
|
||||||
|
geometry-corner-radius 12
|
||||||
|
clip-to-geometry true
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^gnome-control-center$"#
|
||||||
|
match app-id=r#"^pavucontrol$"#
|
||||||
|
match app-id=r#"^nm-connection-editor$"#
|
||||||
|
default-column-width { proportion 0.5; }
|
||||||
|
open-floating false
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^gnome-calculator$"#
|
||||||
|
match app-id=r#"^galculator$"#
|
||||||
|
match app-id=r#"^blueman-manager$"#
|
||||||
|
match app-id=r#"^org\.gnome\.Nautilus$"#
|
||||||
|
match app-id=r#"^steam$"#
|
||||||
|
match app-id=r#"^xdg-desktop-portal$"#
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||||
|
match app-id="Alacritty"
|
||||||
|
match app-id="zen"
|
||||||
|
match app-id="com.mitchellh.ghostty"
|
||||||
|
match app-id="kitty"
|
||||||
|
draw-border-with-background false
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
match is-active=false
|
||||||
|
opacity 0.9
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
||||||
|
match app-id="zoom"
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
geometry-corner-radius 12
|
||||||
|
clip-to-geometry true
|
||||||
|
}
|
||||||
|
// Open dms windows as floating by default
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"org.quickshell$"#
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
binds {
|
||||||
|
// === System & Overview ===
|
||||||
|
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
|
||||||
|
Mod+Tab repeat=false { toggle-overview; }
|
||||||
|
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||||
|
|
||||||
|
// === Application Launchers ===
|
||||||
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
|
||||||
|
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||||
|
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
||||||
|
}
|
||||||
|
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
||||||
|
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
||||||
|
}
|
||||||
|
Mod+M hotkey-overlay-title="Task Manager" {
|
||||||
|
spawn "dms" "ipc" "call" "processlist" "toggle";
|
||||||
|
}
|
||||||
|
Mod+Comma hotkey-overlay-title="Settings" {
|
||||||
|
spawn "dms" "ipc" "call" "settings" "toggle";
|
||||||
|
}
|
||||||
|
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||||
|
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
||||||
|
}
|
||||||
|
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
||||||
|
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
|
|
||||||
|
// === Security ===
|
||||||
|
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
|
||||||
|
spawn "dms" "ipc" "call" "lock" "lock";
|
||||||
|
}
|
||||||
|
Mod+Shift+E { quit; }
|
||||||
|
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
||||||
|
spawn "dms" "ipc" "call" "processlist" "toggle";
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Audio Controls ===
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
||||||
|
}
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
||||||
|
}
|
||||||
|
XF86AudioMute allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "mute";
|
||||||
|
}
|
||||||
|
XF86AudioMicMute allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Brightness Controls ===
|
||||||
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
|
||||||
|
}
|
||||||
|
XF86MonBrightnessDown allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Window Management ===
|
||||||
|
Mod+Q repeat=false { close-window; }
|
||||||
|
Mod+F { maximize-column; }
|
||||||
|
Mod+Shift+F { fullscreen-window; }
|
||||||
|
Mod+Shift+T { toggle-window-floating; }
|
||||||
|
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||||
|
Mod+W { toggle-column-tabbed-display; }
|
||||||
|
|
||||||
|
// === Focus Navigation ===
|
||||||
|
Mod+Left { focus-column-left; }
|
||||||
|
Mod+Down { focus-window-down; }
|
||||||
|
Mod+Up { focus-window-up; }
|
||||||
|
Mod+Right { focus-column-right; }
|
||||||
|
Mod+H { focus-column-left; }
|
||||||
|
Mod+J { focus-window-down; }
|
||||||
|
Mod+K { focus-window-up; }
|
||||||
|
Mod+L { focus-column-right; }
|
||||||
|
|
||||||
|
// === Window Movement ===
|
||||||
|
Mod+Shift+Left { move-column-left; }
|
||||||
|
Mod+Shift+Down { move-window-down; }
|
||||||
|
Mod+Shift+Up { move-window-up; }
|
||||||
|
Mod+Shift+Right { move-column-right; }
|
||||||
|
Mod+Shift+H { move-column-left; }
|
||||||
|
Mod+Shift+J { move-window-down; }
|
||||||
|
Mod+Shift+K { move-window-up; }
|
||||||
|
Mod+Shift+L { move-column-right; }
|
||||||
|
|
||||||
|
// === Column Navigation ===
|
||||||
|
Mod+Home { focus-column-first; }
|
||||||
|
Mod+End { focus-column-last; }
|
||||||
|
Mod+Ctrl+Home { move-column-to-first; }
|
||||||
|
Mod+Ctrl+End { move-column-to-last; }
|
||||||
|
|
||||||
|
// === Monitor Navigation ===
|
||||||
|
Mod+Ctrl+Left { focus-monitor-left; }
|
||||||
|
//Mod+Ctrl+Down { focus-monitor-down; }
|
||||||
|
//Mod+Ctrl+Up { focus-monitor-up; }
|
||||||
|
Mod+Ctrl+Right { focus-monitor-right; }
|
||||||
|
Mod+Ctrl+H { focus-monitor-left; }
|
||||||
|
Mod+Ctrl+J { focus-monitor-down; }
|
||||||
|
Mod+Ctrl+K { focus-monitor-up; }
|
||||||
|
Mod+Ctrl+L { focus-monitor-right; }
|
||||||
|
|
||||||
|
// === Move to Monitor ===
|
||||||
|
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
||||||
|
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||||
|
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||||
|
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
||||||
|
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
|
||||||
|
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
|
||||||
|
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
|
||||||
|
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
|
||||||
|
|
||||||
|
// === Workspace Navigation ===
|
||||||
|
Mod+Page_Down { focus-workspace-down; }
|
||||||
|
Mod+Page_Up { focus-workspace-up; }
|
||||||
|
Mod+U { focus-workspace-down; }
|
||||||
|
Mod+I { focus-workspace-up; }
|
||||||
|
Mod+Ctrl+Down { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+Up { move-column-to-workspace-up; }
|
||||||
|
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
// === Move Workspaces ===
|
||||||
|
Mod+Shift+Page_Down { move-workspace-down; }
|
||||||
|
Mod+Shift+Page_Up { move-workspace-up; }
|
||||||
|
Mod+Shift+U { move-workspace-down; }
|
||||||
|
Mod+Shift+I { move-workspace-up; }
|
||||||
|
|
||||||
|
// === Mouse Wheel Navigation ===
|
||||||
|
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||||
|
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
||||||
|
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
Mod+WheelScrollRight { focus-column-right; }
|
||||||
|
Mod+WheelScrollLeft { focus-column-left; }
|
||||||
|
Mod+Ctrl+WheelScrollRight { move-column-right; }
|
||||||
|
Mod+Ctrl+WheelScrollLeft { move-column-left; }
|
||||||
|
|
||||||
|
Mod+Shift+WheelScrollDown { focus-column-right; }
|
||||||
|
Mod+Shift+WheelScrollUp { focus-column-left; }
|
||||||
|
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
|
||||||
|
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
|
||||||
|
|
||||||
|
// === Numbered Workspaces ===
|
||||||
|
Mod+1 { focus-workspace 1; }
|
||||||
|
Mod+2 { focus-workspace 2; }
|
||||||
|
Mod+3 { focus-workspace 3; }
|
||||||
|
Mod+4 { focus-workspace 4; }
|
||||||
|
Mod+5 { focus-workspace 5; }
|
||||||
|
Mod+6 { focus-workspace 6; }
|
||||||
|
Mod+7 { focus-workspace 7; }
|
||||||
|
Mod+8 { focus-workspace 8; }
|
||||||
|
Mod+9 { focus-workspace 9; }
|
||||||
|
|
||||||
|
// === Move to Numbered Workspaces ===
|
||||||
|
Mod+Shift+1 { move-column-to-workspace 1; }
|
||||||
|
Mod+Shift+2 { move-column-to-workspace 2; }
|
||||||
|
Mod+Shift+3 { move-column-to-workspace 3; }
|
||||||
|
Mod+Shift+4 { move-column-to-workspace 4; }
|
||||||
|
Mod+Shift+5 { move-column-to-workspace 5; }
|
||||||
|
Mod+Shift+6 { move-column-to-workspace 6; }
|
||||||
|
Mod+Shift+7 { move-column-to-workspace 7; }
|
||||||
|
Mod+Shift+8 { move-column-to-workspace 8; }
|
||||||
|
Mod+Shift+9 { move-column-to-workspace 9; }
|
||||||
|
|
||||||
|
// === Column Management ===
|
||||||
|
Mod+BracketLeft { consume-or-expel-window-left; }
|
||||||
|
Mod+BracketRight { consume-or-expel-window-right; }
|
||||||
|
Mod+Period { expel-window-from-column; }
|
||||||
|
|
||||||
|
// === Sizing & Layout ===
|
||||||
|
Mod+R { switch-preset-column-width; }
|
||||||
|
Mod+Shift+R { switch-preset-window-height; }
|
||||||
|
Mod+Ctrl+R { reset-window-height; }
|
||||||
|
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||||
|
Mod+C { center-column; }
|
||||||
|
Mod+Ctrl+C { center-visible-columns; }
|
||||||
|
|
||||||
|
// === Manual Sizing ===
|
||||||
|
Mod+Minus { set-column-width "-10%"; }
|
||||||
|
Mod+Equal { set-column-width "+10%"; }
|
||||||
|
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||||
|
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||||
|
|
||||||
|
// === Screenshots ===
|
||||||
|
XF86Launch1 { screenshot; }
|
||||||
|
Ctrl+XF86Launch1 { screenshot-screen; }
|
||||||
|
Alt+XF86Launch1 { screenshot-window; }
|
||||||
|
Print { screenshot; }
|
||||||
|
Ctrl+Print { screenshot-screen; }
|
||||||
|
Alt+Print { screenshot-window; }
|
||||||
|
// === System Controls ===
|
||||||
|
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||||
|
Mod+Shift+P { power-off-monitors; }
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
honor-xdg-activation-with-invalid-serial
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed embedded/hyprland.conf
|
||||||
|
var HyprlandConfig string
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed embedded/niri.kdl
|
||||||
|
var NiriConfig string
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed embedded/ghostty.conf
|
||||||
|
var GhosttyConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/ghostty-colors.conf
|
||||||
|
var GhosttyColorConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/kitty.conf
|
||||||
|
var KittyConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/kitty-theme.conf
|
||||||
|
var KittyThemeConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/kitty-tabs.conf
|
||||||
|
var KittyTabsConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/alacritty.toml
|
||||||
|
var AlacrittyConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/alacritty-theme.toml
|
||||||
|
var AlacrittyThemeConfig string
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
package dank16
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RGB struct {
|
||||||
|
R, G, B float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type HSV struct {
|
||||||
|
H, S, V float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func HexToRGB(hex string) RGB {
|
||||||
|
if hex[0] == '#' {
|
||||||
|
hex = hex[1:]
|
||||||
|
}
|
||||||
|
var r, g, b uint8
|
||||||
|
fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b)
|
||||||
|
return RGB{
|
||||||
|
R: float64(r) / 255.0,
|
||||||
|
G: float64(g) / 255.0,
|
||||||
|
B: float64(b) / 255.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RGBToHex(rgb RGB) string {
|
||||||
|
r := math.Max(0, math.Min(1, rgb.R))
|
||||||
|
g := math.Max(0, math.Min(1, rgb.G))
|
||||||
|
b := math.Max(0, math.Min(1, rgb.B))
|
||||||
|
return fmt.Sprintf("#%02x%02x%02x", int(r*255), int(g*255), int(b*255))
|
||||||
|
}
|
||||||
|
|
||||||
|
func RGBToHSV(rgb RGB) HSV {
|
||||||
|
max := math.Max(math.Max(rgb.R, rgb.G), rgb.B)
|
||||||
|
min := math.Min(math.Min(rgb.R, rgb.G), rgb.B)
|
||||||
|
delta := max - min
|
||||||
|
|
||||||
|
var h float64
|
||||||
|
if delta == 0 {
|
||||||
|
h = 0
|
||||||
|
} else if max == rgb.R {
|
||||||
|
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
|
||||||
|
} else if max == rgb.G {
|
||||||
|
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
|
||||||
|
} else {
|
||||||
|
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if h < 0 {
|
||||||
|
h += 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var s float64
|
||||||
|
if max == 0 {
|
||||||
|
s = 0
|
||||||
|
} else {
|
||||||
|
s = delta / max
|
||||||
|
}
|
||||||
|
|
||||||
|
return HSV{H: h, S: s, V: max}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HSVToRGB(hsv HSV) RGB {
|
||||||
|
h := hsv.H * 6.0
|
||||||
|
c := hsv.V * hsv.S
|
||||||
|
x := c * (1.0 - math.Abs(math.Mod(h, 2.0)-1.0))
|
||||||
|
m := hsv.V - c
|
||||||
|
|
||||||
|
var r, g, b float64
|
||||||
|
switch int(h) {
|
||||||
|
case 0:
|
||||||
|
r, g, b = c, x, 0
|
||||||
|
case 1:
|
||||||
|
r, g, b = x, c, 0
|
||||||
|
case 2:
|
||||||
|
r, g, b = 0, c, x
|
||||||
|
case 3:
|
||||||
|
r, g, b = 0, x, c
|
||||||
|
case 4:
|
||||||
|
r, g, b = x, 0, c
|
||||||
|
case 5:
|
||||||
|
r, g, b = c, 0, x
|
||||||
|
default:
|
||||||
|
r, g, b = c, 0, x
|
||||||
|
}
|
||||||
|
|
||||||
|
return RGB{R: r + m, G: g + m, B: b + m}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sRGBToLinear(c float64) float64 {
|
||||||
|
if c <= 0.04045 {
|
||||||
|
return c / 12.92
|
||||||
|
}
|
||||||
|
return math.Pow((c+0.055)/1.055, 2.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Luminance(hex string) float64 {
|
||||||
|
rgb := HexToRGB(hex)
|
||||||
|
return 0.2126*sRGBToLinear(rgb.R) + 0.7152*sRGBToLinear(rgb.G) + 0.0722*sRGBToLinear(rgb.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContrastRatio(hexFg, hexBg string) float64 {
|
||||||
|
lumFg := Luminance(hexFg)
|
||||||
|
lumBg := Luminance(hexBg)
|
||||||
|
lighter := math.Max(lumFg, lumBg)
|
||||||
|
darker := math.Min(lumFg, lumBg)
|
||||||
|
return (lighter + 0.05) / (darker + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLstar(hex string) float64 {
|
||||||
|
rgb := HexToRGB(hex)
|
||||||
|
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
||||||
|
L, _, _ := col.Lab()
|
||||||
|
return L * 100.0 // go-colorful uses 0-1, we need 0-100 for DPS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lab to hex, clamping if needed
|
||||||
|
func labToHex(L, a, b float64) string {
|
||||||
|
c := colorful.Lab(L/100.0, a, b) // back to 0-1 for go-colorful
|
||||||
|
r, g, b2 := c.Clamped().RGB255()
|
||||||
|
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust brightness while keeping the same hue
|
||||||
|
func retoneToL(hex string, Ltarget float64) string {
|
||||||
|
rgb := HexToRGB(hex)
|
||||||
|
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
||||||
|
L, a, b := col.Lab()
|
||||||
|
L100 := L * 100.0
|
||||||
|
|
||||||
|
scale := 1.0
|
||||||
|
if L100 != 0 {
|
||||||
|
scale = Ltarget / L100
|
||||||
|
}
|
||||||
|
|
||||||
|
a2, b2 := a*scale, b*scale
|
||||||
|
|
||||||
|
// Don't let it get too saturated
|
||||||
|
maxChroma := 0.4
|
||||||
|
if math.Hypot(a2, b2) > maxChroma {
|
||||||
|
k := maxChroma / math.Hypot(a2, b2)
|
||||||
|
a2 *= k
|
||||||
|
b2 *= k
|
||||||
|
}
|
||||||
|
|
||||||
|
return labToHex(Ltarget, a2, b2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
||||||
|
Lf := getLstar(hexFg)
|
||||||
|
Lb := getLstar(hexBg)
|
||||||
|
|
||||||
|
phi := 1.618
|
||||||
|
inv := 0.618
|
||||||
|
lc := math.Pow(math.Abs(math.Pow(Lb, phi)-math.Pow(Lf, phi)), inv)*1.414 - 40
|
||||||
|
|
||||||
|
if negativePolarity {
|
||||||
|
lc += 5
|
||||||
|
}
|
||||||
|
|
||||||
|
return lc
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeltaPhiStarContrast(hexFg, hexBg string, isLightMode bool) float64 {
|
||||||
|
negativePolarity := !isLightMode
|
||||||
|
return DeltaPhiStar(hexFg, hexBg, negativePolarity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureContrast(hexColor, hexBg string, minRatio float64, isLightMode bool) string {
|
||||||
|
currentRatio := ContrastRatio(hexColor, hexBg)
|
||||||
|
if currentRatio >= minRatio {
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb := HexToRGB(hexColor)
|
||||||
|
hsv := RGBToHSV(rgb)
|
||||||
|
|
||||||
|
for step := 1; step < 30; step++ {
|
||||||
|
delta := float64(step) * 0.02
|
||||||
|
|
||||||
|
if isLightMode {
|
||||||
|
newV := math.Max(0, hsv.V-delta)
|
||||||
|
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||||
|
if ContrastRatio(candidate, hexBg) >= minRatio {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
newV = math.Min(1, hsv.V+delta)
|
||||||
|
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||||
|
if ContrastRatio(candidate, hexBg) >= minRatio {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newV := math.Min(1, hsv.V+delta)
|
||||||
|
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||||
|
if ContrastRatio(candidate, hexBg) >= minRatio {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
newV = math.Max(0, hsv.V-delta)
|
||||||
|
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||||
|
if ContrastRatio(candidate, hexBg) >= minRatio {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureContrastDPS(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||||
|
currentLc := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||||
|
if currentLc >= minLc {
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb := HexToRGB(hexColor)
|
||||||
|
hsv := RGBToHSV(rgb)
|
||||||
|
|
||||||
|
for step := 1; step < 50; step++ {
|
||||||
|
delta := float64(step) * 0.015
|
||||||
|
|
||||||
|
if isLightMode {
|
||||||
|
newV := math.Max(0, hsv.V-delta)
|
||||||
|
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||||
|
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
newV = math.Min(1, hsv.V+delta)
|
||||||
|
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||||
|
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newV := math.Min(1, hsv.V+delta)
|
||||||
|
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||||
|
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
newV = math.Max(0, hsv.V-delta)
|
||||||
|
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||||
|
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nudge L* until contrast is good enough. Keeps hue intact unlike HSV fiddling.
|
||||||
|
func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||||
|
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||||
|
if current >= minLc {
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fg := HexToRGB(hexColor)
|
||||||
|
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||||
|
Lf, af, bf := cf.Lab()
|
||||||
|
|
||||||
|
dir := 1.0
|
||||||
|
if isLightMode {
|
||||||
|
dir = -1.0 // light mode = darker text
|
||||||
|
}
|
||||||
|
|
||||||
|
step := 0.5
|
||||||
|
for i := 0; i < 120; i++ {
|
||||||
|
Lf = math.Max(0, math.Min(100, Lf+dir*step))
|
||||||
|
cand := labToHex(Lf, af, bf)
|
||||||
|
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||||
|
return cand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaletteOptions struct {
|
||||||
|
IsLight bool
|
||||||
|
Background string
|
||||||
|
UseDPS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOptions) string {
|
||||||
|
if opts.UseDPS {
|
||||||
|
return EnsureContrastDPSLstar(hexColor, hexBg, target, opts.IsLight)
|
||||||
|
}
|
||||||
|
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeriveContainer(primary string, isLight bool) string {
|
||||||
|
rgb := HexToRGB(primary)
|
||||||
|
hsv := RGBToHSV(rgb)
|
||||||
|
|
||||||
|
if isLight {
|
||||||
|
containerV := math.Min(hsv.V*1.77, 1.0)
|
||||||
|
containerS := hsv.S * 0.32
|
||||||
|
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
|
||||||
|
}
|
||||||
|
containerV := hsv.V * 0.463
|
||||||
|
containerS := math.Min(hsv.S*1.834, 1.0)
|
||||||
|
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
|
||||||
|
baseColor := DeriveContainer(primaryColor, opts.IsLight)
|
||||||
|
|
||||||
|
rgb := HexToRGB(baseColor)
|
||||||
|
hsv := RGBToHSV(rgb)
|
||||||
|
|
||||||
|
palette := make([]string, 0, 16)
|
||||||
|
|
||||||
|
var normalTextTarget, secondaryTarget float64
|
||||||
|
if opts.UseDPS {
|
||||||
|
normalTextTarget = 40.0
|
||||||
|
secondaryTarget = 35.0
|
||||||
|
} else {
|
||||||
|
normalTextTarget = 4.5
|
||||||
|
secondaryTarget = 3.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var bgColor string
|
||||||
|
if opts.Background != "" {
|
||||||
|
bgColor = opts.Background
|
||||||
|
} else if opts.IsLight {
|
||||||
|
bgColor = "#f8f8f8"
|
||||||
|
} else {
|
||||||
|
bgColor = "#1a1a1a"
|
||||||
|
}
|
||||||
|
palette = append(palette, bgColor)
|
||||||
|
|
||||||
|
hueShift := (hsv.H - 0.6) * 0.12
|
||||||
|
satBoost := 1.15
|
||||||
|
|
||||||
|
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
||||||
|
var redColor string
|
||||||
|
if opts.IsLight {
|
||||||
|
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
||||||
|
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||||
|
} else {
|
||||||
|
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
||||||
|
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
||||||
|
var greenColor string
|
||||||
|
if opts.IsLight {
|
||||||
|
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
||||||
|
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||||
|
} else {
|
||||||
|
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
||||||
|
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
||||||
|
var yellowColor string
|
||||||
|
if opts.IsLight {
|
||||||
|
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
||||||
|
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||||
|
} else {
|
||||||
|
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
||||||
|
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
var blueColor string
|
||||||
|
if opts.IsLight {
|
||||||
|
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
||||||
|
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||||
|
} else {
|
||||||
|
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
||||||
|
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
magH := hsv.H - 0.03
|
||||||
|
if magH < 0 {
|
||||||
|
magH += 1.0
|
||||||
|
}
|
||||||
|
var magColor string
|
||||||
|
hr := HexToRGB(primaryColor)
|
||||||
|
hh := RGBToHSV(hr)
|
||||||
|
if opts.IsLight {
|
||||||
|
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
||||||
|
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||||
|
} else {
|
||||||
|
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
||||||
|
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
cyanH := hsv.H + 0.08
|
||||||
|
if cyanH > 1.0 {
|
||||||
|
cyanH -= 1.0
|
||||||
|
}
|
||||||
|
palette = append(palette, ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
if opts.IsLight {
|
||||||
|
palette = append(palette, "#1a1a1a")
|
||||||
|
palette = append(palette, "#2e2e2e")
|
||||||
|
} else {
|
||||||
|
palette = append(palette, "#abb2bf")
|
||||||
|
palette = append(palette, "#5c6370")
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.IsLight {
|
||||||
|
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||||
|
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||||
|
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||||
|
hr := HexToRGB(primaryColor)
|
||||||
|
hh := RGBToHSV(hr)
|
||||||
|
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
||||||
|
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||||
|
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||||
|
} else {
|
||||||
|
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||||
|
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||||
|
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||||
|
// Make it way brighter for type names in dark mode
|
||||||
|
brightBlue := retoneToL(primaryColor, 85.0)
|
||||||
|
palette = append(palette, brightBlue)
|
||||||
|
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||||
|
brightCyanH := hsv.H + 0.02
|
||||||
|
if brightCyanH > 1.0 {
|
||||||
|
brightCyanH -= 1.0
|
||||||
|
}
|
||||||
|
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
||||||
|
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.IsLight {
|
||||||
|
palette = append(palette, "#1a1a1a")
|
||||||
|
} else {
|
||||||
|
palette = append(palette, "#ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
return palette
|
||||||
|
}
|
||||||
@@ -0,0 +1,727 @@
|
|||||||
|
package dank16
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHexToRGB(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected RGB
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "black with hash",
|
||||||
|
input: "#000000",
|
||||||
|
expected: RGB{R: 0.0, G: 0.0, B: 0.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "white with hash",
|
||||||
|
input: "#ffffff",
|
||||||
|
expected: RGB{R: 1.0, G: 1.0, B: 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "red without hash",
|
||||||
|
input: "ff0000",
|
||||||
|
expected: RGB{R: 1.0, G: 0.0, B: 0.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "purple",
|
||||||
|
input: "#625690",
|
||||||
|
expected: RGB{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mid gray",
|
||||||
|
input: "#808080",
|
||||||
|
expected: RGB{R: 0.5019607843137255, G: 0.5019607843137255, B: 0.5019607843137255},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := HexToRGB(tt.input)
|
||||||
|
if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) {
|
||||||
|
t.Errorf("HexToRGB(%s) = %v, expected %v", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRGBToHex(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input RGB
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "black",
|
||||||
|
input: RGB{R: 0.0, G: 0.0, B: 0.0},
|
||||||
|
expected: "#000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "white",
|
||||||
|
input: RGB{R: 1.0, G: 1.0, B: 1.0},
|
||||||
|
expected: "#ffffff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "red",
|
||||||
|
input: RGB{R: 1.0, G: 0.0, B: 0.0},
|
||||||
|
expected: "#ff0000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clamping above 1.0",
|
||||||
|
input: RGB{R: 1.5, G: 0.5, B: 0.5},
|
||||||
|
expected: "#ff7f7f",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clamping below 0.0",
|
||||||
|
input: RGB{R: -0.5, G: 0.5, B: 0.5},
|
||||||
|
expected: "#007f7f",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := RGBToHex(tt.input)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("RGBToHex(%v) = %s, expected %s", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRGBToHSV(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input RGB
|
||||||
|
expected HSV
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "black",
|
||||||
|
input: RGB{R: 0.0, G: 0.0, B: 0.0},
|
||||||
|
expected: HSV{H: 0.0, S: 0.0, V: 0.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "white",
|
||||||
|
input: RGB{R: 1.0, G: 1.0, B: 1.0},
|
||||||
|
expected: HSV{H: 0.0, S: 0.0, V: 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "red",
|
||||||
|
input: RGB{R: 1.0, G: 0.0, B: 0.0},
|
||||||
|
expected: HSV{H: 0.0, S: 1.0, V: 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "green",
|
||||||
|
input: RGB{R: 0.0, G: 1.0, B: 0.0},
|
||||||
|
expected: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blue",
|
||||||
|
input: RGB{R: 0.0, G: 0.0, B: 1.0},
|
||||||
|
expected: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := RGBToHSV(tt.input)
|
||||||
|
if !floatEqual(result.H, tt.expected.H) || !floatEqual(result.S, tt.expected.S) || !floatEqual(result.V, tt.expected.V) {
|
||||||
|
t.Errorf("RGBToHSV(%v) = %v, expected %v", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHSVToRGB(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input HSV
|
||||||
|
expected RGB
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "black",
|
||||||
|
input: HSV{H: 0.0, S: 0.0, V: 0.0},
|
||||||
|
expected: RGB{R: 0.0, G: 0.0, B: 0.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "white",
|
||||||
|
input: HSV{H: 0.0, S: 0.0, V: 1.0},
|
||||||
|
expected: RGB{R: 1.0, G: 1.0, B: 1.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "red",
|
||||||
|
input: HSV{H: 0.0, S: 1.0, V: 1.0},
|
||||||
|
expected: RGB{R: 1.0, G: 0.0, B: 0.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "green",
|
||||||
|
input: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0},
|
||||||
|
expected: RGB{R: 0.0, G: 1.0, B: 0.0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blue",
|
||||||
|
input: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0},
|
||||||
|
expected: RGB{R: 0.0, G: 0.0, B: 1.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := HSVToRGB(tt.input)
|
||||||
|
if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) {
|
||||||
|
t.Errorf("HSVToRGB(%v) = %v, expected %v", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLuminance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "black",
|
||||||
|
input: "#000000",
|
||||||
|
expected: 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "white",
|
||||||
|
input: "#ffffff",
|
||||||
|
expected: 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "red",
|
||||||
|
input: "#ff0000",
|
||||||
|
expected: 0.2126,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "green",
|
||||||
|
input: "#00ff00",
|
||||||
|
expected: 0.7152,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "blue",
|
||||||
|
input: "#0000ff",
|
||||||
|
expected: 0.0722,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := Luminance(tt.input)
|
||||||
|
if !floatEqual(result, tt.expected) {
|
||||||
|
t.Errorf("Luminance(%s) = %f, expected %f", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContrastRatio(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fg string
|
||||||
|
bg string
|
||||||
|
expected float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "black on white",
|
||||||
|
fg: "#000000",
|
||||||
|
bg: "#ffffff",
|
||||||
|
expected: 21.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "white on black",
|
||||||
|
fg: "#ffffff",
|
||||||
|
bg: "#000000",
|
||||||
|
expected: 21.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same color",
|
||||||
|
fg: "#808080",
|
||||||
|
bg: "#808080",
|
||||||
|
expected: 1.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := ContrastRatio(tt.fg, tt.bg)
|
||||||
|
if !floatEqual(result, tt.expected) {
|
||||||
|
t.Errorf("ContrastRatio(%s, %s) = %f, expected %f", tt.fg, tt.bg, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureContrast(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
color string
|
||||||
|
bg string
|
||||||
|
minRatio float64
|
||||||
|
isLightMode bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "already sufficient contrast dark mode",
|
||||||
|
color: "#ffffff",
|
||||||
|
bg: "#000000",
|
||||||
|
minRatio: 4.5,
|
||||||
|
isLightMode: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "already sufficient contrast light mode",
|
||||||
|
color: "#000000",
|
||||||
|
bg: "#ffffff",
|
||||||
|
minRatio: 4.5,
|
||||||
|
isLightMode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "needs adjustment dark mode",
|
||||||
|
color: "#404040",
|
||||||
|
bg: "#1a1a1a",
|
||||||
|
minRatio: 4.5,
|
||||||
|
isLightMode: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "needs adjustment light mode",
|
||||||
|
color: "#c0c0c0",
|
||||||
|
bg: "#f8f8f8",
|
||||||
|
minRatio: 4.5,
|
||||||
|
isLightMode: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := EnsureContrast(tt.color, tt.bg, tt.minRatio, tt.isLightMode)
|
||||||
|
actualRatio := ContrastRatio(result, tt.bg)
|
||||||
|
if actualRatio < tt.minRatio {
|
||||||
|
t.Errorf("EnsureContrast(%s, %s, %f, %t) = %s with ratio %f, expected ratio >= %f",
|
||||||
|
tt.color, tt.bg, tt.minRatio, tt.isLightMode, result, actualRatio, tt.minRatio)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratePalette(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
base string
|
||||||
|
opts PaletteOptions
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dark theme default",
|
||||||
|
base: "#625690",
|
||||||
|
opts: PaletteOptions{IsLight: false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "light theme default",
|
||||||
|
base: "#625690",
|
||||||
|
opts: PaletteOptions{IsLight: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "light theme with custom background",
|
||||||
|
base: "#625690",
|
||||||
|
opts: PaletteOptions{
|
||||||
|
IsLight: true,
|
||||||
|
Background: "#fafafa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dark theme with custom background",
|
||||||
|
base: "#625690",
|
||||||
|
opts: PaletteOptions{
|
||||||
|
IsLight: false,
|
||||||
|
Background: "#0a0a0a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := GeneratePalette(tt.base, tt.opts)
|
||||||
|
|
||||||
|
if len(result) != 16 {
|
||||||
|
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, color := range result {
|
||||||
|
if len(color) != 7 || color[0] != '#' {
|
||||||
|
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.opts.Background != "" && result[0] != tt.opts.Background {
|
||||||
|
t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background)
|
||||||
|
} else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" {
|
||||||
|
t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0])
|
||||||
|
} else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" {
|
||||||
|
t.Errorf("Light mode background = %s, expected #f8f8f8", result[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.opts.IsLight && result[15] != "#1a1a1a" {
|
||||||
|
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15])
|
||||||
|
} else if !tt.opts.IsLight && result[15] != "#ffffff" {
|
||||||
|
t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnrichVSCodeTheme(t *testing.T) {
|
||||||
|
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
|
||||||
|
|
||||||
|
baseTheme := map[string]interface{}{
|
||||||
|
"name": "Test Theme",
|
||||||
|
"type": "dark",
|
||||||
|
"colors": map[string]interface{}{
|
||||||
|
"editor.background": "#000000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
themeJSON, err := json.Marshal(baseTheme)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal base theme: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := EnrichVSCodeTheme(themeJSON, colors)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EnrichVSCodeTheme failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var enriched map[string]interface{}
|
||||||
|
if err := json.Unmarshal(result, &enriched); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
colorsMap, ok := enriched["colors"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("colors is not a map")
|
||||||
|
}
|
||||||
|
|
||||||
|
terminalColors := []string{
|
||||||
|
"terminal.ansiBlack",
|
||||||
|
"terminal.ansiRed",
|
||||||
|
"terminal.ansiGreen",
|
||||||
|
"terminal.ansiYellow",
|
||||||
|
"terminal.ansiBlue",
|
||||||
|
"terminal.ansiMagenta",
|
||||||
|
"terminal.ansiCyan",
|
||||||
|
"terminal.ansiWhite",
|
||||||
|
"terminal.ansiBrightBlack",
|
||||||
|
"terminal.ansiBrightRed",
|
||||||
|
"terminal.ansiBrightGreen",
|
||||||
|
"terminal.ansiBrightYellow",
|
||||||
|
"terminal.ansiBrightBlue",
|
||||||
|
"terminal.ansiBrightMagenta",
|
||||||
|
"terminal.ansiBrightCyan",
|
||||||
|
"terminal.ansiBrightWhite",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, key := range terminalColors {
|
||||||
|
if val, ok := colorsMap[key]; !ok {
|
||||||
|
t.Errorf("Missing terminal color: %s", key)
|
||||||
|
} else if val != colors[i] {
|
||||||
|
t.Errorf("%s = %s, expected %s", key, val, colors[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if colorsMap["editor.background"] != "#000000" {
|
||||||
|
t.Error("Original theme colors should be preserved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnrichVSCodeThemeInvalidJSON(t *testing.T) {
|
||||||
|
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
|
||||||
|
invalidJSON := []byte("{invalid json")
|
||||||
|
|
||||||
|
_, err := EnrichVSCodeTheme(invalidJSON, colors)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for invalid JSON, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoundTripConversion(t *testing.T) {
|
||||||
|
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
|
||||||
|
|
||||||
|
for _, hex := range testColors {
|
||||||
|
t.Run(hex, func(t *testing.T) {
|
||||||
|
rgb := HexToRGB(hex)
|
||||||
|
result := RGBToHex(rgb)
|
||||||
|
if result != hex {
|
||||||
|
t.Errorf("Round trip %s -> RGB -> %s failed", hex, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRGBHSVRoundTrip(t *testing.T) {
|
||||||
|
testCases := []RGB{
|
||||||
|
{R: 0.0, G: 0.0, B: 0.0},
|
||||||
|
{R: 1.0, G: 1.0, B: 1.0},
|
||||||
|
{R: 1.0, G: 0.0, B: 0.0},
|
||||||
|
{R: 0.0, G: 1.0, B: 0.0},
|
||||||
|
{R: 0.0, G: 0.0, B: 1.0},
|
||||||
|
{R: 0.5, G: 0.5, B: 0.5},
|
||||||
|
{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rgb := range testCases {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
hsv := RGBToHSV(rgb)
|
||||||
|
result := HSVToRGB(hsv)
|
||||||
|
if !floatEqual(result.R, rgb.R) || !floatEqual(result.G, rgb.G) || !floatEqual(result.B, rgb.B) {
|
||||||
|
t.Errorf("Round trip RGB->HSV->RGB failed: %v -> %v -> %v", rgb, hsv, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatEqual(a, b float64) bool {
|
||||||
|
return math.Abs(a-b) < 1e-9
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeltaPhiStar(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fg string
|
||||||
|
bg string
|
||||||
|
negativePolarity bool
|
||||||
|
minExpected float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "white on black (negative polarity)",
|
||||||
|
fg: "#ffffff",
|
||||||
|
bg: "#000000",
|
||||||
|
negativePolarity: true,
|
||||||
|
minExpected: 100.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "black on white (positive polarity)",
|
||||||
|
fg: "#000000",
|
||||||
|
bg: "#ffffff",
|
||||||
|
negativePolarity: false,
|
||||||
|
minExpected: 100.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "low contrast same color",
|
||||||
|
fg: "#808080",
|
||||||
|
bg: "#808080",
|
||||||
|
negativePolarity: false,
|
||||||
|
minExpected: -40.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := DeltaPhiStar(tt.fg, tt.bg, tt.negativePolarity)
|
||||||
|
if result < tt.minExpected {
|
||||||
|
t.Errorf("DeltaPhiStar(%s, %s, %v) = %f, expected >= %f",
|
||||||
|
tt.fg, tt.bg, tt.negativePolarity, result, tt.minExpected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeltaPhiStarContrast(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fg string
|
||||||
|
bg string
|
||||||
|
isLightMode bool
|
||||||
|
minExpected float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "white on black (dark mode)",
|
||||||
|
fg: "#ffffff",
|
||||||
|
bg: "#000000",
|
||||||
|
isLightMode: false,
|
||||||
|
minExpected: 100.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "black on white (light mode)",
|
||||||
|
fg: "#000000",
|
||||||
|
bg: "#ffffff",
|
||||||
|
isLightMode: true,
|
||||||
|
minExpected: 100.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := DeltaPhiStarContrast(tt.fg, tt.bg, tt.isLightMode)
|
||||||
|
if result < tt.minExpected {
|
||||||
|
t.Errorf("DeltaPhiStarContrast(%s, %s, %v) = %f, expected >= %f",
|
||||||
|
tt.fg, tt.bg, tt.isLightMode, result, tt.minExpected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureContrastDPS(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
color string
|
||||||
|
bg string
|
||||||
|
minLc float64
|
||||||
|
isLightMode bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "already sufficient contrast dark mode",
|
||||||
|
color: "#ffffff",
|
||||||
|
bg: "#000000",
|
||||||
|
minLc: 60.0,
|
||||||
|
isLightMode: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "already sufficient contrast light mode",
|
||||||
|
color: "#000000",
|
||||||
|
bg: "#ffffff",
|
||||||
|
minLc: 60.0,
|
||||||
|
isLightMode: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "needs adjustment dark mode",
|
||||||
|
color: "#404040",
|
||||||
|
bg: "#1a1a1a",
|
||||||
|
minLc: 60.0,
|
||||||
|
isLightMode: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "needs adjustment light mode",
|
||||||
|
color: "#c0c0c0",
|
||||||
|
bg: "#f8f8f8",
|
||||||
|
minLc: 60.0,
|
||||||
|
isLightMode: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := EnsureContrastDPS(tt.color, tt.bg, tt.minLc, tt.isLightMode)
|
||||||
|
actualLc := DeltaPhiStarContrast(result, tt.bg, tt.isLightMode)
|
||||||
|
if actualLc < tt.minLc {
|
||||||
|
t.Errorf("EnsureContrastDPS(%s, %s, %f, %t) = %s with Lc %f, expected Lc >= %f",
|
||||||
|
tt.color, tt.bg, tt.minLc, tt.isLightMode, result, actualLc, tt.minLc)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratePaletteWithDPS(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
base string
|
||||||
|
opts PaletteOptions
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dark theme with DPS",
|
||||||
|
base: "#625690",
|
||||||
|
opts: PaletteOptions{IsLight: false, UseDPS: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "light theme with DPS",
|
||||||
|
base: "#625690",
|
||||||
|
opts: PaletteOptions{IsLight: true, UseDPS: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := GeneratePalette(tt.base, tt.opts)
|
||||||
|
|
||||||
|
if len(result) != 16 {
|
||||||
|
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, color := range result {
|
||||||
|
if len(color) != 7 || color[0] != '#' {
|
||||||
|
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bgColor := result[0]
|
||||||
|
for i := 1; i < 8; i++ {
|
||||||
|
lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight)
|
||||||
|
minLc := 30.0
|
||||||
|
if lc < minLc && lc > 0 {
|
||||||
|
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
|
||||||
|
i, result[i], lc, bgColor, minLc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeriveContainer(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
primary string
|
||||||
|
isLight bool
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dark mode",
|
||||||
|
primary: "#ccbdff",
|
||||||
|
isLight: false,
|
||||||
|
expected: "#4a3e76",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "light mode",
|
||||||
|
primary: "#625690",
|
||||||
|
isLight: true,
|
||||||
|
expected: "#e7deff",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := DeriveContainer(tt.primary, tt.isLight)
|
||||||
|
|
||||||
|
resultRGB := HexToRGB(result)
|
||||||
|
expectedRGB := HexToRGB(tt.expected)
|
||||||
|
|
||||||
|
rDiff := math.Abs(resultRGB.R - expectedRGB.R)
|
||||||
|
gDiff := math.Abs(resultRGB.G - expectedRGB.G)
|
||||||
|
bDiff := math.Abs(resultRGB.B - expectedRGB.B)
|
||||||
|
|
||||||
|
tolerance := 0.02
|
||||||
|
if rDiff > tolerance || gDiff > tolerance || bDiff > tolerance {
|
||||||
|
t.Errorf("DeriveContainer(%s, %v) = %s, expected %s (RGB diff: R:%.4f G:%.4f B:%.4f)",
|
||||||
|
tt.primary, tt.isLight, result, tt.expected, rDiff, gDiff, bDiff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContrastAlgorithmComparison(t *testing.T) {
|
||||||
|
base := "#625690"
|
||||||
|
|
||||||
|
optsWCAG := PaletteOptions{IsLight: false, UseDPS: false}
|
||||||
|
optsDPS := PaletteOptions{IsLight: false, UseDPS: true}
|
||||||
|
|
||||||
|
paletteWCAG := GeneratePalette(base, optsWCAG)
|
||||||
|
paletteDPS := GeneratePalette(base, optsDPS)
|
||||||
|
|
||||||
|
if len(paletteWCAG) != 16 || len(paletteDPS) != 16 {
|
||||||
|
t.Fatal("Both palettes should have 16 colors")
|
||||||
|
}
|
||||||
|
|
||||||
|
if paletteWCAG[0] != paletteDPS[0] {
|
||||||
|
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
differentCount := 0
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
if paletteWCAG[i] != paletteDPS[i] {
|
||||||
|
differentCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package dank16
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateJSON(colors []string) string {
|
||||||
|
colorMap := make(map[string]string)
|
||||||
|
|
||||||
|
for i, color := range colors {
|
||||||
|
colorMap[fmt.Sprintf("color%d", i)] = color
|
||||||
|
}
|
||||||
|
|
||||||
|
marshalled, _ := json.Marshal(colorMap)
|
||||||
|
|
||||||
|
return string(marshalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateKittyTheme(colors []string) string {
|
||||||
|
kittyColors := []struct {
|
||||||
|
name string
|
||||||
|
index int
|
||||||
|
}{
|
||||||
|
{"color0", 0},
|
||||||
|
{"color1", 1},
|
||||||
|
{"color2", 2},
|
||||||
|
{"color3", 3},
|
||||||
|
{"color4", 4},
|
||||||
|
{"color5", 5},
|
||||||
|
{"color6", 6},
|
||||||
|
{"color7", 7},
|
||||||
|
{"color8", 8},
|
||||||
|
{"color9", 9},
|
||||||
|
{"color10", 10},
|
||||||
|
{"color11", 11},
|
||||||
|
{"color12", 12},
|
||||||
|
{"color13", 13},
|
||||||
|
{"color14", 14},
|
||||||
|
{"color15", 15},
|
||||||
|
}
|
||||||
|
|
||||||
|
var result strings.Builder
|
||||||
|
for _, kc := range kittyColors {
|
||||||
|
fmt.Fprintf(&result, "%s %s\n", kc.name, colors[kc.index])
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateFootTheme(colors []string) string {
|
||||||
|
footColors := []struct {
|
||||||
|
name string
|
||||||
|
index int
|
||||||
|
}{
|
||||||
|
{"regular0", 0},
|
||||||
|
{"regular1", 1},
|
||||||
|
{"regular2", 2},
|
||||||
|
{"regular3", 3},
|
||||||
|
{"regular4", 4},
|
||||||
|
{"regular5", 5},
|
||||||
|
{"regular6", 6},
|
||||||
|
{"regular7", 7},
|
||||||
|
{"bright0", 8},
|
||||||
|
{"bright1", 9},
|
||||||
|
{"bright2", 10},
|
||||||
|
{"bright3", 11},
|
||||||
|
{"bright4", 12},
|
||||||
|
{"bright5", 13},
|
||||||
|
{"bright6", 14},
|
||||||
|
{"bright7", 15},
|
||||||
|
}
|
||||||
|
|
||||||
|
var result strings.Builder
|
||||||
|
for _, fc := range footColors {
|
||||||
|
fmt.Fprintf(&result, "%s=%s\n", fc.name, strings.TrimPrefix(colors[fc.index], "#"))
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateAlacrittyTheme(colors []string) string {
|
||||||
|
alacrittyColors := []struct {
|
||||||
|
section string
|
||||||
|
name string
|
||||||
|
index int
|
||||||
|
}{
|
||||||
|
{"normal", "black", 0},
|
||||||
|
{"normal", "red", 1},
|
||||||
|
{"normal", "green", 2},
|
||||||
|
{"normal", "yellow", 3},
|
||||||
|
{"normal", "blue", 4},
|
||||||
|
{"normal", "magenta", 5},
|
||||||
|
{"normal", "cyan", 6},
|
||||||
|
{"normal", "white", 7},
|
||||||
|
{"bright", "black", 8},
|
||||||
|
{"bright", "red", 9},
|
||||||
|
{"bright", "green", 10},
|
||||||
|
{"bright", "yellow", 11},
|
||||||
|
{"bright", "blue", 12},
|
||||||
|
{"bright", "magenta", 13},
|
||||||
|
{"bright", "cyan", 14},
|
||||||
|
{"bright", "white", 15},
|
||||||
|
}
|
||||||
|
|
||||||
|
var result strings.Builder
|
||||||
|
currentSection := ""
|
||||||
|
for _, ac := range alacrittyColors {
|
||||||
|
if ac.section != currentSection {
|
||||||
|
if currentSection != "" {
|
||||||
|
result.WriteString("\n")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&result, "[colors.%s]\n", ac.section)
|
||||||
|
currentSection = ac.section
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&result, "%-7s = '%s'\n", ac.name, colors[ac.index])
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateGhosttyTheme(colors []string) string {
|
||||||
|
var result strings.Builder
|
||||||
|
for i, color := range colors {
|
||||||
|
fmt.Fprintf(&result, "palette = %d=%s\n", i, color)
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateWeztermTheme(colors []string) string {
|
||||||
|
var result strings.Builder
|
||||||
|
labels := []string{"ansi", "brights"}
|
||||||
|
for j, label := range labels {
|
||||||
|
start := j * 8
|
||||||
|
colorSlice := make([]string, 8)
|
||||||
|
for i, color := range colors[start : start+8] {
|
||||||
|
colorSlice[i] = fmt.Sprintf("'%s'", color)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&result, "%s = [%s]\n", label, strings.Join(colorSlice, ", "))
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package dank16
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VSCodeTheme struct {
|
||||||
|
Schema string `json:"$schema"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Colors map[string]string `json:"colors"`
|
||||||
|
TokenColors []VSCodeTokenColor `json:"tokenColors"`
|
||||||
|
SemanticHighlighting bool `json:"semanticHighlighting"`
|
||||||
|
SemanticTokenColors map[string]VSCodeTokenSetting `json:"semanticTokenColors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VSCodeTokenColor struct {
|
||||||
|
Scope interface{} `json:"scope"`
|
||||||
|
Settings VSCodeTokenSetting `json:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VSCodeTokenSetting struct {
|
||||||
|
Foreground string `json:"foreground,omitempty"`
|
||||||
|
FontStyle string `json:"fontStyle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTokenColor(tc interface{}, scopeToColor map[string]string) {
|
||||||
|
tcMap, ok := tc.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scopes, ok := tcMap["scope"].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, ok := tcMap["settings"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isYaml := hasScopeContaining(scopes, "yaml")
|
||||||
|
|
||||||
|
for _, scope := range scopes {
|
||||||
|
scopeStr, ok := scope.(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if scopeStr == "string" && isYaml {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if applyColorToScope(settings, scope, scopeToColor) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyColorToScope(settings map[string]interface{}, scope interface{}, scopeToColor map[string]string) bool {
|
||||||
|
scopeStr, ok := scope.(string)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
newColor, exists := scopeToColor[scopeStr]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
settings["foreground"] = newColor
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasScopeContaining(scopes []interface{}, substring string) bool {
|
||||||
|
for _, scope := range scopes {
|
||||||
|
scopeStr, ok := scope.(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i <= len(scopeStr)-len(substring); i++ {
|
||||||
|
if scopeStr[i:i+len(substring)] == substring {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnrichVSCodeTheme(themeData []byte, colors []string) ([]byte, error) {
|
||||||
|
var theme map[string]interface{}
|
||||||
|
if err := json.Unmarshal(themeData, &theme); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
colorsMap, ok := theme["colors"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
colorsMap = make(map[string]interface{})
|
||||||
|
theme["colors"] = colorsMap
|
||||||
|
}
|
||||||
|
|
||||||
|
bg := colors[0]
|
||||||
|
isLight := false
|
||||||
|
if len(bg) == 7 && bg[0] == '#' {
|
||||||
|
r, g, b := 0, 0, 0
|
||||||
|
fmt.Sscanf(bg[1:], "%02x%02x%02x", &r, &g, &b)
|
||||||
|
luminance := (0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 255.0
|
||||||
|
isLight = luminance > 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLight {
|
||||||
|
theme["type"] = "light"
|
||||||
|
} else {
|
||||||
|
theme["type"] = "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
colorsMap["terminal.ansiBlack"] = colors[0]
|
||||||
|
colorsMap["terminal.ansiRed"] = colors[1]
|
||||||
|
colorsMap["terminal.ansiGreen"] = colors[2]
|
||||||
|
colorsMap["terminal.ansiYellow"] = colors[3]
|
||||||
|
colorsMap["terminal.ansiBlue"] = colors[4]
|
||||||
|
colorsMap["terminal.ansiMagenta"] = colors[5]
|
||||||
|
colorsMap["terminal.ansiCyan"] = colors[6]
|
||||||
|
colorsMap["terminal.ansiWhite"] = colors[7]
|
||||||
|
colorsMap["terminal.ansiBrightBlack"] = colors[8]
|
||||||
|
colorsMap["terminal.ansiBrightRed"] = colors[9]
|
||||||
|
colorsMap["terminal.ansiBrightGreen"] = colors[10]
|
||||||
|
colorsMap["terminal.ansiBrightYellow"] = colors[11]
|
||||||
|
colorsMap["terminal.ansiBrightBlue"] = colors[12]
|
||||||
|
colorsMap["terminal.ansiBrightMagenta"] = colors[13]
|
||||||
|
colorsMap["terminal.ansiBrightCyan"] = colors[14]
|
||||||
|
colorsMap["terminal.ansiBrightWhite"] = colors[15]
|
||||||
|
|
||||||
|
tokenColors, ok := theme["tokenColors"].([]interface{})
|
||||||
|
if ok {
|
||||||
|
scopeToColor := map[string]string{
|
||||||
|
"comment": colors[8],
|
||||||
|
"punctuation.definition.comment": colors[8],
|
||||||
|
"keyword": colors[5],
|
||||||
|
"storage.type": colors[13],
|
||||||
|
"storage.modifier": colors[5],
|
||||||
|
"variable": colors[15],
|
||||||
|
"variable.parameter": colors[7],
|
||||||
|
"meta.object-literal.key": colors[4],
|
||||||
|
"meta.property.object": colors[4],
|
||||||
|
"variable.other.property": colors[4],
|
||||||
|
"constant.other.symbol": colors[12],
|
||||||
|
"constant.numeric": colors[12],
|
||||||
|
"constant.language": colors[12],
|
||||||
|
"constant.character": colors[3],
|
||||||
|
"entity.name.type": colors[12],
|
||||||
|
"support.type": colors[13],
|
||||||
|
"entity.name.class": colors[12],
|
||||||
|
"entity.name.function": colors[2],
|
||||||
|
"support.function": colors[2],
|
||||||
|
"support.class": colors[15],
|
||||||
|
"support.variable": colors[15],
|
||||||
|
"variable.language": colors[12],
|
||||||
|
"entity.name.tag.yaml": colors[12],
|
||||||
|
"string.unquoted.plain.out.yaml": colors[15],
|
||||||
|
"string.unquoted.yaml": colors[15],
|
||||||
|
"string": colors[3],
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range tokenColors {
|
||||||
|
updateTokenColor(tc, scopeToColor)
|
||||||
|
tokenColors[i] = tc
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlRules := []VSCodeTokenColor{
|
||||||
|
{
|
||||||
|
Scope: "entity.name.tag.yaml",
|
||||||
|
Settings: VSCodeTokenSetting{Foreground: colors[12]},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Scope: []string{"string.unquoted.plain.out.yaml", "string.unquoted.yaml"},
|
||||||
|
Settings: VSCodeTokenSetting{Foreground: colors[15]},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range yamlRules {
|
||||||
|
tokenColors = append(tokenColors, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
theme["tokenColors"] = tokenColors
|
||||||
|
}
|
||||||
|
|
||||||
|
if semanticTokenColors, ok := theme["semanticTokenColors"].(map[string]interface{}); ok {
|
||||||
|
updates := map[string]string{
|
||||||
|
"variable": colors[15],
|
||||||
|
"variable.readonly": colors[12],
|
||||||
|
"property": colors[4],
|
||||||
|
"function": colors[2],
|
||||||
|
"method": colors[2],
|
||||||
|
"type": colors[12],
|
||||||
|
"class": colors[12],
|
||||||
|
"typeParameter": colors[13],
|
||||||
|
"enumMember": colors[12],
|
||||||
|
"string": colors[3],
|
||||||
|
"number": colors[12],
|
||||||
|
"comment": colors[8],
|
||||||
|
"keyword": colors[5],
|
||||||
|
"operator": colors[15],
|
||||||
|
"parameter": colors[7],
|
||||||
|
"namespace": colors[15],
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, color := range updates {
|
||||||
|
if existing, ok := semanticTokenColors[key].(map[string]interface{}); ok {
|
||||||
|
existing["foreground"] = color
|
||||||
|
} else {
|
||||||
|
semanticTokenColors[key] = map[string]interface{}{
|
||||||
|
"foreground": color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
semanticTokenColors := make(map[string]interface{})
|
||||||
|
updates := map[string]string{
|
||||||
|
"variable": colors[7],
|
||||||
|
"variable.readonly": colors[12],
|
||||||
|
"property": colors[4],
|
||||||
|
"function": colors[2],
|
||||||
|
"method": colors[2],
|
||||||
|
"type": colors[12],
|
||||||
|
"class": colors[12],
|
||||||
|
"typeParameter": colors[13],
|
||||||
|
"enumMember": colors[12],
|
||||||
|
"string": colors[3],
|
||||||
|
"number": colors[12],
|
||||||
|
"comment": colors[8],
|
||||||
|
"keyword": colors[5],
|
||||||
|
"operator": colors[15],
|
||||||
|
"parameter": colors[7],
|
||||||
|
"namespace": colors[15],
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, color := range updates {
|
||||||
|
semanticTokenColors[key] = map[string]interface{}{
|
||||||
|
"foreground": color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
theme["semanticTokenColors"] = semanticTokenColors
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.MarshalIndent(theme, "", " ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package deps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DependencyStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusMissing DependencyStatus = iota
|
||||||
|
StatusInstalled
|
||||||
|
StatusNeedsUpdate
|
||||||
|
StatusNeedsReinstall
|
||||||
|
)
|
||||||
|
|
||||||
|
type PackageVariant int
|
||||||
|
|
||||||
|
const (
|
||||||
|
VariantStable PackageVariant = iota
|
||||||
|
VariantGit
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dependency struct {
|
||||||
|
Name string
|
||||||
|
Status DependencyStatus
|
||||||
|
Version string
|
||||||
|
Description string
|
||||||
|
Required bool
|
||||||
|
Variant PackageVariant
|
||||||
|
CanToggle bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowManager int
|
||||||
|
|
||||||
|
const (
|
||||||
|
WindowManagerHyprland WindowManager = iota
|
||||||
|
WindowManagerNiri
|
||||||
|
)
|
||||||
|
|
||||||
|
type Terminal int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TerminalGhostty Terminal = iota
|
||||||
|
TerminalKitty
|
||||||
|
TerminalAlacritty
|
||||||
|
)
|
||||||
|
|
||||||
|
type DependencyDetector interface {
|
||||||
|
DetectDependencies(ctx context.Context, wm WindowManager) ([]Dependency, error)
|
||||||
|
DetectDependenciesWithTerminal(ctx context.Context, wm WindowManager, terminal Terminal) ([]Dependency, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,785 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("arch", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("archarm", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("archcraft", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("manjaro", "#35BF5C", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("obarun", "#2494be", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArchDistribution struct {
|
||||||
|
*BaseDistribution
|
||||||
|
*ManualPackageInstaller
|
||||||
|
config DistroConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewArchDistribution(config DistroConfig, logChan chan<- string) *ArchDistribution {
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
return &ArchDistribution{
|
||||||
|
BaseDistribution: base,
|
||||||
|
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) GetID() string {
|
||||||
|
return a.config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) GetColorHex() string {
|
||||||
|
return a.config.ColorHex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) GetFamily() DistroFamily {
|
||||||
|
return a.config.Family
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) GetPackageManager() PackageManagerType {
|
||||||
|
return PackageManagerPacman
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||||
|
return a.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
// DMS at the top (shell is prominent)
|
||||||
|
dependencies = append(dependencies, a.detectDMS())
|
||||||
|
|
||||||
|
// Terminal with choice support
|
||||||
|
dependencies = append(dependencies, a.detectSpecificTerminal(terminal))
|
||||||
|
|
||||||
|
// Common detections using base methods
|
||||||
|
dependencies = append(dependencies, a.detectGit())
|
||||||
|
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||||
|
dependencies = append(dependencies, a.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, a.detectXDGPortal())
|
||||||
|
dependencies = append(dependencies, a.detectPolkitAgent())
|
||||||
|
dependencies = append(dependencies, a.detectAccountsService())
|
||||||
|
|
||||||
|
// Hyprland-specific tools
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
dependencies = append(dependencies, a.detectHyprlandTools()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Niri-specific tools
|
||||||
|
if wm == deps.WindowManagerNiri {
|
||||||
|
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
|
dependencies = append(dependencies, a.detectHyprpicker())
|
||||||
|
dependencies = append(dependencies, a.detectClipboardTools()...)
|
||||||
|
|
||||||
|
return dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if a.packageInstalled("xdg-desktop-portal-gtk") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xdg-desktop-portal-gtk",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop integration portal for GTK",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) detectPolkitAgent() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if a.packageInstalled("mate-polkit") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "mate-polkit",
|
||||||
|
Status: status,
|
||||||
|
Description: "PolicyKit authentication agent",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if a.packageInstalled("accountsservice") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "accountsservice",
|
||||||
|
Status: status,
|
||||||
|
Description: "D-Bus interface for user account query and manipulation",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||||
|
cmd := exec.Command("pacman", "-Q", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
|
packages := map[string]PackageMapping{
|
||||||
|
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||||
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
|
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||||
|
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
|
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||||
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
|
||||||
|
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||||
|
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||||
|
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
|
||||||
|
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||||
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
packages["niri"] = a.getNiriMapping(variants["niri"])
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "niri-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if runtime.GOARCH == "arm64" {
|
||||||
|
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceDMSGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.packageInstalled("dms-shell-git") {
|
||||||
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.packageInstalled("dms-shell-bin") {
|
||||||
|
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if a.commandExists("xwayland-satellite") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xwayland-satellite",
|
||||||
|
Status: status,
|
||||||
|
Description: "Xwayland support",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.06,
|
||||||
|
Step: "Checking base-devel...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Checking if base-devel is installed",
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCmd := exec.CommandContext(ctx, "pacman", "-Qq", "base-devel")
|
||||||
|
if err := checkCmd.Run(); err == nil {
|
||||||
|
a.log("base-devel already installed")
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.10,
|
||||||
|
Step: "base-devel already installed",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "base-devel is already installed on the system",
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.log("Installing base-devel...")
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.08,
|
||||||
|
Step: "Installing base-devel...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo pacman -S --needed --noconfirm base-devel",
|
||||||
|
LogOutput: "Installing base-devel development tools",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||||
|
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
||||||
|
return fmt.Errorf("failed to install base-devel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.12,
|
||||||
|
Step: "base-devel installation complete",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "base-devel successfully installed",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
// Phase 1: Check Prerequisites
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Checking system prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting prerequisite check...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
// Phase 3: System Packages
|
||||||
|
if len(systemPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install system packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: AUR Packages
|
||||||
|
if len(aurPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d AUR packages...", len(aurPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing AUR packages: %s", strings.Join(aurPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := a.installAURPackages(ctx, aurPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install AUR packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: Manual Builds
|
||||||
|
if len(manualPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.85,
|
||||||
|
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := a.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 6: Configuration
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Configuring system...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting post-installation configuration...",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 7: Complete
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseComplete,
|
||||||
|
Progress: 1.0,
|
||||||
|
Step: "Installation complete!",
|
||||||
|
IsComplete: true,
|
||||||
|
LogOutput: "All packages installed and configured successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, []string, map[string]deps.PackageVariant) {
|
||||||
|
systemPkgs := []string{}
|
||||||
|
aurPkgs := []string{}
|
||||||
|
manualPkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := a.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if disabledFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
|
if !exists {
|
||||||
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgInfo.Repository {
|
||||||
|
case RepoTypeAUR:
|
||||||
|
aurPkgs = append(aurPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeSystem:
|
||||||
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeManual:
|
||||||
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
hasNiri := false
|
||||||
|
hasQuickshell := false
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri-git" {
|
||||||
|
hasNiri = true
|
||||||
|
}
|
||||||
|
if pkg == "quickshell" || pkg == "quickshell-git" {
|
||||||
|
hasQuickshell = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If quickshell is in the list, always reinstall google-breakpad first
|
||||||
|
if hasQuickshell {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.63,
|
||||||
|
Step: "Reinstalling google-breakpad for quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
||||||
|
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||||
|
if hasNiri {
|
||||||
|
if !a.packageInstalled("makepkg-git-lfs-proto") {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: "Installing makepkg-git-lfs-proto for niri...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Installing prerequisite for niri-git",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.installSingleAURPackage(ctx, "makepkg-git-lfs-proto", sudoPassword, progressChan, 0.65, 0.67); err != nil {
|
||||||
|
return fmt.Errorf("failed to install makepkg-git-lfs-proto prerequisite for niri: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder packages to ensure dms-shell-git dependencies are installed first
|
||||||
|
orderedPackages := a.reorderAURPackages(packages)
|
||||||
|
|
||||||
|
baseProgress := 0.67
|
||||||
|
progressStep := 0.13 / float64(len(orderedPackages))
|
||||||
|
|
||||||
|
for i, pkg := range orderedPackages {
|
||||||
|
currentProgress := baseProgress + (float64(i) * progressStep)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: currentProgress,
|
||||||
|
Step: fmt.Sprintf("Installing AUR package %s (%d/%d)...", pkg, i+1, len(packages)),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("Building and installing %s", pkg),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.installSingleAURPackage(ctx, pkg, sudoPassword, progressChan, currentProgress, currentProgress+progressStep); err != nil {
|
||||||
|
return fmt.Errorf("failed to install AUR package %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.80,
|
||||||
|
Step: "All AUR packages installed successfully",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Successfully installed AUR packages: %s", strings.Join(packages, ", ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
||||||
|
dmsDepencies := []string{"quickshell", "quickshell-git", "dgop"}
|
||||||
|
|
||||||
|
var deps []string
|
||||||
|
var others []string
|
||||||
|
var dmsShell []string
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||||
|
dmsShell = append(dmsShell, pkg)
|
||||||
|
} else {
|
||||||
|
isDep := false
|
||||||
|
for _, dep := range dmsDepencies {
|
||||||
|
if pkg == dep {
|
||||||
|
deps = append(deps, pkg)
|
||||||
|
isDep = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isDep {
|
||||||
|
others = append(others, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := append(deps, others...)
|
||||||
|
result = append(result, dmsShell...)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "aur-builds", pkg)
|
||||||
|
|
||||||
|
// Clean up any existing cache first
|
||||||
|
if err := os.RemoveAll(buildDir); err != nil {
|
||||||
|
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if removeErr := os.RemoveAll(buildDir); removeErr != nil {
|
||||||
|
a.log(fmt.Sprintf("Warning: failed to cleanup build directory %s: %v", buildDir, removeErr))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Clone the AUR package
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.1*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Cloning %s from AUR...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("git clone https://aur.archlinux.org/%s.git", pkg),
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", fmt.Sprintf("https://aur.archlinux.org/%s.git", pkg), filepath.Join(buildDir, pkg))
|
||||||
|
if err := a.runWithProgress(cloneCmd, progressChan, PhaseAURPackages, startProgress+0.1*(endProgress-startProgress), startProgress+0.2*(endProgress-startProgress)); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
packageDir := filepath.Join(buildDir, pkg)
|
||||||
|
|
||||||
|
if pkg == "niri-git" {
|
||||||
|
pkgbuildPath := filepath.Join(packageDir, "PKGBUILD")
|
||||||
|
sedCmd := exec.CommandContext(ctx, "sed", "-i", "s/makepkg-git-lfs-proto//g", pkgbuildPath)
|
||||||
|
if err := sedCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to patch PKGBUILD for niri-git: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
|
sedCmd2 := exec.CommandContext(ctx, "sed", "-i", "/makedepends = makepkg-git-lfs-proto/d", srcinfoPath)
|
||||||
|
if err := sedCmd2.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to patch .SRCINFO for niri-git: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||||
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
|
depsToRemove := []string{
|
||||||
|
"depends = quickshell",
|
||||||
|
"depends = dgop",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dep := range depsToRemove {
|
||||||
|
sedCmd := exec.CommandContext(ctx, "sed", "-i", fmt.Sprintf("/%s/d", dep), srcinfoPath)
|
||||||
|
if err := sedCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove dependency %s from .SRCINFO for %s: %w", dep, pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all optdepends from .SRCINFO for all packages
|
||||||
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
|
optdepsCmd := exec.CommandContext(ctx, "sed", "-i", "/^[[:space:]]*optdepends = /d", srcinfoPath)
|
||||||
|
if err := optdepsCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
||||||
|
// since we manually manage those dependencies
|
||||||
|
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
||||||
|
// Pre-install dependencies from .SRCINFO
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Installing package dependencies and makedepends",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install dependencies and makedepends explicitly
|
||||||
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
|
|
||||||
|
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
|
fmt.Sprintf(`
|
||||||
|
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
|
if [[ "%s" == *"quickshell"* ]]; then
|
||||||
|
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
||||||
|
fi
|
||||||
|
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||||
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||||
|
fi
|
||||||
|
`, srcinfoPath, pkg, sudoPassword))
|
||||||
|
|
||||||
|
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
||||||
|
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
|
fmt.Sprintf(`
|
||||||
|
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
||||||
|
if [ ! -z "$makedeps" ]; then
|
||||||
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
||||||
|
fi
|
||||||
|
`, srcinfoPath, sudoPassword))
|
||||||
|
|
||||||
|
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
||||||
|
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.4*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Building %s...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "makepkg --noconfirm",
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||||
|
buildCmd.Dir = packageDir
|
||||||
|
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
|
||||||
|
|
||||||
|
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
||||||
|
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find built package file
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.7*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing %s...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "sudo pacman -U built-package",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
||||||
|
var files []string
|
||||||
|
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||||
|
// For DMS split packages, install base package
|
||||||
|
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err == nil {
|
||||||
|
for _, match := range matches {
|
||||||
|
basename := filepath.Base(match)
|
||||||
|
// Always include base package
|
||||||
|
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
||||||
|
files = append(files, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also update compositor-specific packages if they're installed
|
||||||
|
if strings.HasSuffix(pkg, "-git") {
|
||||||
|
if a.packageInstalled("dms-shell-hyprland-git") {
|
||||||
|
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
||||||
|
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
||||||
|
files = append(files, hyprlandMatches[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.packageInstalled("dms-shell-niri-git") {
|
||||||
|
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
||||||
|
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
||||||
|
files = append(files, niriMatches[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For other packages, install all built packages
|
||||||
|
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||||
|
files = matches
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 0 {
|
||||||
|
return fmt.Errorf("no package files found after building %s", pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
||||||
|
installArgs = append(installArgs, files...)
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||||
|
|
||||||
|
fileNames := make([]string, len(files))
|
||||||
|
for i, f := range files {
|
||||||
|
fileNames[i] = filepath.Base(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.7*(endProgress-startProgress),
|
||||||
|
LogOutput: fmt.Sprintf("Installing packages: %s", strings.Join(fileNames, ", ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.runWithProgress(installCmd, progressChan, PhaseAURPackages, startProgress+0.7*(endProgress-startProgress), endProgress); err != nil {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress,
|
||||||
|
LogOutput: fmt.Sprintf("ERROR: pacman -U failed for %s with error: %v", pkg, err),
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to install built package %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.log(fmt.Sprintf("Successfully installed AUR package: %s", pkg))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,659 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
const forceQuickshellGit = false
|
||||||
|
const forceDMSGit = false
|
||||||
|
|
||||||
|
// BaseDistribution provides common functionality for all distributions
|
||||||
|
type BaseDistribution struct {
|
||||||
|
logChan chan<- string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseDistribution creates a new base distribution
|
||||||
|
func NewBaseDistribution(logChan chan<- string) *BaseDistribution {
|
||||||
|
return &BaseDistribution{
|
||||||
|
logChan: logChan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common helper methods
|
||||||
|
func (b *BaseDistribution) commandExists(cmd string) bool {
|
||||||
|
_, err := exec.LookPath(cmd)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) CommandExists(cmd string) bool {
|
||||||
|
return b.commandExists(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) log(message string) {
|
||||||
|
if b.logChan != nil {
|
||||||
|
b.logChan <- message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) logError(message string, err error) {
|
||||||
|
errorMsg := fmt.Sprintf("ERROR: %s: %v", message, err)
|
||||||
|
b.log(errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
||||||
|
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
||||||
|
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
||||||
|
func escapeSingleQuotes(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "'", "'\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
||||||
|
// This helper escapes special characters in the password to prevent shell injection
|
||||||
|
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
||||||
|
func MakeSudoCommand(sudoPassword string, command string) string {
|
||||||
|
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
||||||
|
// The password is properly escaped to prevent shell injection and syntax errors.
|
||||||
|
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||||
|
cmdStr := MakeSudoCommand(sudoPassword, command)
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common dependency detection methods
|
||||||
|
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if b.commandExists("git") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "git",
|
||||||
|
Status: status,
|
||||||
|
Description: "Version control system",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectMatugen() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if b.commandExists("matugen") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "matugen",
|
||||||
|
Status: status,
|
||||||
|
Description: "Material Design color generation tool",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectDgop() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if b.commandExists("dgop") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "dgop",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop portal management tool",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||||
|
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||||
|
|
||||||
|
status := deps.StatusMissing
|
||||||
|
currentVersion := ""
|
||||||
|
|
||||||
|
if _, err := os.Stat(dmsPath); err == nil {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
|
||||||
|
// Only get current version, don't check for updates (lazy loading)
|
||||||
|
current, err := version.GetCurrentDMSVersion()
|
||||||
|
if err == nil {
|
||||||
|
currentVersion = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dep := deps.Dependency{
|
||||||
|
Name: "dms (DankMaterialShell)",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop Management System configuration",
|
||||||
|
Required: true,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentVersion != "" {
|
||||||
|
dep.Version = currentVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
return dep
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.Dependency {
|
||||||
|
switch terminal {
|
||||||
|
case deps.TerminalGhostty:
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if b.commandExists("ghostty") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "ghostty",
|
||||||
|
Status: status,
|
||||||
|
Description: "A fast, native terminal emulator built in Zig.",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
case deps.TerminalKitty:
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if b.commandExists("kitty") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "kitty",
|
||||||
|
Status: status,
|
||||||
|
Description: "A feature-rich, customizable terminal emulator.",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
case deps.TerminalAlacritty:
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if b.commandExists("alacritty") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "alacritty",
|
||||||
|
Status: status,
|
||||||
|
Description: "A simple terminal emulator. (No dynamic theming)",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return b.detectSpecificTerminal(deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
cliphist := deps.StatusMissing
|
||||||
|
if b.commandExists("cliphist") {
|
||||||
|
cliphist = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
wlClipboard := deps.StatusMissing
|
||||||
|
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
|
||||||
|
wlClipboard = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies,
|
||||||
|
deps.Dependency{
|
||||||
|
Name: "cliphist",
|
||||||
|
Status: cliphist,
|
||||||
|
Description: "Wayland clipboard manager",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
deps.Dependency{
|
||||||
|
Name: "wl-clipboard",
|
||||||
|
Status: wlClipboard,
|
||||||
|
Description: "Wayland clipboard utilities",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectHyprpicker() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if b.commandExists("hyprpicker") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "hyprpicker",
|
||||||
|
Status: status,
|
||||||
|
Description: "Color picker for Wayland",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
tools := []struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{"grim", "Screenshot utility for Wayland"},
|
||||||
|
{"slurp", "Region selection utility for Wayland"},
|
||||||
|
{"hyprctl", "Hyprland control utility"},
|
||||||
|
{"grimblast", "Screenshot script for Hyprland"},
|
||||||
|
{"jq", "JSON processor"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tool := range tools {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if b.commandExists(tool.name) {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies, deps.Dependency{
|
||||||
|
Name: tool.name,
|
||||||
|
Status: status,
|
||||||
|
Description: tool.description,
|
||||||
|
Required: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectQuickshell() deps.Dependency {
|
||||||
|
if !b.commandExists("qs") {
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "quickshell",
|
||||||
|
Status: deps.StatusMissing,
|
||||||
|
Description: "QtQuick based desktop shell toolkit",
|
||||||
|
Required: true,
|
||||||
|
Variant: deps.VariantStable,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("qs", "--version")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "quickshell",
|
||||||
|
Status: deps.StatusNeedsReinstall,
|
||||||
|
Description: "QtQuick based desktop shell toolkit (version check failed)",
|
||||||
|
Required: true,
|
||||||
|
Variant: deps.VariantStable,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStr := string(output)
|
||||||
|
versionRegex := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||||
|
matches := versionRegex.FindStringSubmatch(versionStr)
|
||||||
|
|
||||||
|
if len(matches) < 2 {
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "quickshell",
|
||||||
|
Status: deps.StatusNeedsReinstall,
|
||||||
|
Description: "QtQuick based desktop shell toolkit (unknown version)",
|
||||||
|
Required: true,
|
||||||
|
Variant: deps.VariantStable,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
version := matches[1]
|
||||||
|
variant := deps.VariantStable
|
||||||
|
if strings.Contains(versionStr, "git") || strings.Contains(versionStr, "+") {
|
||||||
|
variant = deps.VariantGit
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.versionCompare(version, "0.2.0") >= 0 {
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "quickshell",
|
||||||
|
Status: deps.StatusInstalled,
|
||||||
|
Version: version,
|
||||||
|
Description: "QtQuick based desktop shell toolkit",
|
||||||
|
Required: true,
|
||||||
|
Variant: variant,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "quickshell",
|
||||||
|
Status: deps.StatusNeedsUpdate,
|
||||||
|
Variant: variant,
|
||||||
|
CanToggle: true,
|
||||||
|
Version: version,
|
||||||
|
Description: "QtQuick based desktop shell toolkit (needs 0.2.0+)",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
status := deps.StatusMissing
|
||||||
|
variant := deps.VariantStable
|
||||||
|
version := ""
|
||||||
|
|
||||||
|
if b.commandExists("hyprland") || b.commandExists("Hyprland") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
cmd := exec.Command("hyprctl", "version")
|
||||||
|
if output, err := cmd.Output(); err == nil {
|
||||||
|
outStr := string(output)
|
||||||
|
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
||||||
|
variant = deps.VariantGit
|
||||||
|
}
|
||||||
|
if versionRegex := regexp.MustCompile(`v(\d+\.\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||||
|
matches := versionRegex.FindStringSubmatch(outStr)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "hyprland",
|
||||||
|
Status: status,
|
||||||
|
Version: version,
|
||||||
|
Description: "Dynamic tiling Wayland compositor",
|
||||||
|
Required: true,
|
||||||
|
Variant: variant,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
status := deps.StatusMissing
|
||||||
|
variant := deps.VariantStable
|
||||||
|
version := ""
|
||||||
|
|
||||||
|
if b.commandExists("niri") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
cmd := exec.Command("niri", "--version")
|
||||||
|
if output, err := cmd.Output(); err == nil {
|
||||||
|
outStr := string(output)
|
||||||
|
if strings.Contains(outStr, "git") || strings.Contains(outStr, "+") {
|
||||||
|
variant = deps.VariantGit
|
||||||
|
}
|
||||||
|
if versionRegex := regexp.MustCompile(`niri (\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||||
|
matches := versionRegex.FindStringSubmatch(outStr)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "niri",
|
||||||
|
Status: status,
|
||||||
|
Version: version,
|
||||||
|
Description: "Scrollable-tiling Wayland compositor",
|
||||||
|
Required: true,
|
||||||
|
Variant: variant,
|
||||||
|
CanToggle: true,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "unknown-wm",
|
||||||
|
Status: deps.StatusMissing,
|
||||||
|
Description: "Unknown window manager",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version comparison helper
|
||||||
|
func (b *BaseDistribution) versionCompare(v1, v2 string) int {
|
||||||
|
parts1 := strings.Split(v1, ".")
|
||||||
|
parts2 := strings.Split(v2, ".")
|
||||||
|
|
||||||
|
for i := 0; i < len(parts1) && i < len(parts2); i++ {
|
||||||
|
if parts1[i] < parts2[i] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if parts1[i] > parts2[i] {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts1) < len(parts2) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if len(parts1) > len(parts2) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common installation helper
|
||||||
|
func (b *BaseDistribution) runWithProgress(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64) error {
|
||||||
|
return b.runWithProgressTimeout(cmd, progressChan, phase, startProgress, endProgress, 20*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) runWithProgressTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, timeout time.Duration) error {
|
||||||
|
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, "Installing...", timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) runWithProgressStep(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string) error {
|
||||||
|
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, stepMessage, 20*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string, timeoutDuration time.Duration) error {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stderr pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputChan := make(chan string, 100)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
b.log(line)
|
||||||
|
outputChan <- line
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
b.log(line)
|
||||||
|
outputChan <- line
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
done <- cmd.Wait()
|
||||||
|
close(outputChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(200 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
progress := startProgress
|
||||||
|
progressStep := (endProgress - startProgress) / 50
|
||||||
|
lastOutput := ""
|
||||||
|
|
||||||
|
var timeout *time.Timer
|
||||||
|
var timeoutChan <-chan time.Time
|
||||||
|
if timeoutDuration > 0 {
|
||||||
|
timeout = time.NewTimer(timeoutDuration)
|
||||||
|
defer timeout.Stop()
|
||||||
|
timeoutChan = timeout.C
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
b.logError("Command execution failed", err)
|
||||||
|
b.log(fmt.Sprintf("Last output before failure: %s", lastOutput))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: startProgress,
|
||||||
|
Step: "Command failed",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: lastOutput,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: endProgress,
|
||||||
|
Step: "Installation step complete",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: lastOutput,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case output, ok := <-outputChan:
|
||||||
|
if ok {
|
||||||
|
lastOutput = output
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: progress,
|
||||||
|
Step: stepMessage,
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: output,
|
||||||
|
}
|
||||||
|
if timeout != nil {
|
||||||
|
timeout.Reset(timeoutDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-timeoutChan:
|
||||||
|
if cmd.Process != nil {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
err := fmt.Errorf("installation timed out after %v", timeoutDuration)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: startProgress,
|
||||||
|
Step: "Installation timed out",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: lastOutput,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
case <-ticker.C:
|
||||||
|
if progress < endProgress-0.01 {
|
||||||
|
progress += progressStep
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: progress,
|
||||||
|
Step: "Installing...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: lastOutput,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// installDMSBinary installs the DMS binary from GitHub releases
|
||||||
|
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
b.log("Installing/updating DMS binary...")
|
||||||
|
|
||||||
|
// Detect architecture
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
switch arch {
|
||||||
|
case "amd64":
|
||||||
|
case "arm64":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported architecture for DMS: %s", arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.80,
|
||||||
|
Step: "Downloading DMS binary...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("Downloading dms-%s.gz", arch),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get latest release version
|
||||||
|
latestVersionCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
|
`curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'`)
|
||||||
|
versionOutput, err := latestVersionCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get latest DMS version: %w", err)
|
||||||
|
}
|
||||||
|
version := strings.TrimSpace(string(versionOutput))
|
||||||
|
if version == "" {
|
||||||
|
return fmt.Errorf("could not determine latest DMS version")
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Download the gzipped binary
|
||||||
|
downloadURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch)
|
||||||
|
gzPath := filepath.Join(tmpDir, "dms.gz")
|
||||||
|
|
||||||
|
downloadCmd := exec.CommandContext(ctx, "curl", "-L", downloadURL, "-o", gzPath)
|
||||||
|
if err := downloadCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to download DMS binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.85,
|
||||||
|
Step: "Extracting DMS binary...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "gunzip dms.gz",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the binary
|
||||||
|
extractCmd := exec.CommandContext(ctx, "gunzip", gzPath)
|
||||||
|
if err := extractCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to extract DMS binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryPath := filepath.Join(tmpDir, "dms")
|
||||||
|
|
||||||
|
// Make it executable
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", binaryPath)
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to make DMS binary executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.88,
|
||||||
|
Step: "Installing DMS to /usr/local/bin...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo cp dms /usr/local/bin/",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install to /usr/local/bin
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.log("DMS binary installed successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
|
||||||
|
logChan := make(chan string, 10)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
dep := base.detectDMS()
|
||||||
|
|
||||||
|
if dep.Status != deps.StatusMissing {
|
||||||
|
t.Errorf("Expected StatusMissing, got %d", dep.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Name != "dms (DankMaterialShell)" {
|
||||||
|
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dep.Required {
|
||||||
|
t.Error("Expected Required to be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
||||||
|
if !commandExists("git") {
|
||||||
|
t.Skip("git not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
|
os.MkdirAll(dmsPath, 0755)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
|
||||||
|
exec.Command("git", "init", dmsPath).Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
||||||
|
|
||||||
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
|
os.WriteFile(testFile, []byte("test"), 0644)
|
||||||
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
logChan := make(chan string, 10)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
dep := base.detectDMS()
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusMissing {
|
||||||
|
t.Error("Expected DMS to be detected as installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Name != "dms (DankMaterialShell)" {
|
||||||
|
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dep.Required {
|
||||||
|
t.Error("Expected Required to be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Status: %d, Version: %s", dep.Status, dep.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
||||||
|
if !commandExists("git") {
|
||||||
|
t.Skip("git not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
|
os.MkdirAll(dmsPath, 0755)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
|
||||||
|
exec.Command("git", "init", dmsPath).Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
||||||
|
|
||||||
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
|
os.WriteFile(testFile, []byte("test"), 0644)
|
||||||
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "checkout", "v0.0.1").Run()
|
||||||
|
|
||||||
|
logChan := make(chan string, 10)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
dep := base.detectDMS()
|
||||||
|
|
||||||
|
if dep.Name != "dms (DankMaterialShell)" {
|
||||||
|
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dep.Required {
|
||||||
|
t.Error("Expected Required to be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Status: %d, Version: %s", dep.Status, dep.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
|
os.MkdirAll(dmsPath, 0755)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
|
||||||
|
logChan := make(chan string, 10)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
dep := base.detectDMS()
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusMissing {
|
||||||
|
t.Error("Expected DMS to be detected as present")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Name != "dms (DankMaterialShell)" {
|
||||||
|
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dep.Required {
|
||||||
|
t.Error("Expected Required to be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
|
||||||
|
logChan := make(chan string, 10)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
|
||||||
|
if base == nil {
|
||||||
|
t.Fatal("NewBaseDistribution returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if base.logChan == nil {
|
||||||
|
t.Error("logChan was not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandExists(cmd string) bool {
|
||||||
|
_, err := exec.LookPath(cmd)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseDistribution_versionCompare(t *testing.T) {
|
||||||
|
logChan := make(chan string, 10)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
v1 string
|
||||||
|
v2 string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"0.1.0", "0.1.0", 0},
|
||||||
|
{"0.1.0", "0.1.1", -1},
|
||||||
|
{"0.1.1", "0.1.0", 1},
|
||||||
|
{"0.2.0", "0.1.9", 1},
|
||||||
|
{"1.0.0", "0.9.9", 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := base.versionCompare(tt.v1, tt.v2)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseDistribution_versionCompare_WithPrefix(t *testing.T) {
|
||||||
|
logChan := make(chan string, 10)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
v1 string
|
||||||
|
v2 string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"v0.1.0", "v0.1.0", 0},
|
||||||
|
{"v0.1.0", "v0.1.1", -1},
|
||||||
|
{"v0.1.1", "v0.1.0", 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := base.versionCompare(tt.v1, tt.v2)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,542 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("debian", "#A80030", FamilyDebian, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewDebianDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type DebianDistribution struct {
|
||||||
|
*BaseDistribution
|
||||||
|
*ManualPackageInstaller
|
||||||
|
config DistroConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDebianDistribution(config DistroConfig, logChan chan<- string) *DebianDistribution {
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
return &DebianDistribution{
|
||||||
|
BaseDistribution: base,
|
||||||
|
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) GetID() string {
|
||||||
|
return d.config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) GetColorHex() string {
|
||||||
|
return d.config.ColorHex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) GetFamily() DistroFamily {
|
||||||
|
return d.config.Family
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) GetPackageManager() PackageManagerType {
|
||||||
|
return PackageManagerAPT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||||
|
return d.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
dependencies = append(dependencies, d.detectDMS())
|
||||||
|
|
||||||
|
dependencies = append(dependencies, d.detectSpecificTerminal(terminal))
|
||||||
|
|
||||||
|
dependencies = append(dependencies, d.detectGit())
|
||||||
|
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||||
|
dependencies = append(dependencies, d.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, d.detectXDGPortal())
|
||||||
|
dependencies = append(dependencies, d.detectPolkitAgent())
|
||||||
|
dependencies = append(dependencies, d.detectAccountsService())
|
||||||
|
|
||||||
|
if wm == deps.WindowManagerNiri {
|
||||||
|
dependencies = append(dependencies, d.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies, d.detectMatugen())
|
||||||
|
dependencies = append(dependencies, d.detectDgop())
|
||||||
|
dependencies = append(dependencies, d.detectHyprpicker())
|
||||||
|
dependencies = append(dependencies, d.detectClipboardTools()...)
|
||||||
|
|
||||||
|
return dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if d.packageInstalled("xdg-desktop-portal-gtk") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xdg-desktop-portal-gtk",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop integration portal for GTK",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if d.packageInstalled("mate-polkit") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "mate-polkit",
|
||||||
|
Status: status,
|
||||||
|
Description: "PolicyKit authentication agent",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if d.commandExists("xwayland-satellite") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xwayland-satellite",
|
||||||
|
Status: status,
|
||||||
|
Description: "Xwayland support",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if d.packageInstalled("accountsservice") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "accountsservice",
|
||||||
|
Status: status,
|
||||||
|
Description: "D-Bus interface for user account query and manipulation",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
|
cmd := exec.Command("dpkg", "-l", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
packages := map[string]PackageMapping{
|
||||||
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
|
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||||
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
|
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||||
|
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
||||||
|
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||||
|
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||||
|
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
||||||
|
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm == deps.WindowManagerNiri {
|
||||||
|
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.06,
|
||||||
|
Step: "Updating package lists...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Updating APT package lists",
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
|
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.08,
|
||||||
|
Step: "Installing build-essential...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get install -y build-essential",
|
||||||
|
LogOutput: "Installing build tools",
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||||
|
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.10,
|
||||||
|
Step: "Installing development dependencies...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||||
|
LogOutput: "Installing additional development tools",
|
||||||
|
}
|
||||||
|
|
||||||
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.12,
|
||||||
|
Step: "Prerequisites installation complete",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Prerequisites successfully installed",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Checking system prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting prerequisite check...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
if len(systemPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := d.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install APT packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(manualPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.80,
|
||||||
|
Step: "Installing build dependencies...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Installing build tools for manual compilation",
|
||||||
|
}
|
||||||
|
if err := d.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install build dependencies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.85,
|
||||||
|
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := d.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Configuring system...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting post-installation configuration...",
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseComplete,
|
||||||
|
Progress: 1.0,
|
||||||
|
Step: "Installation complete!",
|
||||||
|
IsComplete: true,
|
||||||
|
LogOutput: "All packages installed and configured successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
||||||
|
systemPkgs := []string{}
|
||||||
|
manualPkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := d.GetPackageMapping(wm)
|
||||||
|
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if disabledFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
|
if !exists {
|
||||||
|
d.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgInfo.Repository {
|
||||||
|
case RepoTypeSystem:
|
||||||
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeManual:
|
||||||
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
args := []string{"apt-get", "install", "-y"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
buildDeps := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, pkg := range manualPkgs {
|
||||||
|
switch pkg {
|
||||||
|
case "niri":
|
||||||
|
buildDeps["curl"] = true
|
||||||
|
buildDeps["libxkbcommon-dev"] = true
|
||||||
|
buildDeps["libwayland-dev"] = true
|
||||||
|
buildDeps["libudev-dev"] = true
|
||||||
|
buildDeps["libinput-dev"] = true
|
||||||
|
buildDeps["libdisplay-info-dev"] = true
|
||||||
|
buildDeps["libpango1.0-dev"] = true
|
||||||
|
buildDeps["libcairo-dev"] = true
|
||||||
|
buildDeps["libpipewire-0.3-dev"] = true
|
||||||
|
buildDeps["libc6-dev"] = true
|
||||||
|
buildDeps["clang"] = true
|
||||||
|
buildDeps["libseat-dev"] = true
|
||||||
|
buildDeps["libgbm-dev"] = true
|
||||||
|
buildDeps["alacritty"] = true
|
||||||
|
buildDeps["fuzzel"] = true
|
||||||
|
case "quickshell":
|
||||||
|
buildDeps["qt6-base-dev"] = true
|
||||||
|
buildDeps["qt6-base-private-dev"] = true
|
||||||
|
buildDeps["qt6-declarative-dev"] = true
|
||||||
|
buildDeps["qt6-declarative-private-dev"] = true
|
||||||
|
buildDeps["qt6-wayland-dev"] = true
|
||||||
|
buildDeps["qt6-wayland-private-dev"] = true
|
||||||
|
buildDeps["qt6-tools-dev"] = true
|
||||||
|
buildDeps["libqt6svg6-dev"] = true
|
||||||
|
buildDeps["qt6-shadertools-dev"] = true
|
||||||
|
buildDeps["spirv-tools"] = true
|
||||||
|
buildDeps["libcli11-dev"] = true
|
||||||
|
buildDeps["libjemalloc-dev"] = true
|
||||||
|
buildDeps["libwayland-dev"] = true
|
||||||
|
buildDeps["wayland-protocols"] = true
|
||||||
|
buildDeps["libdrm-dev"] = true
|
||||||
|
buildDeps["libgbm-dev"] = true
|
||||||
|
buildDeps["libegl-dev"] = true
|
||||||
|
buildDeps["libgles2-mesa-dev"] = true
|
||||||
|
buildDeps["libgl1-mesa-dev"] = true
|
||||||
|
buildDeps["libxcb1-dev"] = true
|
||||||
|
buildDeps["libpipewire-0.3-dev"] = true
|
||||||
|
buildDeps["libpam0g-dev"] = true
|
||||||
|
case "ghostty":
|
||||||
|
buildDeps["curl"] = true
|
||||||
|
case "matugen":
|
||||||
|
buildDeps["curl"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range manualPkgs {
|
||||||
|
switch pkg {
|
||||||
|
case "niri", "matugen":
|
||||||
|
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
|
}
|
||||||
|
case "cliphist", "dgop":
|
||||||
|
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buildDeps) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
depList := make([]string, 0, len(buildDeps))
|
||||||
|
for dep := range buildDeps {
|
||||||
|
depList = append(depList, dep)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"apt-get", "install", "-y"}
|
||||||
|
args = append(args, depList...)
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if d.commandExists("cargo") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.82,
|
||||||
|
Step: "Installing rustup...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
|
}
|
||||||
|
|
||||||
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||||
|
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.83,
|
||||||
|
Step: "Installing stable Rust toolchain...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "rustup install stable",
|
||||||
|
}
|
||||||
|
|
||||||
|
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||||
|
if err := d.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.commandExists("cargo") {
|
||||||
|
d.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if d.commandExists("go") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.87,
|
||||||
|
Step: "Installing Go...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||||
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
d.log("Installing Ghostty using Debian installer script...")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Running Ghostty Debian installer...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
||||||
|
LogOutput: "Installing Ghostty using pre-built Debian package",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
||||||
|
|
||||||
|
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.log("Ghostty installed successfully using Debian installer")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
switch pkg {
|
||||||
|
case "ghostty":
|
||||||
|
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDependencyDetector creates a DependencyDetector for the specified distribution
|
||||||
|
func NewDependencyDetector(distribution string, logChan chan<- string) (deps.DependencyDetector, error) {
|
||||||
|
distro, err := NewDistribution(distribution, logChan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return distro, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPackageInstaller creates a Distribution for package installation
|
||||||
|
func NewPackageInstaller(distribution string, logChan chan<- string) (Distribution, error) {
|
||||||
|
return NewDistribution(distribution, logChan)
|
||||||
|
}
|
||||||
@@ -0,0 +1,563 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewFedoraDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewFedoraDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewFedoraDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewFedoraDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewFedoraDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type FedoraDistribution struct {
|
||||||
|
*BaseDistribution
|
||||||
|
*ManualPackageInstaller
|
||||||
|
config DistroConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFedoraDistribution(config DistroConfig, logChan chan<- string) *FedoraDistribution {
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
return &FedoraDistribution{
|
||||||
|
BaseDistribution: base,
|
||||||
|
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) GetID() string {
|
||||||
|
return f.config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) GetColorHex() string {
|
||||||
|
return f.config.ColorHex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) GetFamily() DistroFamily {
|
||||||
|
return f.config.Family
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) GetPackageManager() PackageManagerType {
|
||||||
|
return PackageManagerDNF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||||
|
return f.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
// DMS at the top (shell is prominent)
|
||||||
|
dependencies = append(dependencies, f.detectDMS())
|
||||||
|
|
||||||
|
// Terminal with choice support
|
||||||
|
dependencies = append(dependencies, f.detectSpecificTerminal(terminal))
|
||||||
|
|
||||||
|
// Common detections using base methods
|
||||||
|
dependencies = append(dependencies, f.detectGit())
|
||||||
|
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||||
|
dependencies = append(dependencies, f.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, f.detectXDGPortal())
|
||||||
|
dependencies = append(dependencies, f.detectPolkitAgent())
|
||||||
|
dependencies = append(dependencies, f.detectAccountsService())
|
||||||
|
|
||||||
|
// Hyprland-specific tools
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
dependencies = append(dependencies, f.detectHyprlandTools()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Niri-specific tools
|
||||||
|
if wm == deps.WindowManagerNiri {
|
||||||
|
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
|
dependencies = append(dependencies, f.detectHyprpicker())
|
||||||
|
dependencies = append(dependencies, f.detectClipboardTools()...)
|
||||||
|
|
||||||
|
return dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if f.packageInstalled("xdg-desktop-portal-gtk") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xdg-desktop-portal-gtk",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop integration portal for GTK",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) detectPolkitAgent() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if f.packageInstalled("mate-polkit") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "mate-polkit",
|
||||||
|
Status: status,
|
||||||
|
Description: "PolicyKit authentication agent",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
||||||
|
cmd := exec.Command("rpm", "-q", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return f.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
|
packages := map[string]PackageMapping{
|
||||||
|
// Standard DNF packages
|
||||||
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
|
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||||
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
|
||||||
|
|
||||||
|
// COPR packages
|
||||||
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
|
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
|
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
||||||
|
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||||
|
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||||
|
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
||||||
|
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||||
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms-git"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) getHyprpickerMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "hyprpicker-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "hyprpicker", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if f.commandExists("xwayland-satellite") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xwayland-satellite",
|
||||||
|
Status: status,
|
||||||
|
Description: "Xwayland support",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if f.packageInstalled("accountsservice") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "accountsservice",
|
||||||
|
Status: status,
|
||||||
|
Description: "D-Bus interface for user account query and manipulation",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) getPrerequisites() []string {
|
||||||
|
return []string{
|
||||||
|
"dnf-plugins-core",
|
||||||
|
"make",
|
||||||
|
"unzip",
|
||||||
|
"libwayland-server",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
prerequisites := f.getPrerequisites()
|
||||||
|
var missingPkgs []string
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.06,
|
||||||
|
Step: "Checking prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Checking prerequisite packages",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range prerequisites {
|
||||||
|
checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg)
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
missingPkgs = append(missingPkgs, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
f.log("go not found in PATH, will install golang-bin")
|
||||||
|
missingPkgs = append(missingPkgs, "golang-bin")
|
||||||
|
} else {
|
||||||
|
f.log("go already available in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingPkgs) == 0 {
|
||||||
|
f.log("All prerequisites already installed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.08,
|
||||||
|
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo dnf install -y %s", strings.Join(missingPkgs, " ")),
|
||||||
|
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"dnf", "install", "-y"}
|
||||||
|
args = append(args, missingPkgs...)
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logError("failed to install prerequisites", err)
|
||||||
|
f.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
f.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
// Phase 1: Check Prerequisites
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Checking system prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting prerequisite check...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnfPkgs, coprPkgs, manualPkgs, variantMap := f.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
// Phase 2: Enable COPR repositories
|
||||||
|
if len(coprPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Enabling COPR repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Setting up COPR repositories for additional packages",
|
||||||
|
}
|
||||||
|
if err := f.enableCOPRRepos(ctx, coprPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable COPR repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: System Packages (DNF)
|
||||||
|
if len(dnfPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: fmt.Sprintf("Installing %d system packages...", len(dnfPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(dnfPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := f.installDNFPackages(ctx, dnfPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install DNF packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: COPR Packages
|
||||||
|
coprPkgNames := f.extractPackageNames(coprPkgs)
|
||||||
|
if len(coprPkgNames) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages, // Reusing AUR phase for COPR
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d COPR packages...", len(coprPkgNames)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing COPR packages: %s", strings.Join(coprPkgNames, ", ")),
|
||||||
|
}
|
||||||
|
if err := f.installCOPRPackages(ctx, coprPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install COPR packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: Manual Builds
|
||||||
|
if len(manualPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.85,
|
||||||
|
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := f.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 6: Configuration
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Configuring system...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting post-installation configuration...",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 7: Complete
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseComplete,
|
||||||
|
Progress: 1.0,
|
||||||
|
Step: "Installation complete!",
|
||||||
|
IsComplete: true,
|
||||||
|
LogOutput: "All packages installed and configured successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
|
||||||
|
dnfPkgs := []string{}
|
||||||
|
coprPkgs := []PackageMapping{}
|
||||||
|
manualPkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := f.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if disabledFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
|
if !exists {
|
||||||
|
f.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgInfo.Repository {
|
||||||
|
case RepoTypeSystem:
|
||||||
|
dnfPkgs = append(dnfPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeCOPR:
|
||||||
|
coprPkgs = append(coprPkgs, pkgInfo)
|
||||||
|
case RepoTypeManual:
|
||||||
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dnfPkgs, coprPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
|
names := make([]string, len(packages))
|
||||||
|
for i, pkg := range packages {
|
||||||
|
names[i] = pkg.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, pkg := range coprPkgs {
|
||||||
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
|
f.log(fmt.Sprintf("Enabling COPR repository: %s", pkg.RepoURL))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.20,
|
||||||
|
Step: fmt.Sprintf("Enabling COPR repo %s...", pkg.RepoURL),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logError(fmt.Sprintf("failed to enable COPR repo %s", pkg.RepoURL), err)
|
||||||
|
f.log(fmt.Sprintf("COPR enable command output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to enable COPR repo %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
f.log(fmt.Sprintf("COPR repo %s enabled successfully: %s", pkg.RepoURL, string(output)))
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
|
||||||
|
// Special handling for niri COPR repo - set priority=1
|
||||||
|
if pkg.RepoURL == "yalter/niri-git" {
|
||||||
|
f.log("Setting priority=1 for niri-git COPR repo...")
|
||||||
|
repoFile := "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:yalter:niri-git.repo"
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.22,
|
||||||
|
Step: "Setting niri COPR repo priority...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||||
|
}
|
||||||
|
|
||||||
|
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||||
|
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
f.logError("failed to set niri COPR repo priority", err)
|
||||||
|
f.log(fmt.Sprintf("Priority command output: %s", string(priorityOutput)))
|
||||||
|
return fmt.Errorf("failed to set niri COPR repo priority: %w", err)
|
||||||
|
}
|
||||||
|
f.log(fmt.Sprintf("niri COPR repo priority set successfully: %s", string(priorityOutput)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
args := []string{"dnf", "install", "-y"}
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
args := []string{"dnf", "install", "-y"}
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.70,
|
||||||
|
Step: "Installing COPR packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||||
|
}
|
||||||
@@ -0,0 +1,749 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
var GentooGlobalUseFlags = []string{
|
||||||
|
"dbus",
|
||||||
|
"udev",
|
||||||
|
"alsa",
|
||||||
|
"policykit",
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"webp",
|
||||||
|
"gif",
|
||||||
|
"tiff",
|
||||||
|
"svg",
|
||||||
|
"brotli",
|
||||||
|
"gdbm",
|
||||||
|
"accessibility",
|
||||||
|
"gtk",
|
||||||
|
"qt6",
|
||||||
|
"egl",
|
||||||
|
"gbm",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("gentoo", "#54487A", FamilyGentoo, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewGentooDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type GentooDistribution struct {
|
||||||
|
*BaseDistribution
|
||||||
|
*ManualPackageInstaller
|
||||||
|
config DistroConfig
|
||||||
|
skipGlobalUseFlags bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDistribution {
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
return &GentooDistribution{
|
||||||
|
BaseDistribution: base,
|
||||||
|
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) getArchKeyword() string {
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
switch arch {
|
||||||
|
case "amd64":
|
||||||
|
return "~amd64"
|
||||||
|
case "arm64":
|
||||||
|
return "~arm64"
|
||||||
|
default:
|
||||||
|
return "~amd64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) GetID() string {
|
||||||
|
return g.config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) GetColorHex() string {
|
||||||
|
return g.config.ColorHex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) GetFamily() DistroFamily {
|
||||||
|
return g.config.Family
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) GetPackageManager() PackageManagerType {
|
||||||
|
return PackageManagerPortage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||||
|
return g.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
dependencies = append(dependencies, g.detectDMS())
|
||||||
|
|
||||||
|
dependencies = append(dependencies, g.detectSpecificTerminal(terminal))
|
||||||
|
|
||||||
|
dependencies = append(dependencies, g.detectGit())
|
||||||
|
dependencies = append(dependencies, g.detectWindowManager(wm))
|
||||||
|
dependencies = append(dependencies, g.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, g.detectXDGPortal())
|
||||||
|
dependencies = append(dependencies, g.detectPolkitAgent())
|
||||||
|
dependencies = append(dependencies, g.detectAccountsService())
|
||||||
|
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
dependencies = append(dependencies, g.detectHyprlandTools()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm == deps.WindowManagerNiri {
|
||||||
|
dependencies = append(dependencies, g.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies, g.detectMatugen())
|
||||||
|
dependencies = append(dependencies, g.detectDgop())
|
||||||
|
dependencies = append(dependencies, g.detectHyprpicker())
|
||||||
|
dependencies = append(dependencies, g.detectClipboardTools()...)
|
||||||
|
|
||||||
|
return dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xdg-desktop-portal-gtk",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop integration portal for GTK",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if g.packageInstalled("mate-extra/mate-polkit") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "mate-polkit",
|
||||||
|
Status: status,
|
||||||
|
Description: "PolicyKit authentication agent",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if g.packageInstalled("gui-apps/xwayland-satellite") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xwayland-satellite",
|
||||||
|
Status: status,
|
||||||
|
Description: "Xwayland support",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if g.packageInstalled("sys-apps/accountsservice") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "accountsservice",
|
||||||
|
Status: status,
|
||||||
|
Description: "D-Bus interface for user account query and manipulation",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) packageInstalled(pkg string) bool {
|
||||||
|
cmd := exec.Command("qlist", "-I", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return g.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
|
archKeyword := g.getArchKeyword()
|
||||||
|
packages := map[string]PackageMapping{
|
||||||
|
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
||||||
|
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
|
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
|
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
|
||||||
|
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||||
|
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
||||||
|
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
|
||||||
|
|
||||||
|
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
||||||
|
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||||
|
"qtwayland": {Name: "dev-qt/qtwayland", Repository: RepoTypeSystem},
|
||||||
|
"mesa": {Name: "media-libs/mesa", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||||
|
|
||||||
|
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
|
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
|
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
|
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
||||||
|
packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem}
|
||||||
|
packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem}
|
||||||
|
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
||||||
|
packages["grimblast"] = PackageMapping{Name: "gui-wm/hyprland-contrib", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||||
|
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
packages["niri"] = g.getNiriMapping(variants["niri"])
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) getQuickshellMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
|
return PackageMapping{Name: "gui-apps/quickshell", Repository: RepoTypeGURU, UseFlags: "breakpad jemalloc sockets wayland layer-shell session-lock toplevel-management screencopy X pipewire tray mpris pam hyprland hyprland-global-shortcuts hyprland-focus-grab i3 i3-ipc bluetooth", AcceptKeywords: "**"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
archKeyword := g.getArchKeyword()
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
|
return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
|
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) getPrerequisites() []string {
|
||||||
|
return []string{
|
||||||
|
"app-eselect/eselect-repository",
|
||||||
|
"dev-vcs/git",
|
||||||
|
"dev-build/make",
|
||||||
|
"app-arch/unzip",
|
||||||
|
"dev-util/pkgconf",
|
||||||
|
"dev-qt/qtdeclarative",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword string) error {
|
||||||
|
useFlags := strings.Join(GentooGlobalUseFlags, " ")
|
||||||
|
|
||||||
|
checkCmd := exec.CommandContext(ctx, "grep", "-q", "^USE=", "/etc/portage/make.conf")
|
||||||
|
hasUse := checkCmd.Run() == nil
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if hasUse {
|
||||||
|
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||||
|
} else {
|
||||||
|
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
g.log(fmt.Sprintf("Failed to set global USE flags: %s", string(output)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
g.log(fmt.Sprintf("Set global USE flags: %s", useFlags))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
prerequisites := g.getPrerequisites()
|
||||||
|
var missingPkgs []string
|
||||||
|
|
||||||
|
if !g.skipGlobalUseFlags {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Setting global USE flags...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Configuring global USE flags in /etc/portage/make.conf",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.setGlobalUseFlags(ctx, sudoPassword); err != nil {
|
||||||
|
g.logError("failed to set global USE flags", err)
|
||||||
|
return fmt.Errorf("failed to set global USE flags: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Skipping global USE flags...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Skipping global USE flags configuration (using existing configuration)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.06,
|
||||||
|
Step: "Checking prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Checking prerequisite packages",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range prerequisites {
|
||||||
|
checkCmd := exec.CommandContext(ctx, "qlist", "-I", pkg)
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
missingPkgs = append(missingPkgs, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
g.log("go not found in PATH, will install dev-lang/go")
|
||||||
|
missingPkgs = append(missingPkgs, "dev-lang/go")
|
||||||
|
} else {
|
||||||
|
g.log("go already available in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingPkgs) == 0 {
|
||||||
|
g.log("All prerequisites already installed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.07,
|
||||||
|
Step: "Syncing Portage tree...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo emerge --sync",
|
||||||
|
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||||
|
}
|
||||||
|
|
||||||
|
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||||
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
|
if syncErr != nil {
|
||||||
|
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||||
|
return fmt.Errorf("failed to sync Portage tree: %w\nOutput: %s", syncErr, string(syncOutput))
|
||||||
|
}
|
||||||
|
g.log("Portage tree synced successfully")
|
||||||
|
|
||||||
|
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.08,
|
||||||
|
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")),
|
||||||
|
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
|
args = append(args, missingPkgs...)
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
g.logError("failed to install prerequisites", err)
|
||||||
|
g.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w\nOutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
g.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
g.skipGlobalUseFlags = skipGlobalUseFlags
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Checking system prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting prerequisite check...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPkgs, guruPkgs, manualPkgs, variantMap := g.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
g.log(fmt.Sprintf("CATEGORIZED PACKAGES: system=%d, guru=%d, manual=%d", len(systemPkgs), len(guruPkgs), len(manualPkgs)))
|
||||||
|
|
||||||
|
if len(systemPkgs) > 0 {
|
||||||
|
systemPkgNames := g.extractPackageNames(systemPkgs)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgNames, ", ")),
|
||||||
|
}
|
||||||
|
if err := g.installPortagePackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Portage packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(guruPkgs) > 0 {
|
||||||
|
g.log(fmt.Sprintf("FOUND %d GURU PACKAGES - WILL SYNC GURU REPO", len(guruPkgs)))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.60,
|
||||||
|
Step: "Syncing GURU repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Syncing GURU repository to fetch latest ebuilds",
|
||||||
|
}
|
||||||
|
g.log("ABOUT TO CALL syncGURURepo")
|
||||||
|
if err := g.syncGURURepo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
g.log(fmt.Sprintf("syncGURURepo RETURNED ERROR: %v", err))
|
||||||
|
return fmt.Errorf("failed to sync GURU repository: %w", err)
|
||||||
|
}
|
||||||
|
g.log("syncGURURepo COMPLETED SUCCESSFULLY")
|
||||||
|
|
||||||
|
guruPkgNames := g.extractPackageNames(guruPkgs)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d GURU packages...", len(guruPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing GURU packages: %s", strings.Join(guruPkgNames, ", ")),
|
||||||
|
}
|
||||||
|
if err := g.installGURUPackages(ctx, guruPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install GURU packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(manualPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.85,
|
||||||
|
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := g.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Configuring system...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting post-installation configuration...",
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseComplete,
|
||||||
|
Progress: 1.0,
|
||||||
|
Step: "Installation complete!",
|
||||||
|
IsComplete: true,
|
||||||
|
LogOutput: "All packages installed and configured successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]PackageMapping, []PackageMapping, []string, map[string]deps.PackageVariant) {
|
||||||
|
systemPkgs := []PackageMapping{}
|
||||||
|
guruPkgs := []PackageMapping{}
|
||||||
|
manualPkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := g.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if disabledFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
|
if !exists {
|
||||||
|
g.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgInfo.Repository {
|
||||||
|
case RepoTypeSystem:
|
||||||
|
systemPkgs = append(systemPkgs, pkgInfo)
|
||||||
|
case RepoTypeGURU:
|
||||||
|
guruPkgs = append(guruPkgs, pkgInfo)
|
||||||
|
case RepoTypeManual:
|
||||||
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs, guruPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
|
names := make([]string, len(packages))
|
||||||
|
for i, pkg := range packages {
|
||||||
|
names[i] = pkg.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) installPortagePackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
packageNames := g.extractPackageNames(packages)
|
||||||
|
g.log(fmt.Sprintf("Installing Portage packages: %s", strings.Join(packageNames, ", ")))
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg.AcceptKeywords != "" {
|
||||||
|
if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil {
|
||||||
|
return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pkg.UseFlags != "" {
|
||||||
|
if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil {
|
||||||
|
return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
|
args = append(args, packageNames...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||||
|
packageUseDir := "/etc/portage/package.use"
|
||||||
|
|
||||||
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||||
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to create package.use directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
useFlagLine := fmt.Sprintf("%s %s", packageName, useFlags)
|
||||||
|
|
||||||
|
checkExistingCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
|
fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, packageUseDir))
|
||||||
|
if checkExistingCmd.Run() == nil {
|
||||||
|
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||||
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
|
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||||
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to remove old USE flags: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||||
|
|
||||||
|
output, err := appendCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
g.log(fmt.Sprintf("append output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to write USE flags to package.use: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.log(fmt.Sprintf("Set USE flags for %s: %s", packageName, useFlags))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.55,
|
||||||
|
Step: "Enabling GURU repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo eselect repository enable guru",
|
||||||
|
LogOutput: "Enabling GURU repository with eselect",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable GURU repository
|
||||||
|
enableCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
|
output, err := enableCmd.CombinedOutput()
|
||||||
|
|
||||||
|
g.log(fmt.Sprintf("eselect repository enable guru output:\n%s", string(output)))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.55,
|
||||||
|
LogOutput: "GURU repository enabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.55,
|
||||||
|
LogOutput: fmt.Sprintf("ERROR enabling GURU: %v", err),
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to enable GURU repository: %w\nOutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync GURU repository
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.57,
|
||||||
|
Step: "Syncing GURU repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo emaint sync --repo guru",
|
||||||
|
LogOutput: "Syncing GURU repository",
|
||||||
|
}
|
||||||
|
|
||||||
|
syncCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
|
|
||||||
|
g.log(fmt.Sprintf("emaint sync --repo guru output:\n%s", string(syncOutput)))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.57,
|
||||||
|
LogOutput: "GURU repository synced",
|
||||||
|
}
|
||||||
|
|
||||||
|
if syncErr != nil {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.57,
|
||||||
|
LogOutput: fmt.Sprintf("ERROR syncing GURU: %v", syncErr),
|
||||||
|
Error: syncErr,
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to sync GURU repository: %w\nOutput: %s", syncErr, string(syncOutput))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packageName, keywords, sudoPassword string) error {
|
||||||
|
checkCmd := exec.CommandContext(ctx, "portageq", "match", "/", packageName)
|
||||||
|
if output, err := checkCmd.CombinedOutput(); err == nil && len(output) > 0 {
|
||||||
|
g.log(fmt.Sprintf("Package %s is already available (may already be unmasked)", packageName))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||||
|
|
||||||
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||||
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to create package.accept_keywords directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keywordLine := fmt.Sprintf("%s %s", packageName, keywords)
|
||||||
|
|
||||||
|
checkExistingCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
|
fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, acceptKeywordsDir))
|
||||||
|
if checkExistingCmd.Run() == nil {
|
||||||
|
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||||
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
|
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||||
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to remove old accept keywords: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||||
|
|
||||||
|
output, err := appendCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
g.log(fmt.Sprintf("append output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to write accept keywords: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.log(fmt.Sprintf("Set accept keywords for %s: %s", packageName, keywords))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
packageNames := g.extractPackageNames(packages)
|
||||||
|
g.log(fmt.Sprintf("Installing GURU packages: %s", strings.Join(packageNames, ", ")))
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg.AcceptKeywords != "" {
|
||||||
|
if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil {
|
||||||
|
return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pkg.UseFlags != "" {
|
||||||
|
if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil {
|
||||||
|
return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guruPackages := make([]string, len(packageNames))
|
||||||
|
for i, pkg := range packageNames {
|
||||||
|
guruPackages[i] = pkg + "::guru"
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
|
args = append(args, guruPackages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.70,
|
||||||
|
Step: "Installing GURU packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DistroFamily represents a family of related distributions
|
||||||
|
type DistroFamily string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FamilyArch DistroFamily = "arch"
|
||||||
|
FamilyFedora DistroFamily = "fedora"
|
||||||
|
FamilySUSE DistroFamily = "suse"
|
||||||
|
FamilyUbuntu DistroFamily = "ubuntu"
|
||||||
|
FamilyDebian DistroFamily = "debian"
|
||||||
|
FamilyNix DistroFamily = "nix"
|
||||||
|
FamilyGentoo DistroFamily = "gentoo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PackageManagerType defines the package manager a distro uses
|
||||||
|
type PackageManagerType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PackageManagerPacman PackageManagerType = "pacman"
|
||||||
|
PackageManagerDNF PackageManagerType = "dnf"
|
||||||
|
PackageManagerAPT PackageManagerType = "apt"
|
||||||
|
PackageManagerZypper PackageManagerType = "zypper"
|
||||||
|
PackageManagerNix PackageManagerType = "nix"
|
||||||
|
PackageManagerPortage PackageManagerType = "portage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepositoryType defines the type of repository for a package
|
||||||
|
type RepositoryType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RepoTypeSystem RepositoryType = "system" // Standard system repo (pacman, dnf, apt)
|
||||||
|
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
|
||||||
|
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
|
||||||
|
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
|
||||||
|
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||||
|
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||||
|
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstallPhase represents the current phase of installation
|
||||||
|
type InstallPhase int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PhasePrerequisites InstallPhase = iota
|
||||||
|
PhaseAURHelper
|
||||||
|
PhaseSystemPackages
|
||||||
|
PhaseAURPackages
|
||||||
|
PhaseCursorTheme
|
||||||
|
PhaseConfiguration
|
||||||
|
PhaseComplete
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstallProgressMsg represents progress during package installation
|
||||||
|
type InstallProgressMsg struct {
|
||||||
|
Phase InstallPhase
|
||||||
|
Progress float64
|
||||||
|
Step string
|
||||||
|
IsComplete bool
|
||||||
|
NeedsSudo bool
|
||||||
|
CommandInfo string
|
||||||
|
LogOutput string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageMapping defines how to install a package on a specific distro
|
||||||
|
type PackageMapping struct {
|
||||||
|
Name string // Package name to install
|
||||||
|
Repository RepositoryType // Repository type
|
||||||
|
RepoURL string // Repository URL if needed (e.g., COPR repo, PPA)
|
||||||
|
BuildFunc string // Name of manual build function if RepoTypeManual
|
||||||
|
UseFlags string // USE flags for Gentoo packages
|
||||||
|
AcceptKeywords string // Accept keywords for Gentoo packages (e.g., "~amd64")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distribution defines a Linux distribution with all its specific configurations
|
||||||
|
type Distribution interface {
|
||||||
|
// Metadata
|
||||||
|
GetID() string
|
||||||
|
GetColorHex() string
|
||||||
|
GetFamily() DistroFamily
|
||||||
|
GetPackageManager() PackageManagerType
|
||||||
|
|
||||||
|
// Dependency Detection
|
||||||
|
DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error)
|
||||||
|
DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error)
|
||||||
|
|
||||||
|
// Package Installation
|
||||||
|
InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error
|
||||||
|
|
||||||
|
// Package Mapping
|
||||||
|
GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping
|
||||||
|
|
||||||
|
// Prerequisites
|
||||||
|
InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistroConfig holds configuration for a distribution
|
||||||
|
type DistroConfig struct {
|
||||||
|
ID string
|
||||||
|
ColorHex string
|
||||||
|
Family DistroFamily
|
||||||
|
Constructor func(config DistroConfig, logChan chan<- string) Distribution
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry holds all supported distributions
|
||||||
|
var Registry = make(map[string]DistroConfig)
|
||||||
|
|
||||||
|
// Register adds a distribution to the registry
|
||||||
|
func Register(id, colorHex string, family DistroFamily, constructor func(config DistroConfig, logChan chan<- string) Distribution) {
|
||||||
|
Registry[id] = DistroConfig{
|
||||||
|
ID: id,
|
||||||
|
ColorHex: colorHex,
|
||||||
|
Family: family,
|
||||||
|
Constructor: constructor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupportedDistros returns a list of all supported distribution IDs
|
||||||
|
func GetSupportedDistros() []string {
|
||||||
|
ids := make([]string, 0, len(Registry))
|
||||||
|
for id := range Registry {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDistroSupported checks if a distribution ID is supported
|
||||||
|
func IsDistroSupported(id string) bool {
|
||||||
|
_, exists := Registry[id]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDistribution creates a distribution instance by ID
|
||||||
|
func NewDistribution(id string, logChan chan<- string) (Distribution, error) {
|
||||||
|
config, exists := Registry[id]
|
||||||
|
if !exists {
|
||||||
|
return nil, &UnsupportedDistributionError{ID: id}
|
||||||
|
}
|
||||||
|
return config.Constructor(config, logChan), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsupportedDistributionError is returned when a distribution is not supported
|
||||||
|
type UnsupportedDistributionError struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnsupportedDistributionError) Error() string {
|
||||||
|
return "unsupported distribution: " + e.ID
|
||||||
|
}
|
||||||
@@ -0,0 +1,955 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ManualPackageInstaller provides methods for installing packages from source
|
||||||
|
type ManualPackageInstaller struct {
|
||||||
|
*BaseDistribution
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
||||||
|
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
||||||
|
parts := strings.Split(line, "refs/tags/")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
latestTag := strings.TrimSpace(parts[1])
|
||||||
|
return latestTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLatestQuickshellTag fetches the latest tag from the quickshell repository
|
||||||
|
func (m *ManualPackageInstaller) getLatestQuickshellTag(ctx context.Context) string {
|
||||||
|
tagCmd := exec.CommandContext(ctx, "git", "ls-remote", "--tags", "--sort=-v:refname",
|
||||||
|
"https://github.com/quickshell-mirror/quickshell.git")
|
||||||
|
tagOutput, err := tagCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
m.log(fmt.Sprintf("Warning: failed to fetch quickshell tags: %v", err))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.parseLatestTagFromGitOutput(string(tagOutput))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
variant := variantMap[pkg]
|
||||||
|
switch pkg {
|
||||||
|
case "dms (DankMaterialShell)", "dms":
|
||||||
|
if err := m.installDankMaterialShell(ctx, variant, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install DankMaterialShell: %w", err)
|
||||||
|
}
|
||||||
|
case "dgop":
|
||||||
|
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install dgop: %w", err)
|
||||||
|
}
|
||||||
|
case "grimblast":
|
||||||
|
if err := m.installGrimblast(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install grimblast: %w", err)
|
||||||
|
}
|
||||||
|
case "niri":
|
||||||
|
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install niri: %w", err)
|
||||||
|
}
|
||||||
|
case "quickshell":
|
||||||
|
if err := m.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
|
}
|
||||||
|
case "hyprland":
|
||||||
|
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprland: %w", err)
|
||||||
|
}
|
||||||
|
case "hyprpicker":
|
||||||
|
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
case "ghostty":
|
||||||
|
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||||
|
}
|
||||||
|
case "matugen":
|
||||||
|
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install matugen: %w", err)
|
||||||
|
}
|
||||||
|
case "cliphist":
|
||||||
|
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||||
|
}
|
||||||
|
case "xwayland-satellite":
|
||||||
|
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
m.log(fmt.Sprintf("Warning: No manual build method for %s", pkg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing dgop from source...")
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
if homeDir == "" {
|
||||||
|
return fmt.Errorf("HOME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Cloning dgop repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/AvengeMedia/dgop.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/AvengeMedia/dgop.git", tmpDir)
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
m.logError("failed to clone dgop repository", err)
|
||||||
|
return fmt.Errorf("failed to clone dgop repository: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "make")
|
||||||
|
buildCmd.Dir = tmpDir
|
||||||
|
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.7, "Building dgop..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build dgop: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.7,
|
||||||
|
Step: "Installing dgop...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo make install",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||||
|
installCmd.Dir = tmpDir
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
m.logError("failed to install dgop", err)
|
||||||
|
return fmt.Errorf("failed to install dgop: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("dgop installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installGrimblast(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing grimblast script for Hyprland...")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Downloading grimblast script...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "curl grimblast script",
|
||||||
|
}
|
||||||
|
|
||||||
|
grimblastURL := "https://raw.githubusercontent.com/hyprwm/contrib/refs/heads/main/grimblast/grimblast"
|
||||||
|
tmpPath := filepath.Join(os.TempDir(), "grimblast")
|
||||||
|
|
||||||
|
downloadCmd := exec.CommandContext(ctx, "curl", "-L", "-o", tmpPath, grimblastURL)
|
||||||
|
if err := downloadCmd.Run(); err != nil {
|
||||||
|
m.logError("failed to download grimblast", err)
|
||||||
|
return fmt.Errorf("failed to download grimblast: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.5,
|
||||||
|
Step: "Making grimblast executable...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "chmod +x grimblast",
|
||||||
|
}
|
||||||
|
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", tmpPath)
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
|
m.logError("failed to make grimblast executable", err)
|
||||||
|
return fmt.Errorf("failed to make grimblast executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.8,
|
||||||
|
Step: "Installing grimblast to /usr/local/bin...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo cp grimblast /usr/local/bin/",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("cp %s /usr/local/bin/grimblast", tmpPath))
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
m.logError("failed to install grimblast", err)
|
||||||
|
return fmt.Errorf("failed to install grimblast: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
|
||||||
|
m.log("grimblast installed successfully to /usr/local/bin")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing niri from source...")
|
||||||
|
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
||||||
|
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
||||||
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
os.RemoveAll(buildDir)
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
|
}()
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.2,
|
||||||
|
Step: "Cloning niri repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/YaLTeR/niri.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/YaLTeR/niri.git", buildDir)
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone niri: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkoutCmd := exec.CommandContext(ctx, "git", "-C", buildDir, "checkout", "v25.08")
|
||||||
|
if err := checkoutCmd.Run(); err != nil {
|
||||||
|
m.log(fmt.Sprintf("Warning: failed to checkout v25.08, using main: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.commandExists("cargo-deb") {
|
||||||
|
cargoDebInstallCmd := exec.CommandContext(ctx, "cargo", "install", "cargo-deb")
|
||||||
|
cargoDebInstallCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir)
|
||||||
|
if err := m.runWithProgressStep(cargoDebInstallCmd, progressChan, PhaseSystemPackages, 0.3, 0.35, "Installing cargo-deb..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to install cargo-deb: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDebCmd := exec.CommandContext(ctx, "cargo", "deb")
|
||||||
|
buildDebCmd.Dir = buildDir
|
||||||
|
buildDebCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir)
|
||||||
|
if err := m.runWithProgressStep(buildDebCmd, progressChan, PhaseSystemPackages, 0.35, 0.95, "Building niri deb package..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build niri deb: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.95,
|
||||||
|
Step: "Installing niri deb package...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "dpkg -i niri.deb",
|
||||||
|
}
|
||||||
|
|
||||||
|
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||||
|
|
||||||
|
output, err := installDebCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
m.log(fmt.Sprintf("dpkg install failed. Output:\n%s", string(output)))
|
||||||
|
return fmt.Errorf("failed to install niri deb package: %w\nOutput:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log(fmt.Sprintf("dpkg install successful. Output:\n%s", string(output)))
|
||||||
|
|
||||||
|
m.log("niri installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing quickshell from source...")
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
if homeDir == "" {
|
||||||
|
return fmt.Errorf("HOME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Cloning quickshell repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
var cloneCmd *exec.Cmd
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||||
|
} else {
|
||||||
|
latestTag := m.getLatestQuickshellTag(ctx)
|
||||||
|
if latestTag != "" {
|
||||||
|
m.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
||||||
|
cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||||
|
} else {
|
||||||
|
m.log("Warning: failed to fetch latest tag, using default branch")
|
||||||
|
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone quickshell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDir := tmpDir + "/build"
|
||||||
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.3,
|
||||||
|
Step: "Configuring quickshell build...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake -B build -S . -G Ninja",
|
||||||
|
}
|
||||||
|
|
||||||
|
configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build",
|
||||||
|
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
|
||||||
|
"-DCRASH_REPORTER=off",
|
||||||
|
"-DCMAKE_CXX_STANDARD=20")
|
||||||
|
configureCmd.Dir = tmpDir
|
||||||
|
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
|
||||||
|
output, err := configureCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||||
|
return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output)))
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.4,
|
||||||
|
Step: "Building quickshell (this may take a while)...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake --build build",
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build")
|
||||||
|
buildCmd.Dir = tmpDir
|
||||||
|
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build quickshell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.8,
|
||||||
|
Step: "Installing quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo cmake --install build",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
|
installCmd.Dir = tmpDir
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("quickshell installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing Hyprland from source...")
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
if homeDir == "" {
|
||||||
|
return fmt.Errorf("HOME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Cloning Hyprland repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone --recursive https://github.com/hyprwm/Hyprland.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recursive", "https://github.com/hyprwm/Hyprland.git", tmpDir)
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone Hyprland: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkoutCmd := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "v0.50.1")
|
||||||
|
if err := checkoutCmd.Run(); err != nil {
|
||||||
|
m.log(fmt.Sprintf("Warning: failed to checkout v0.50.1, using main: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "make", "all")
|
||||||
|
buildCmd.Dir = tmpDir
|
||||||
|
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.2, 0.8, "Building Hyprland..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build Hyprland: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.8,
|
||||||
|
Step: "Installing Hyprland...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo make install",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||||
|
installCmd.Dir = tmpDir
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("Hyprland installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing hyprpicker from source...")
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
if homeDir == "" {
|
||||||
|
return fmt.Errorf("HOME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install hyprutils first
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Building hyprutils dependency...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
|
||||||
|
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create hyprutils directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(hyprutilsDir)
|
||||||
|
|
||||||
|
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
|
||||||
|
if err := cloneUtilsCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"--no-warn-unused-cli",
|
||||||
|
"-DCMAKE_BUILD_TYPE:STRING=Release",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
|
||||||
|
"-DBUILD_TESTING=off",
|
||||||
|
"-S", ".",
|
||||||
|
"-B", "./build")
|
||||||
|
configureUtilsCmd.Dir = hyprutilsDir
|
||||||
|
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
|
||||||
|
buildUtilsCmd.Dir = hyprutilsDir
|
||||||
|
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
||||||
|
installUtilsCmd.Dir = hyprutilsDir
|
||||||
|
if err := installUtilsCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprutils: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install hyprwayland-scanner
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.2,
|
||||||
|
Step: "Building hyprwayland-scanner dependency...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
|
||||||
|
if err := os.MkdirAll(scannerDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create scanner directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(scannerDir)
|
||||||
|
|
||||||
|
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
|
||||||
|
if err := cloneScannerCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureScannerCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX=/usr",
|
||||||
|
"-B", "build")
|
||||||
|
configureScannerCmd.Dir = scannerDir
|
||||||
|
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
|
||||||
|
buildScannerCmd.Dir = scannerDir
|
||||||
|
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
|
installScannerCmd.Dir = scannerDir
|
||||||
|
if err := installScannerCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now build hyprpicker
|
||||||
|
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: "Cloning hyprpicker repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.45,
|
||||||
|
Step: "Configuring hyprpicker build...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
|
||||||
|
}
|
||||||
|
|
||||||
|
configureCmd := exec.CommandContext(ctx, "cmake",
|
||||||
|
"--no-warn-unused-cli",
|
||||||
|
"-DCMAKE_BUILD_TYPE:STRING=Release",
|
||||||
|
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
|
||||||
|
"-S", ".",
|
||||||
|
"-B", "./build")
|
||||||
|
configureCmd.Dir = tmpDir
|
||||||
|
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
|
||||||
|
output, err := configureCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||||
|
return fmt.Errorf("failed to configure hyprpicker: %w\nCMake output:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.55,
|
||||||
|
Step: "Building hyprpicker...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake --build build --target hyprpicker",
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "hyprpicker")
|
||||||
|
buildCmd.Dir = tmpDir
|
||||||
|
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.8,
|
||||||
|
Step: "Installing hyprpicker...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo cmake --install build",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
||||||
|
installCmd.Dir = tmpDir
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("hyprpicker installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing Ghostty from source...")
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
if homeDir == "" {
|
||||||
|
return fmt.Errorf("HOME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Cloning Ghostty repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/ghostty-org/ghostty.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/ghostty-org/ghostty.git", tmpDir)
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone Ghostty: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.2,
|
||||||
|
Step: "Building Ghostty (this may take a while)...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "zig build -Doptimize=ReleaseFast",
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "zig", "build", "-Doptimize=ReleaseFast")
|
||||||
|
buildCmd.Dir = tmpDir
|
||||||
|
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
|
if err := buildCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to build Ghostty: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.8,
|
||||||
|
Step: "Installing Ghostty...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("Ghostty installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing matugen from source...")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Installing matugen via cargo...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cargo install matugen",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := exec.CommandContext(ctx, "cargo", "install", "matugen")
|
||||||
|
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building matugen..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to install matugen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
sourcePath := filepath.Join(homeDir, ".cargo", "bin", "matugen")
|
||||||
|
targetPath := "/usr/local/bin/matugen"
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.7,
|
||||||
|
Step: "Installing matugen binary to system...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it executable
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("matugen installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing DankMaterialShell (DMS)...")
|
||||||
|
|
||||||
|
if err := m.installDMSBinary(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
m.logError("Failed to install DMS binary", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||||
|
|
||||||
|
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Cloning DankMaterialShell...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Dir(dmsPath)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
||||||
|
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceDMSGit || variant == deps.VariantGit {
|
||||||
|
m.log("Using git variant (master branch)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master")
|
||||||
|
tagOutput, err := tagCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
m.log("Using default branch (no tags found)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
latestTag := strings.TrimSpace(string(tagOutput))
|
||||||
|
checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag)
|
||||||
|
if err := checkoutCmd.Run(); err != nil {
|
||||||
|
m.logError(fmt.Sprintf("Failed to checkout tag %s", latestTag), err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
||||||
|
m.log("DankMaterialShell cloned successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Updating DankMaterialShell...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Updating ~/.config/quickshell/dms",
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "fetch", "origin", "--tags", "--force")
|
||||||
|
if err := fetchCmd.Run(); err != nil {
|
||||||
|
m.logError("Failed to fetch updates", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceDMSGit || variant == deps.VariantGit {
|
||||||
|
branchCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
branchOutput, err := branchCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
m.logError("Failed to get current branch", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
branch := strings.TrimSpace(string(branchOutput))
|
||||||
|
if branch == "" {
|
||||||
|
branch = "master"
|
||||||
|
}
|
||||||
|
|
||||||
|
pullCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "pull", "origin", branch)
|
||||||
|
if err := pullCmd.Run(); err != nil {
|
||||||
|
m.logError("Failed to pull updates", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("DankMaterialShell updated successfully (git variant)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
latestTagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master")
|
||||||
|
tagOutput, err := latestTagCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
m.logError("Failed to get latest tag", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
latestTag := strings.TrimSpace(string(tagOutput))
|
||||||
|
checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag)
|
||||||
|
if err := checkoutCmd.Run(); err != nil {
|
||||||
|
m.logError(fmt.Sprintf("Failed to checkout tag %s", latestTag), err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing cliphist from source...")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Installing cliphist via go install...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "go install go.senan.xyz/cliphist@latest",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
|
||||||
|
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
|
||||||
|
targetPath := "/usr/local/bin/cliphist"
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.7,
|
||||||
|
Step: "Installing cliphist binary to system...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it executable
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to make cliphist executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("cliphist installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
m.log("Installing xwayland-satellite from source...")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Installing xwayland-satellite via cargo...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cargo install --git https://github.com/Supreeeme/xwayland-satellite --tag v0.7",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := exec.CommandContext(ctx, "cargo", "install", "--git", "https://github.com/Supreeeme/xwayland-satellite", "--tag", "v0.7")
|
||||||
|
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building xwayland-satellite..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
sourcePath := filepath.Join(homeDir, ".cargo", "bin", "xwayland-satellite")
|
||||||
|
targetPath := "/usr/local/bin/xwayland-satellite"
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.7,
|
||||||
|
Step: "Installing xwayland-satellite binary to system...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log("xwayland-satellite installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestManualPackageInstaller_parseLatestTagFromGitOutput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal tag output",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||||
|
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0
|
||||||
|
703a3789083d2f990c4e99cd25c97c2a4cccbd81 refs/tags/v0.1.0`,
|
||||||
|
expected: "v0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "annotated tags with ^{}",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||||
|
b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.2.1^{}
|
||||||
|
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||||
|
expected: "v0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed tags",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0
|
||||||
|
b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.3.0^{}
|
||||||
|
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0
|
||||||
|
c1c150fab00a93ea983aaca5df55304bc837f51d refs/tags/beta-1`,
|
||||||
|
expected: "v0.3.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty output",
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no tags",
|
||||||
|
input: "some other output\nwithout tags",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only annotated tags",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1^{}
|
||||||
|
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0^{}`,
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single tag",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.0.0`,
|
||||||
|
expected: "v1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag with extra whitespace",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||||
|
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||||
|
expected: "v0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "beta and rc tags",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0-beta.1
|
||||||
|
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||||
|
expected: "v0.3.0-beta.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tags without v prefix",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/0.2.1
|
||||||
|
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/0.2.0`,
|
||||||
|
expected: "0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple lines with spaces",
|
||||||
|
input: `
|
||||||
|
a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.2.3
|
||||||
|
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v1.2.2
|
||||||
|
`,
|
||||||
|
expected: "v1.2.3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag at end of line",
|
||||||
|
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1`,
|
||||||
|
expected: "v0.2.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
installer := &ManualPackageInstaller{BaseDistribution: base}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := installer.parseLatestTagFromGitOutput(tt.input)
|
||||||
|
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("parseLatestTagFromGitOutput() = %q, expected %q", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManualPackageInstaller_parseLatestTagFromGitOutput_EmptyInstaller(t *testing.T) {
|
||||||
|
// Test that parsing works even with a minimal installer setup
|
||||||
|
logChan := make(chan string, 10)
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
installer := &ManualPackageInstaller{BaseDistribution: base}
|
||||||
|
|
||||||
|
input := `abc123 refs/tags/v1.0.0
|
||||||
|
def456 refs/tags/v0.9.0`
|
||||||
|
|
||||||
|
result := installer.parseLatestTagFromGitOutput(input)
|
||||||
|
|
||||||
|
if result != "v1.0.0" {
|
||||||
|
t.Errorf("Expected v1.0.0, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,466 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("nixos", "#7EBAE4", FamilyNix, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewNixOSDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type NixOSDistribution struct {
|
||||||
|
*BaseDistribution
|
||||||
|
config DistroConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNixOSDistribution(config DistroConfig, logChan chan<- string) *NixOSDistribution {
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
return &NixOSDistribution{
|
||||||
|
BaseDistribution: base,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) GetID() string {
|
||||||
|
return n.config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) GetColorHex() string {
|
||||||
|
return n.config.ColorHex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) GetFamily() DistroFamily {
|
||||||
|
return n.config.Family
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) GetPackageManager() PackageManagerType {
|
||||||
|
return PackageManagerNix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||||
|
return n.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
// DMS at the top (shell is prominent)
|
||||||
|
dependencies = append(dependencies, n.detectDMS())
|
||||||
|
|
||||||
|
// Terminal with choice support
|
||||||
|
dependencies = append(dependencies, n.detectSpecificTerminal(terminal))
|
||||||
|
|
||||||
|
// Common detections using base methods
|
||||||
|
dependencies = append(dependencies, n.detectGit())
|
||||||
|
dependencies = append(dependencies, n.detectWindowManager(wm))
|
||||||
|
dependencies = append(dependencies, n.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, n.detectXDGPortal())
|
||||||
|
dependencies = append(dependencies, n.detectPolkitAgent())
|
||||||
|
dependencies = append(dependencies, n.detectAccountsService())
|
||||||
|
|
||||||
|
// Hyprland-specific tools
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
dependencies = append(dependencies, n.detectHyprlandTools()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Niri-specific tools
|
||||||
|
if wm == deps.WindowManagerNiri {
|
||||||
|
dependencies = append(dependencies, n.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
|
dependencies = append(dependencies, n.detectMatugen())
|
||||||
|
dependencies = append(dependencies, n.detectDgop())
|
||||||
|
dependencies = append(dependencies, n.detectHyprpicker())
|
||||||
|
dependencies = append(dependencies, n.detectClipboardTools()...)
|
||||||
|
|
||||||
|
return dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) detectDMS() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
|
||||||
|
// For NixOS, check if quickshell can find the dms config
|
||||||
|
cmd := exec.Command("qs", "-c", "dms", "--list")
|
||||||
|
if err := cmd.Run(); err == nil {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
} else if n.packageInstalled("DankMaterialShell") {
|
||||||
|
// Fallback: check if flake is in profile
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "dms (DankMaterialShell)",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop Management System configuration (installed as flake)",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) detectXDGPortal() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if n.packageInstalled("xdg-desktop-portal-gtk") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xdg-desktop-portal-gtk",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop integration portal for GTK",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
status := deps.StatusMissing
|
||||||
|
description := "Dynamic tiling Wayland compositor"
|
||||||
|
if n.commandExists("hyprland") || n.commandExists("Hyprland") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
} else {
|
||||||
|
description = "Install system-wide: programs.hyprland.enable = true; in configuration.nix"
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "hyprland",
|
||||||
|
Status: status,
|
||||||
|
Description: description,
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
status := deps.StatusMissing
|
||||||
|
description := "Scrollable-tiling Wayland compositor"
|
||||||
|
if n.commandExists("niri") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
} else {
|
||||||
|
description = "Install system-wide: environment.systemPackages = [ pkgs.niri ]; in configuration.nix"
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "niri",
|
||||||
|
Status: status,
|
||||||
|
Description: description,
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "unknown-wm",
|
||||||
|
Status: deps.StatusMissing,
|
||||||
|
Description: "Unknown window manager",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) detectHyprlandTools() []deps.Dependency {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
tools := []struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{"grim", "Screenshot utility for Wayland"},
|
||||||
|
{"slurp", "Region selection utility for Wayland"},
|
||||||
|
{"hyprctl", "Hyprland control utility (comes with system Hyprland)"},
|
||||||
|
{"hyprpicker", "Color picker for Hyprland"},
|
||||||
|
{"grimblast", "Screenshot script for Hyprland"},
|
||||||
|
{"jq", "JSON processor"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tool := range tools {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
|
||||||
|
// Special handling for hyprctl - it comes with system hyprland
|
||||||
|
if tool.name == "hyprctl" {
|
||||||
|
if n.commandExists("hyprctl") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if n.commandExists(tool.name) {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = append(dependencies, deps.Dependency{
|
||||||
|
Name: tool.name,
|
||||||
|
Status: status,
|
||||||
|
Description: tool.description,
|
||||||
|
Required: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if n.commandExists("xwayland-satellite") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xwayland-satellite",
|
||||||
|
Status: status,
|
||||||
|
Description: "Xwayland support",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) detectPolkitAgent() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if n.packageInstalled("mate-polkit") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "mate-polkit",
|
||||||
|
Status: status,
|
||||||
|
Description: "PolicyKit authentication agent",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) detectAccountsService() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if n.packageInstalled("accountsservice") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "accountsservice",
|
||||||
|
Status: status,
|
||||||
|
Description: "D-Bus interface for user account query and manipulation",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) packageInstalled(pkg string) bool {
|
||||||
|
cmd := exec.Command("nix", "profile", "list")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(string(output), pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
packages := map[string]PackageMapping{
|
||||||
|
"git": {Name: "nixpkgs#git", Repository: RepoTypeSystem},
|
||||||
|
"quickshell": {Name: "github:quickshell-mirror/quickshell", Repository: RepoTypeFlake},
|
||||||
|
"matugen": {Name: "github:InioX/matugen", Repository: RepoTypeFlake},
|
||||||
|
"dgop": {Name: "github:AvengeMedia/dgop", Repository: RepoTypeFlake},
|
||||||
|
"dms (DankMaterialShell)": {Name: "github:AvengeMedia/DankMaterialShell", Repository: RepoTypeFlake},
|
||||||
|
"ghostty": {Name: "nixpkgs#ghostty", Repository: RepoTypeSystem},
|
||||||
|
"alacritty": {Name: "nixpkgs#alacritty", Repository: RepoTypeSystem},
|
||||||
|
"cliphist": {Name: "nixpkgs#cliphist", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "nixpkgs#wl-clipboard", Repository: RepoTypeSystem},
|
||||||
|
"xdg-desktop-portal-gtk": {Name: "nixpkgs#xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
|
"mate-polkit": {Name: "nixpkgs#mate.mate-polkit", Repository: RepoTypeSystem},
|
||||||
|
"accountsservice": {Name: "nixpkgs#accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"hyprpicker": {Name: "nixpkgs#hyprpicker", Repository: RepoTypeSystem},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Window managers (hyprland/niri) should be installed system-wide on NixOS
|
||||||
|
// We only install the tools here
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
// Skip hyprland itself - should be installed system-wide
|
||||||
|
packages["grim"] = PackageMapping{Name: "nixpkgs#grim", Repository: RepoTypeSystem}
|
||||||
|
packages["slurp"] = PackageMapping{Name: "nixpkgs#slurp", Repository: RepoTypeSystem}
|
||||||
|
packages["grimblast"] = PackageMapping{Name: "github:hyprwm/contrib#grimblast", Repository: RepoTypeFlake}
|
||||||
|
packages["jq"] = PackageMapping{Name: "nixpkgs#jq", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
// Skip niri itself - should be installed system-wide
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "nixpkgs#xwayland-satellite", Repository: RepoTypeFlake}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.10,
|
||||||
|
Step: "NixOS prerequisites ready",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "NixOS package manager is ready to use",
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
// Phase 1: Check Prerequisites
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Checking system prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting prerequisite check...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := n.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nixpkgsPkgs, flakePkgs, _ := n.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
// Phase 2: Nixpkgs Packages
|
||||||
|
if len(nixpkgsPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: fmt.Sprintf("Installing %d packages from nixpkgs...", len(nixpkgsPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(nixpkgsPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := n.installNixpkgsPackages(ctx, nixpkgsPkgs, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install nixpkgs packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Flake Packages
|
||||||
|
if len(flakePkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d packages from flakes...", len(flakePkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing flake packages: %s", strings.Join(flakePkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := n.installFlakePackages(ctx, flakePkgs, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install flake packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: Configuration
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Configuring system...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting post-installation configuration...",
|
||||||
|
}
|
||||||
|
if err := n.postInstallConfig(progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure system: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: Complete
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseComplete,
|
||||||
|
Progress: 1.0,
|
||||||
|
Step: "Installation complete!",
|
||||||
|
IsComplete: true,
|
||||||
|
LogOutput: "All packages installed and configured successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
||||||
|
nixpkgsPkgs := []string{}
|
||||||
|
flakePkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := n.GetPackageMapping(wm)
|
||||||
|
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if disabledFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
|
if !exists {
|
||||||
|
n.log(fmt.Sprintf("Warning: No package mapping found for %s", dep.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgInfo.Repository {
|
||||||
|
case RepoTypeSystem:
|
||||||
|
nixpkgsPkgs = append(nixpkgsPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeFlake:
|
||||||
|
flakePkgs = append(flakePkgs, pkgInfo.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nixpkgsPkgs, flakePkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) installNixpkgsPackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n.log(fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
args := []string{"profile", "install"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing nixpkgs packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("nix %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "nix", args...)
|
||||||
|
return n.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) installFlakePackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n.log(fmt.Sprintf("Installing flake packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
baseProgress := 0.65
|
||||||
|
progressStep := 0.20 / float64(len(packages))
|
||||||
|
|
||||||
|
for i, pkg := range packages {
|
||||||
|
currentProgress := baseProgress + (float64(i) * progressStep)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: currentProgress,
|
||||||
|
Step: fmt.Sprintf("Installing flake package %s (%d/%d)...", pkg, i+1, len(packages)),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("nix profile install %s", pkg),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "nix", "profile", "install", pkg)
|
||||||
|
if err := n.runWithProgress(cmd, progressChan, PhaseAURPackages, currentProgress, currentProgress+progressStep); err != nil {
|
||||||
|
return fmt.Errorf("failed to install flake package %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixOSDistribution) postInstallConfig(progressChan chan<- InstallProgressMsg) error {
|
||||||
|
// For NixOS, DMS is installed as a flake package, so we skip both the binary installation and git clone
|
||||||
|
// The flake installation handles both the binary and config files correctly
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.95,
|
||||||
|
Step: "NixOS configuration complete",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "DMS installed via flake - binary and config handled by Nix",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewOpenSUSEDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenSUSEDistribution struct {
|
||||||
|
*BaseDistribution
|
||||||
|
*ManualPackageInstaller
|
||||||
|
config DistroConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
return &OpenSUSEDistribution{
|
||||||
|
BaseDistribution: base,
|
||||||
|
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) GetID() string {
|
||||||
|
return o.config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) GetColorHex() string {
|
||||||
|
return o.config.ColorHex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) GetFamily() DistroFamily {
|
||||||
|
return o.config.Family
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) GetPackageManager() PackageManagerType {
|
||||||
|
return PackageManagerZypper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||||
|
return o.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
// DMS at the top (shell is prominent)
|
||||||
|
dependencies = append(dependencies, o.detectDMS())
|
||||||
|
|
||||||
|
// Terminal with choice support
|
||||||
|
dependencies = append(dependencies, o.detectSpecificTerminal(terminal))
|
||||||
|
|
||||||
|
// Common detections using base methods
|
||||||
|
dependencies = append(dependencies, o.detectGit())
|
||||||
|
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||||
|
dependencies = append(dependencies, o.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, o.detectXDGPortal())
|
||||||
|
dependencies = append(dependencies, o.detectPolkitAgent())
|
||||||
|
dependencies = append(dependencies, o.detectAccountsService())
|
||||||
|
|
||||||
|
// Hyprland-specific tools
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
dependencies = append(dependencies, o.detectHyprlandTools()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Niri-specific tools
|
||||||
|
if wm == deps.WindowManagerNiri {
|
||||||
|
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
|
dependencies = append(dependencies, o.detectHyprpicker())
|
||||||
|
dependencies = append(dependencies, o.detectClipboardTools()...)
|
||||||
|
|
||||||
|
return dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if o.packageInstalled("xdg-desktop-portal-gtk") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xdg-desktop-portal-gtk",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop integration portal for GTK",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if o.packageInstalled("mate-polkit") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "mate-polkit",
|
||||||
|
Status: status,
|
||||||
|
Description: "PolicyKit authentication agent",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||||
|
cmd := exec.Command("rpm", "-q", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
|
packages := map[string]PackageMapping{
|
||||||
|
// Standard zypper packages
|
||||||
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
|
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||||
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
|
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
|
// Manual builds
|
||||||
|
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||||
|
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||||
|
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||||
|
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
|
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||||
|
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||||
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
|
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||||
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if o.commandExists("xwayland-satellite") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xwayland-satellite",
|
||||||
|
Status: status,
|
||||||
|
Description: "Xwayland support",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if o.packageInstalled("accountsservice") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "accountsservice",
|
||||||
|
Status: status,
|
||||||
|
Description: "D-Bus interface for user account query and manipulation",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||||
|
return []string{
|
||||||
|
"make",
|
||||||
|
"unzip",
|
||||||
|
"gcc",
|
||||||
|
"gcc-c++",
|
||||||
|
"cmake",
|
||||||
|
"ninja",
|
||||||
|
"pkgconf-pkg-config",
|
||||||
|
"git",
|
||||||
|
"qt6-base-devel",
|
||||||
|
"qt6-declarative-devel",
|
||||||
|
"qt6-declarative-private-devel",
|
||||||
|
"qt6-shadertools",
|
||||||
|
"qt6-shadertools-devel",
|
||||||
|
"qt6-wayland-devel",
|
||||||
|
"qt6-waylandclient-private-devel",
|
||||||
|
"spirv-tools-devel",
|
||||||
|
"cli11-devel",
|
||||||
|
"wayland-protocols-devel",
|
||||||
|
"libgbm-devel",
|
||||||
|
"libdrm-devel",
|
||||||
|
"pipewire-devel",
|
||||||
|
"jemalloc-devel",
|
||||||
|
"wayland-utils",
|
||||||
|
"Mesa-libGLESv3-devel",
|
||||||
|
"pam-devel",
|
||||||
|
"glib2-devel",
|
||||||
|
"polkit-devel",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
prerequisites := o.getPrerequisites()
|
||||||
|
var missingPkgs []string
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.06,
|
||||||
|
Step: "Checking prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Checking prerequisite packages",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range prerequisites {
|
||||||
|
checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg)
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
missingPkgs = append(missingPkgs, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
o.log("go not found in PATH, will install go")
|
||||||
|
missingPkgs = append(missingPkgs, "go")
|
||||||
|
} else {
|
||||||
|
o.log("go already available in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingPkgs) == 0 {
|
||||||
|
o.log("All prerequisites already installed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.08,
|
||||||
|
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo zypper install -y %s", strings.Join(missingPkgs, " ")),
|
||||||
|
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"zypper", "install", "-y"}
|
||||||
|
args = append(args, missingPkgs...)
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
o.logError("failed to install prerequisites", err)
|
||||||
|
o.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
o.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
// Phase 1: Check Prerequisites
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Checking system prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting prerequisite check...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
// Phase 2: System Packages (Zypper)
|
||||||
|
if len(systemPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Manual Builds
|
||||||
|
if len(manualPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.85,
|
||||||
|
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := o.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: Configuration
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Configuring system...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting post-installation configuration...",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: Complete
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseComplete,
|
||||||
|
Progress: 1.0,
|
||||||
|
Step: "Installation complete!",
|
||||||
|
IsComplete: true,
|
||||||
|
LogOutput: "All packages installed and configured successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
||||||
|
systemPkgs := []string{}
|
||||||
|
manualPkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := o.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if disabledFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
|
if !exists {
|
||||||
|
o.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgInfo.Repository {
|
||||||
|
case RepoTypeSystem:
|
||||||
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeManual:
|
||||||
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
args := []string{"zypper", "install", "-y"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
o.log("Installing quickshell from source (with openSUSE-specific build flags)...")
|
||||||
|
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
if homeDir == "" {
|
||||||
|
return fmt.Errorf("HOME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Cloning quickshell repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
var cloneCmd *exec.Cmd
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||||
|
} else {
|
||||||
|
latestTag := o.getLatestQuickshellTag(ctx)
|
||||||
|
if latestTag != "" {
|
||||||
|
o.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
||||||
|
cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||||
|
} else {
|
||||||
|
o.log("Warning: failed to fetch latest tag, using default branch")
|
||||||
|
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone quickshell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDir := tmpDir + "/build"
|
||||||
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.3,
|
||||||
|
Step: "Configuring quickshell build (with openSUSE flags)...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake -B build -S . -G Ninja",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get optflags from rpm
|
||||||
|
optflagsCmd := exec.CommandContext(ctx, "rpm", "--eval", "%{optflags}")
|
||||||
|
optflagsOutput, err := optflagsCmd.Output()
|
||||||
|
optflags := strings.TrimSpace(string(optflagsOutput))
|
||||||
|
if err != nil || optflags == "" {
|
||||||
|
o.log("Warning: Could not get optflags from rpm, using default -O2 -g")
|
||||||
|
optflags = "-O2 -g"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set openSUSE-specific CFLAGS
|
||||||
|
customCFLAGS := fmt.Sprintf("%s -I/usr/include/wayland", optflags)
|
||||||
|
|
||||||
|
configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build",
|
||||||
|
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
|
||||||
|
"-DCRASH_REPORTER=off",
|
||||||
|
"-DCMAKE_CXX_STANDARD=20")
|
||||||
|
configureCmd.Dir = tmpDir
|
||||||
|
configureCmd.Env = append(os.Environ(),
|
||||||
|
"TMPDIR="+cacheDir,
|
||||||
|
"CFLAGS="+customCFLAGS,
|
||||||
|
"CXXFLAGS="+customCFLAGS)
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Using CFLAGS: %s", customCFLAGS))
|
||||||
|
|
||||||
|
output, err := configureCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
o.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||||
|
return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output)))
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.4,
|
||||||
|
Step: "Building quickshell (this may take a while)...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "cmake --build build",
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build")
|
||||||
|
buildCmd.Dir = tmpDir
|
||||||
|
buildCmd.Env = append(os.Environ(),
|
||||||
|
"TMPDIR="+cacheDir,
|
||||||
|
"CFLAGS="+customCFLAGS,
|
||||||
|
"CXXFLAGS="+customCFLAGS)
|
||||||
|
if err := o.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil {
|
||||||
|
return fmt.Errorf("failed to build quickshell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.8,
|
||||||
|
Step: "Installing quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo cmake --install build",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
|
installCmd.Dir = tmpDir
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log("quickshell installed successfully from source")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if o.commandExists("cargo") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.82,
|
||||||
|
Step: "Installing rustup...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo zypper install rustup",
|
||||||
|
}
|
||||||
|
|
||||||
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||||
|
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.83,
|
||||||
|
Step: "Installing stable Rust toolchain...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "rustup install stable",
|
||||||
|
}
|
||||||
|
|
||||||
|
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||||
|
if err := o.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !o.commandExists("cargo") {
|
||||||
|
o.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "matugen" {
|
||||||
|
if err := o.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
variant := variantMap[pkg]
|
||||||
|
if pkg == "quickshell" {
|
||||||
|
if err := o.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := o.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DistroInfo contains basic information about a distribution
|
||||||
|
type DistroInfo struct {
|
||||||
|
ID string
|
||||||
|
HexColorCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSInfo contains complete OS information
|
||||||
|
type OSInfo struct {
|
||||||
|
Distribution DistroInfo
|
||||||
|
Version string
|
||||||
|
VersionID string
|
||||||
|
PrettyName string
|
||||||
|
Architecture string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOSInfo detects the current OS and returns information about it
|
||||||
|
func GetOSInfo() (*OSInfo, error) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
return nil, errdefs.NewCustomError(errdefs.ErrTypeNotLinux, fmt.Sprintf("Only linux is supported, but I found %s", runtime.GOOS))
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
|
||||||
|
return nil, errdefs.NewCustomError(errdefs.ErrTypeInvalidArchitecture, fmt.Sprintf("Only amd64 and arm64 are supported, but I found %s", runtime.GOARCH))
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &OSInfo{
|
||||||
|
Architecture: runtime.GOARCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open("/etc/os-release")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := parts[0]
|
||||||
|
value := strings.Trim(parts[1], "\"")
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "ID":
|
||||||
|
config, exists := Registry[value]
|
||||||
|
if !exists {
|
||||||
|
return nil, errdefs.NewCustomError(errdefs.ErrTypeUnsupportedDistribution, fmt.Sprintf("Unsupported distribution: %s", value))
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Distribution = DistroInfo{
|
||||||
|
ID: value, // Use the actual ID from os-release
|
||||||
|
HexColorCode: config.ColorHex,
|
||||||
|
}
|
||||||
|
case "VERSION_ID", "BUILD_ID":
|
||||||
|
info.VersionID = value
|
||||||
|
case "VERSION":
|
||||||
|
info.Version = value
|
||||||
|
case "PRETTY_NAME":
|
||||||
|
info.PrettyName = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUnsupportedDistro checks if a distribution/version combination is supported
|
||||||
|
func IsUnsupportedDistro(distroID, versionID string) bool {
|
||||||
|
if !IsDistroSupported(distroID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if distroID == "ubuntu" {
|
||||||
|
parts := strings.Split(versionID, ".")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
major, err1 := strconv.Atoi(parts[0])
|
||||||
|
minor, err2 := strconv.Atoi(parts[1])
|
||||||
|
|
||||||
|
if err1 == nil && err2 == nil {
|
||||||
|
return major < 25 || (major == 25 && minor < 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if distroID == "debian" {
|
||||||
|
if versionID == "" {
|
||||||
|
// debian testing/sid have no version ID
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
versionNum, err := strconv.Atoi(versionID)
|
||||||
|
if err == nil {
|
||||||
|
return versionNum < 12
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,758 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("ubuntu", "#E95420", FamilyUbuntu, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewUbuntuDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type UbuntuDistribution struct {
|
||||||
|
*BaseDistribution
|
||||||
|
*ManualPackageInstaller
|
||||||
|
config DistroConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUbuntuDistribution(config DistroConfig, logChan chan<- string) *UbuntuDistribution {
|
||||||
|
base := NewBaseDistribution(logChan)
|
||||||
|
return &UbuntuDistribution{
|
||||||
|
BaseDistribution: base,
|
||||||
|
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) GetID() string {
|
||||||
|
return u.config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) GetColorHex() string {
|
||||||
|
return u.config.ColorHex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) GetFamily() DistroFamily {
|
||||||
|
return u.config.Family
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) GetPackageManager() PackageManagerType {
|
||||||
|
return PackageManagerAPT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||||
|
return u.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||||
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
|
// DMS at the top (shell is prominent)
|
||||||
|
dependencies = append(dependencies, u.detectDMS())
|
||||||
|
|
||||||
|
// Terminal with choice support
|
||||||
|
dependencies = append(dependencies, u.detectSpecificTerminal(terminal))
|
||||||
|
|
||||||
|
// Common detections using base methods
|
||||||
|
dependencies = append(dependencies, u.detectGit())
|
||||||
|
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||||
|
dependencies = append(dependencies, u.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, u.detectXDGPortal())
|
||||||
|
dependencies = append(dependencies, u.detectPolkitAgent())
|
||||||
|
dependencies = append(dependencies, u.detectAccountsService())
|
||||||
|
|
||||||
|
// Hyprland-specific tools
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
dependencies = append(dependencies, u.detectHyprlandTools()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Niri-specific tools
|
||||||
|
if wm == deps.WindowManagerNiri {
|
||||||
|
dependencies = append(dependencies, u.detectXwaylandSatellite())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base detections (common across distros)
|
||||||
|
dependencies = append(dependencies, u.detectMatugen())
|
||||||
|
dependencies = append(dependencies, u.detectDgop())
|
||||||
|
dependencies = append(dependencies, u.detectHyprpicker())
|
||||||
|
dependencies = append(dependencies, u.detectClipboardTools()...)
|
||||||
|
|
||||||
|
return dependencies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if u.packageInstalled("xdg-desktop-portal-gtk") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xdg-desktop-portal-gtk",
|
||||||
|
Status: status,
|
||||||
|
Description: "Desktop integration portal for GTK",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) detectPolkitAgent() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if u.packageInstalled("mate-polkit") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "mate-polkit",
|
||||||
|
Status: status,
|
||||||
|
Description: "PolicyKit authentication agent",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if u.commandExists("xwayland-satellite") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "xwayland-satellite",
|
||||||
|
Status: status,
|
||||||
|
Description: "Xwayland support",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if u.packageInstalled("accountsservice") {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: "accountsservice",
|
||||||
|
Status: status,
|
||||||
|
Description: "D-Bus interface for user account query and manipulation",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
|
cmd := exec.Command("dpkg", "-l", pkg)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
packages := map[string]PackageMapping{
|
||||||
|
// Standard APT packages
|
||||||
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
|
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||||
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"},
|
||||||
|
|
||||||
|
// Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs)
|
||||||
|
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||||
|
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
||||||
|
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||||
|
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||||
|
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||||
|
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
// Use the cppiber PPA for Hyprland
|
||||||
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||||
|
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||||
|
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||||
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||||
|
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||||
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
||||||
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.06,
|
||||||
|
Step: "Updating package lists...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Updating APT package lists",
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
|
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.08,
|
||||||
|
Step: "Installing build-essential...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get install -y build-essential",
|
||||||
|
LogOutput: "Installing build tools",
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
// Not installed, install it
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||||
|
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.10,
|
||||||
|
Step: "Installing development dependencies...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev",
|
||||||
|
LogOutput: "Installing additional development tools",
|
||||||
|
}
|
||||||
|
|
||||||
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||||
|
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.12,
|
||||||
|
Step: "Prerequisites installation complete",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Prerequisites successfully installed",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
// Phase 1: Check Prerequisites
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.05,
|
||||||
|
Step: "Checking system prerequisites...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting prerequisite check...",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPkgs, ppaPkgs, manualPkgs, variantMap := u.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
// Phase 2: Enable PPA repositories
|
||||||
|
if len(ppaPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Enabling PPA repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Setting up PPA repositories for additional packages",
|
||||||
|
}
|
||||||
|
if err := u.enablePPARepos(ctx, ppaPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable PPA repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: System Packages (APT)
|
||||||
|
if len(systemPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.35,
|
||||||
|
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := u.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install APT packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: PPA Packages
|
||||||
|
ppaPkgNames := u.extractPackageNames(ppaPkgs)
|
||||||
|
if len(ppaPkgNames) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages, // Reusing AUR phase for PPA
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d PPA packages...", len(ppaPkgNames)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing PPA packages: %s", strings.Join(ppaPkgNames, ", ")),
|
||||||
|
}
|
||||||
|
if err := u.installPPAPackages(ctx, ppaPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install PPA packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: Manual Builds
|
||||||
|
if len(manualPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.80,
|
||||||
|
Step: "Installing build dependencies...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Installing build tools for manual compilation",
|
||||||
|
}
|
||||||
|
if err := u.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install build dependencies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.85,
|
||||||
|
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
|
}
|
||||||
|
if err := u.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 6: Configuration
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseConfiguration,
|
||||||
|
Progress: 0.90,
|
||||||
|
Step: "Configuring system...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Starting post-installation configuration...",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 7: Complete
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseComplete,
|
||||||
|
Progress: 1.0,
|
||||||
|
Step: "Installation complete!",
|
||||||
|
IsComplete: true,
|
||||||
|
LogOutput: "All packages installed and configured successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
|
||||||
|
systemPkgs := []string{}
|
||||||
|
ppaPkgs := []PackageMapping{}
|
||||||
|
manualPkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := u.GetPackageMapping(wm)
|
||||||
|
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if disabledFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
|
if !exists {
|
||||||
|
u.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgInfo.Repository {
|
||||||
|
case RepoTypeSystem:
|
||||||
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypePPA:
|
||||||
|
ppaPkgs = append(ppaPkgs, pkgInfo)
|
||||||
|
case RepoTypeManual:
|
||||||
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs, ppaPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
|
names := make([]string, len(packages))
|
||||||
|
for i, pkg := range packages {
|
||||||
|
names[i] = pkg.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
installPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"apt-get install -y software-properties-common")
|
||||||
|
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||||
|
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range ppaPkgs {
|
||||||
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
|
u.log(fmt.Sprintf("Enabling PPA repository: %s", pkg.RepoURL))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.20,
|
||||||
|
Step: fmt.Sprintf("Enabling PPA repo %s...", pkg.RepoURL),
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||||
|
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
|
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||||
|
return fmt.Errorf("failed to enable PPA repo %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
u.log(fmt.Sprintf("PPA repo %s enabled successfully", pkg.RepoURL))
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(enabledRepos) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.25,
|
||||||
|
Step: "Updating package lists...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get update",
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
|
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
args := []string{"apt-get", "install", "-y"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.40,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
args := []string{"apt-get", "install", "-y"}
|
||||||
|
args = append(args, packages...)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.70,
|
||||||
|
Step: "Installing PPA packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
buildDeps := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, pkg := range manualPkgs {
|
||||||
|
switch pkg {
|
||||||
|
case "niri":
|
||||||
|
buildDeps["curl"] = true
|
||||||
|
buildDeps["libxkbcommon-dev"] = true
|
||||||
|
buildDeps["libwayland-dev"] = true
|
||||||
|
buildDeps["libudev-dev"] = true
|
||||||
|
buildDeps["libinput-dev"] = true
|
||||||
|
buildDeps["libdisplay-info-dev"] = true
|
||||||
|
buildDeps["libpango1.0-dev"] = true
|
||||||
|
buildDeps["libcairo-dev"] = true
|
||||||
|
buildDeps["libpipewire-0.3-dev"] = true
|
||||||
|
buildDeps["libc6-dev"] = true
|
||||||
|
buildDeps["clang"] = true
|
||||||
|
buildDeps["libseat-dev"] = true
|
||||||
|
buildDeps["libgbm-dev"] = true
|
||||||
|
buildDeps["alacritty"] = true
|
||||||
|
buildDeps["fuzzel"] = true
|
||||||
|
buildDeps["libxcb-cursor-dev"] = true
|
||||||
|
case "quickshell":
|
||||||
|
buildDeps["qt6-base-dev"] = true
|
||||||
|
buildDeps["qt6-base-private-dev"] = true
|
||||||
|
buildDeps["qt6-declarative-dev"] = true
|
||||||
|
buildDeps["qt6-declarative-private-dev"] = true
|
||||||
|
buildDeps["qt6-wayland-dev"] = true
|
||||||
|
buildDeps["qt6-wayland-private-dev"] = true
|
||||||
|
buildDeps["qt6-tools-dev"] = true
|
||||||
|
buildDeps["libqt6svg6-dev"] = true
|
||||||
|
buildDeps["qt6-shadertools-dev"] = true
|
||||||
|
buildDeps["spirv-tools"] = true
|
||||||
|
buildDeps["libcli11-dev"] = true
|
||||||
|
buildDeps["libjemalloc-dev"] = true
|
||||||
|
buildDeps["libwayland-dev"] = true
|
||||||
|
buildDeps["wayland-protocols"] = true
|
||||||
|
buildDeps["libdrm-dev"] = true
|
||||||
|
buildDeps["libgbm-dev"] = true
|
||||||
|
buildDeps["libegl-dev"] = true
|
||||||
|
buildDeps["libgles2-mesa-dev"] = true
|
||||||
|
buildDeps["libgl1-mesa-dev"] = true
|
||||||
|
buildDeps["libxcb1-dev"] = true
|
||||||
|
buildDeps["libpipewire-0.3-dev"] = true
|
||||||
|
buildDeps["libpam0g-dev"] = true
|
||||||
|
case "ghostty":
|
||||||
|
buildDeps["curl"] = true
|
||||||
|
buildDeps["libgtk-4-dev"] = true
|
||||||
|
buildDeps["libadwaita-1-dev"] = true
|
||||||
|
case "matugen":
|
||||||
|
buildDeps["curl"] = true
|
||||||
|
case "cliphist":
|
||||||
|
// Go will be installed separately with PPA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range manualPkgs {
|
||||||
|
switch pkg {
|
||||||
|
case "niri", "matugen":
|
||||||
|
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
|
}
|
||||||
|
case "ghostty":
|
||||||
|
if err := u.installZig(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Zig: %w", err)
|
||||||
|
}
|
||||||
|
case "cliphist", "dgop":
|
||||||
|
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buildDeps) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
depList := make([]string, 0, len(buildDeps))
|
||||||
|
for dep := range buildDeps {
|
||||||
|
depList = append(depList, dep)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"apt-get", "install", "-y"}
|
||||||
|
args = append(args, depList...)
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if u.commandExists("cargo") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.82,
|
||||||
|
Step: "Installing rustup...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
|
}
|
||||||
|
|
||||||
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||||
|
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.83,
|
||||||
|
Step: "Installing stable Rust toolchain...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "rustup install stable",
|
||||||
|
}
|
||||||
|
|
||||||
|
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||||
|
if err := u.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify cargo is now available
|
||||||
|
if !u.commandExists("cargo") {
|
||||||
|
u.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) installZig(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if u.commandExists("zig") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zigUrl := "https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz"
|
||||||
|
zigTmp := filepath.Join(cacheDir, "zig.tar.xz")
|
||||||
|
|
||||||
|
downloadCmd := exec.CommandContext(ctx, "curl", "-L", zigUrl, "-o", zigTmp)
|
||||||
|
if err := u.runWithProgress(downloadCmd, progressChan, PhaseSystemPackages, 0.84, 0.85); err != nil {
|
||||||
|
return fmt.Errorf("failed to download Zig: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
extractCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("tar -xf %s -C /opt/", zigTmp))
|
||||||
|
if err := u.runWithProgress(extractCmd, progressChan, PhaseSystemPackages, 0.85, 0.86); err != nil {
|
||||||
|
return fmt.Errorf("failed to extract Zig: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
linkCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"ln -sf /opt/zig-linux-x86_64-0.11.0/zig /usr/local/bin/zig")
|
||||||
|
return u.runWithProgress(linkCmd, progressChan, PhaseSystemPackages, 0.86, 0.87)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if u.commandExists("go") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.87,
|
||||||
|
Step: "Adding Go PPA repository...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||||
|
}
|
||||||
|
|
||||||
|
addPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||||
|
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||||
|
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.88,
|
||||||
|
Step: "Updating package lists...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get update",
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||||
|
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.89,
|
||||||
|
Step: "Installing Go...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||||
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
u.log("Installing Ghostty using Ubuntu installer script...")
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.1,
|
||||||
|
Step: "Running Ghostty Ubuntu installer...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
||||||
|
LogOutput: "Installing Ghostty using pre-built Ubuntu package",
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
||||||
|
|
||||||
|
if err := u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
||||||
|
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log("Ghostty installed successfully using Ubuntu installer")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
switch pkg {
|
||||||
|
case "ghostty":
|
||||||
|
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
//go:build !distro_binary
|
||||||
|
|
||||||
|
package dms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateMainMenu AppState = iota
|
||||||
|
StateUpdate
|
||||||
|
StateUpdatePassword
|
||||||
|
StateUpdateProgress
|
||||||
|
StateShell
|
||||||
|
StatePluginsMenu
|
||||||
|
StatePluginsBrowse
|
||||||
|
StatePluginDetail
|
||||||
|
StatePluginSearch
|
||||||
|
StatePluginsInstalled
|
||||||
|
StatePluginInstalledDetail
|
||||||
|
StateGreeterMenu
|
||||||
|
StateGreeterCompositorSelect
|
||||||
|
StateGreeterPassword
|
||||||
|
StateGreeterInstalling
|
||||||
|
StateAbout
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
version string
|
||||||
|
detector *Detector
|
||||||
|
dependencies []DependencyInfo
|
||||||
|
state AppState
|
||||||
|
selectedItem int
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
|
||||||
|
// Menu items
|
||||||
|
menuItems []MenuItem
|
||||||
|
|
||||||
|
updateDeps []DependencyInfo
|
||||||
|
selectedUpdateDep int
|
||||||
|
updateToggles map[string]bool
|
||||||
|
|
||||||
|
updateProgressChan chan updateProgressMsg
|
||||||
|
updateProgress updateProgressMsg
|
||||||
|
updateLogs []string
|
||||||
|
sudoPassword string
|
||||||
|
passwordInput string
|
||||||
|
passwordError string
|
||||||
|
|
||||||
|
// Window manager states
|
||||||
|
hyprlandInstalled bool
|
||||||
|
niriInstalled bool
|
||||||
|
|
||||||
|
selectedGreeterItem int
|
||||||
|
greeterInstallChan chan greeterProgressMsg
|
||||||
|
greeterProgress greeterProgressMsg
|
||||||
|
greeterLogs []string
|
||||||
|
greeterPasswordInput string
|
||||||
|
greeterPasswordError string
|
||||||
|
greeterSudoPassword string
|
||||||
|
greeterCompositors []string
|
||||||
|
greeterSelectedComp int
|
||||||
|
greeterChosenCompositor string
|
||||||
|
|
||||||
|
pluginsMenuItems []MenuItem
|
||||||
|
selectedPluginsMenuItem int
|
||||||
|
pluginsList []pluginInfo
|
||||||
|
filteredPluginsList []pluginInfo
|
||||||
|
selectedPluginIndex int
|
||||||
|
pluginsLoading bool
|
||||||
|
pluginsError string
|
||||||
|
pluginSearchQuery string
|
||||||
|
installedPluginsList []pluginInfo
|
||||||
|
selectedInstalledIndex int
|
||||||
|
installedPluginsLoading bool
|
||||||
|
installedPluginsError string
|
||||||
|
pluginInstallStatus map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type pluginInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Category string
|
||||||
|
Author string
|
||||||
|
Description string
|
||||||
|
Repo string
|
||||||
|
Path string
|
||||||
|
Capabilities []string
|
||||||
|
Compositors []string
|
||||||
|
Dependencies []string
|
||||||
|
FirstParty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type MenuItem struct {
|
||||||
|
Label string
|
||||||
|
Action AppState
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModel(version string) Model {
|
||||||
|
detector, _ := NewDetector()
|
||||||
|
dependencies := detector.GetInstalledComponents()
|
||||||
|
|
||||||
|
// Use the proper detection method for both window managers
|
||||||
|
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to false if detection fails
|
||||||
|
hyprlandInstalled = false
|
||||||
|
niriInstalled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToggles := make(map[string]bool)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if dep.Name == "dms (DankMaterialShell)" && dep.Status == deps.StatusNeedsUpdate {
|
||||||
|
updateToggles[dep.Name] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := Model{
|
||||||
|
version: version,
|
||||||
|
detector: detector,
|
||||||
|
dependencies: dependencies,
|
||||||
|
state: StateMainMenu,
|
||||||
|
selectedItem: 0,
|
||||||
|
updateToggles: updateToggles,
|
||||||
|
updateDeps: dependencies,
|
||||||
|
updateProgressChan: make(chan updateProgressMsg, 100),
|
||||||
|
hyprlandInstalled: hyprlandInstalled,
|
||||||
|
niriInstalled: niriInstalled,
|
||||||
|
greeterInstallChan: make(chan greeterProgressMsg, 100),
|
||||||
|
pluginInstallStatus: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.menuItems = m.buildMenuItems()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) buildMenuItems() []MenuItem {
|
||||||
|
items := []MenuItem{
|
||||||
|
{Label: "Update", Action: StateUpdate},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shell management
|
||||||
|
if m.isShellRunning() {
|
||||||
|
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||||
|
} else {
|
||||||
|
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugins management
|
||||||
|
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||||
|
|
||||||
|
// Greeter management
|
||||||
|
items = append(items, MenuItem{Label: "Greeter", Action: StateGreeterMenu})
|
||||||
|
|
||||||
|
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||||
|
return []MenuItem{
|
||||||
|
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||||
|
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) isShellRunning() bool {
|
||||||
|
// Check for both -c and -p flag patterns since quickshell can be started either way
|
||||||
|
// -c dms: config name mode
|
||||||
|
// -p <path>/dms: path mode (used when installed via system packages)
|
||||||
|
cmd := exec.Command("pgrep", "-f", "qs.*dms")
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
case shellStartedMsg:
|
||||||
|
m.menuItems = m.buildMenuItems()
|
||||||
|
if m.selectedItem >= len(m.menuItems) {
|
||||||
|
m.selectedItem = len(m.menuItems) - 1
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case updateProgressMsg:
|
||||||
|
m.updateProgress = msg
|
||||||
|
if msg.logOutput != "" {
|
||||||
|
m.updateLogs = append(m.updateLogs, msg.logOutput)
|
||||||
|
}
|
||||||
|
return m, m.waitForProgress()
|
||||||
|
case updateCompleteMsg:
|
||||||
|
m.updateProgress.complete = true
|
||||||
|
m.updateProgress.err = msg.err
|
||||||
|
m.dependencies = m.detector.GetInstalledComponents()
|
||||||
|
m.updateDeps = m.dependencies
|
||||||
|
m.menuItems = m.buildMenuItems()
|
||||||
|
|
||||||
|
// Restart shell if update was successful and shell is running
|
||||||
|
if msg.err == nil && m.isShellRunning() {
|
||||||
|
restartShell()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case greeterProgressMsg:
|
||||||
|
m.greeterProgress = msg
|
||||||
|
if msg.logOutput != "" {
|
||||||
|
m.greeterLogs = append(m.greeterLogs, msg.logOutput)
|
||||||
|
}
|
||||||
|
return m, m.waitForGreeterProgress()
|
||||||
|
case pluginsLoadedMsg:
|
||||||
|
m.pluginsLoading = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.pluginsError = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||||
|
for i, p := range msg.plugins {
|
||||||
|
m.pluginsList[i] = pluginInfo{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
Category: p.Category,
|
||||||
|
Author: p.Author,
|
||||||
|
Description: p.Description,
|
||||||
|
Repo: p.Repo,
|
||||||
|
Path: p.Path,
|
||||||
|
Capabilities: p.Capabilities,
|
||||||
|
Compositors: p.Compositors,
|
||||||
|
Dependencies: p.Dependencies,
|
||||||
|
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.filteredPluginsList = m.pluginsList
|
||||||
|
m.selectedPluginIndex = 0
|
||||||
|
m.updatePluginInstallStatus()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case installedPluginsLoadedMsg:
|
||||||
|
m.installedPluginsLoading = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.installedPluginsError = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||||
|
for i, p := range msg.plugins {
|
||||||
|
m.installedPluginsList[i] = pluginInfo{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
Category: p.Category,
|
||||||
|
Author: p.Author,
|
||||||
|
Description: p.Description,
|
||||||
|
Repo: p.Repo,
|
||||||
|
Path: p.Path,
|
||||||
|
Capabilities: p.Capabilities,
|
||||||
|
Compositors: p.Compositors,
|
||||||
|
Dependencies: p.Dependencies,
|
||||||
|
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.selectedInstalledIndex = 0
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case pluginUninstalledMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.installedPluginsError = msg.err.Error()
|
||||||
|
m.state = StatePluginInstalledDetail
|
||||||
|
} else {
|
||||||
|
m.state = StatePluginsInstalled
|
||||||
|
m.installedPluginsLoading = true
|
||||||
|
m.installedPluginsError = ""
|
||||||
|
return m, loadInstalledPlugins
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case pluginInstalledMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.pluginsError = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.pluginInstallStatus[msg.pluginName] = true
|
||||||
|
m.pluginsError = ""
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case greeterPasswordValidMsg:
|
||||||
|
if msg.valid {
|
||||||
|
m.greeterSudoPassword = msg.password
|
||||||
|
m.greeterPasswordInput = ""
|
||||||
|
m.greeterPasswordError = ""
|
||||||
|
m.state = StateGreeterInstalling
|
||||||
|
m.greeterProgress = greeterProgressMsg{step: "Starting greeter installation..."}
|
||||||
|
m.greeterLogs = []string{}
|
||||||
|
return m, tea.Batch(m.performGreeterInstall(), m.waitForGreeterProgress())
|
||||||
|
} else {
|
||||||
|
m.greeterPasswordError = "Incorrect password. Please try again."
|
||||||
|
m.greeterPasswordInput = ""
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case passwordValidMsg:
|
||||||
|
if msg.valid {
|
||||||
|
m.sudoPassword = msg.password
|
||||||
|
m.passwordInput = ""
|
||||||
|
m.passwordError = ""
|
||||||
|
m.state = StateUpdateProgress
|
||||||
|
m.updateProgress = updateProgressMsg{progress: 0.0, step: "Starting update..."}
|
||||||
|
m.updateLogs = []string{}
|
||||||
|
return m, tea.Batch(m.performUpdate(), m.waitForProgress())
|
||||||
|
} else {
|
||||||
|
m.passwordError = "Incorrect password. Please try again."
|
||||||
|
m.passwordInput = ""
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch m.state {
|
||||||
|
case StateMainMenu:
|
||||||
|
return m.updateMainMenu(msg)
|
||||||
|
case StateUpdate:
|
||||||
|
return m.updateUpdateView(msg)
|
||||||
|
case StateUpdatePassword:
|
||||||
|
return m.updatePasswordView(msg)
|
||||||
|
case StateUpdateProgress:
|
||||||
|
return m.updateProgressView(msg)
|
||||||
|
case StateShell:
|
||||||
|
return m.updateShellView(msg)
|
||||||
|
case StatePluginsMenu:
|
||||||
|
return m.updatePluginsMenu(msg)
|
||||||
|
case StatePluginsBrowse:
|
||||||
|
return m.updatePluginsBrowse(msg)
|
||||||
|
case StatePluginDetail:
|
||||||
|
return m.updatePluginDetail(msg)
|
||||||
|
case StatePluginSearch:
|
||||||
|
return m.updatePluginSearch(msg)
|
||||||
|
case StatePluginsInstalled:
|
||||||
|
return m.updatePluginsInstalled(msg)
|
||||||
|
case StatePluginInstalledDetail:
|
||||||
|
return m.updatePluginInstalledDetail(msg)
|
||||||
|
case StateGreeterMenu:
|
||||||
|
return m.updateGreeterMenu(msg)
|
||||||
|
case StateGreeterCompositorSelect:
|
||||||
|
return m.updateGreeterCompositorSelect(msg)
|
||||||
|
case StateGreeterPassword:
|
||||||
|
return m.updateGreeterPasswordView(msg)
|
||||||
|
case StateGreeterInstalling:
|
||||||
|
return m.updateGreeterInstalling(msg)
|
||||||
|
case StateAbout:
|
||||||
|
return m.updateAboutView(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateProgressMsg struct {
|
||||||
|
progress float64
|
||||||
|
step string
|
||||||
|
complete bool
|
||||||
|
err error
|
||||||
|
logOutput string
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateCompleteMsg struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type passwordValidMsg struct {
|
||||||
|
password string
|
||||||
|
valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type greeterProgressMsg struct {
|
||||||
|
step string
|
||||||
|
complete bool
|
||||||
|
err error
|
||||||
|
logOutput string
|
||||||
|
}
|
||||||
|
|
||||||
|
type greeterPasswordValidMsg struct {
|
||||||
|
password string
|
||||||
|
valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) waitForProgress() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return <-m.updateProgressChan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) waitForGreeterProgress() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return <-m.greeterInstallChan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) View() string {
|
||||||
|
switch m.state {
|
||||||
|
case StateMainMenu:
|
||||||
|
return m.renderMainMenu()
|
||||||
|
case StateUpdate:
|
||||||
|
return m.renderUpdateView()
|
||||||
|
case StateUpdatePassword:
|
||||||
|
return m.renderPasswordView()
|
||||||
|
case StateUpdateProgress:
|
||||||
|
return m.renderProgressView()
|
||||||
|
case StateShell:
|
||||||
|
return m.renderShellView()
|
||||||
|
case StatePluginsMenu:
|
||||||
|
return m.renderPluginsMenu()
|
||||||
|
case StatePluginsBrowse:
|
||||||
|
return m.renderPluginsBrowse()
|
||||||
|
case StatePluginDetail:
|
||||||
|
return m.renderPluginDetail()
|
||||||
|
case StatePluginSearch:
|
||||||
|
return m.renderPluginSearch()
|
||||||
|
case StatePluginsInstalled:
|
||||||
|
return m.renderPluginsInstalled()
|
||||||
|
case StatePluginInstalledDetail:
|
||||||
|
return m.renderPluginInstalledDetail()
|
||||||
|
case StateGreeterMenu:
|
||||||
|
return m.renderGreeterMenu()
|
||||||
|
case StateGreeterCompositorSelect:
|
||||||
|
return m.renderGreeterCompositorSelect()
|
||||||
|
case StateGreeterPassword:
|
||||||
|
return m.renderGreeterPasswordView()
|
||||||
|
case StateGreeterInstalling:
|
||||||
|
return m.renderGreeterInstalling()
|
||||||
|
case StateAbout:
|
||||||
|
return m.renderAboutView()
|
||||||
|
default:
|
||||||
|
return m.renderMainMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
//go:build distro_binary
|
||||||
|
|
||||||
|
package dms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateMainMenu AppState = iota
|
||||||
|
StateShell
|
||||||
|
StatePluginsMenu
|
||||||
|
StatePluginsBrowse
|
||||||
|
StatePluginDetail
|
||||||
|
StatePluginSearch
|
||||||
|
StatePluginsInstalled
|
||||||
|
StatePluginInstalledDetail
|
||||||
|
StateAbout
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
version string
|
||||||
|
detector *Detector
|
||||||
|
dependencies []DependencyInfo
|
||||||
|
state AppState
|
||||||
|
selectedItem int
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
|
||||||
|
// Menu items
|
||||||
|
menuItems []MenuItem
|
||||||
|
|
||||||
|
// Window manager states
|
||||||
|
hyprlandInstalled bool
|
||||||
|
niriInstalled bool
|
||||||
|
|
||||||
|
pluginsMenuItems []MenuItem
|
||||||
|
selectedPluginsMenuItem int
|
||||||
|
pluginsList []pluginInfo
|
||||||
|
filteredPluginsList []pluginInfo
|
||||||
|
selectedPluginIndex int
|
||||||
|
pluginsLoading bool
|
||||||
|
pluginsError string
|
||||||
|
pluginSearchQuery string
|
||||||
|
installedPluginsList []pluginInfo
|
||||||
|
selectedInstalledIndex int
|
||||||
|
installedPluginsLoading bool
|
||||||
|
installedPluginsError string
|
||||||
|
pluginInstallStatus map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type pluginInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Category string
|
||||||
|
Author string
|
||||||
|
Description string
|
||||||
|
Repo string
|
||||||
|
Path string
|
||||||
|
Capabilities []string
|
||||||
|
Compositors []string
|
||||||
|
Dependencies []string
|
||||||
|
FirstParty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type MenuItem struct {
|
||||||
|
Label string
|
||||||
|
Action AppState
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModel(version string) Model {
|
||||||
|
detector, _ := NewDetector()
|
||||||
|
dependencies := detector.GetInstalledComponents()
|
||||||
|
|
||||||
|
// Use the proper detection method for both window managers
|
||||||
|
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to false if detection fails
|
||||||
|
hyprlandInstalled = false
|
||||||
|
niriInstalled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
m := Model{
|
||||||
|
version: version,
|
||||||
|
detector: detector,
|
||||||
|
dependencies: dependencies,
|
||||||
|
state: StateMainMenu,
|
||||||
|
selectedItem: 0,
|
||||||
|
hyprlandInstalled: hyprlandInstalled,
|
||||||
|
niriInstalled: niriInstalled,
|
||||||
|
pluginInstallStatus: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.menuItems = m.buildMenuItems()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) buildMenuItems() []MenuItem {
|
||||||
|
items := []MenuItem{}
|
||||||
|
|
||||||
|
// Shell management
|
||||||
|
if m.isShellRunning() {
|
||||||
|
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||||
|
} else {
|
||||||
|
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugins management
|
||||||
|
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||||
|
|
||||||
|
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||||
|
return []MenuItem{
|
||||||
|
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||||
|
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) isShellRunning() bool {
|
||||||
|
cmd := exec.Command("pgrep", "-f", "qs -c dms")
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
case pluginsLoadedMsg:
|
||||||
|
m.pluginsLoading = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.pluginsError = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||||
|
for i, p := range msg.plugins {
|
||||||
|
m.pluginsList[i] = pluginInfo{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
Category: p.Category,
|
||||||
|
Author: p.Author,
|
||||||
|
Description: p.Description,
|
||||||
|
Repo: p.Repo,
|
||||||
|
Path: p.Path,
|
||||||
|
Capabilities: p.Capabilities,
|
||||||
|
Compositors: p.Compositors,
|
||||||
|
Dependencies: p.Dependencies,
|
||||||
|
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.filteredPluginsList = m.pluginsList
|
||||||
|
m.selectedPluginIndex = 0
|
||||||
|
m.updatePluginInstallStatus()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case installedPluginsLoadedMsg:
|
||||||
|
m.installedPluginsLoading = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.installedPluginsError = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||||
|
for i, p := range msg.plugins {
|
||||||
|
m.installedPluginsList[i] = pluginInfo{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
Category: p.Category,
|
||||||
|
Author: p.Author,
|
||||||
|
Description: p.Description,
|
||||||
|
Repo: p.Repo,
|
||||||
|
Path: p.Path,
|
||||||
|
Capabilities: p.Capabilities,
|
||||||
|
Compositors: p.Compositors,
|
||||||
|
Dependencies: p.Dependencies,
|
||||||
|
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.selectedInstalledIndex = 0
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case pluginUninstalledMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.installedPluginsError = msg.err.Error()
|
||||||
|
m.state = StatePluginInstalledDetail
|
||||||
|
} else {
|
||||||
|
m.state = StatePluginsInstalled
|
||||||
|
m.installedPluginsLoading = true
|
||||||
|
m.installedPluginsError = ""
|
||||||
|
return m, loadInstalledPlugins
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case pluginInstalledMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.pluginsError = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.pluginInstallStatus[msg.pluginName] = true
|
||||||
|
m.pluginsError = ""
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch m.state {
|
||||||
|
case StateMainMenu:
|
||||||
|
return m.updateMainMenu(msg)
|
||||||
|
case StateShell:
|
||||||
|
return m.updateShellView(msg)
|
||||||
|
case StatePluginsMenu:
|
||||||
|
return m.updatePluginsMenu(msg)
|
||||||
|
case StatePluginsBrowse:
|
||||||
|
return m.updatePluginsBrowse(msg)
|
||||||
|
case StatePluginDetail:
|
||||||
|
return m.updatePluginDetail(msg)
|
||||||
|
case StatePluginSearch:
|
||||||
|
return m.updatePluginSearch(msg)
|
||||||
|
case StatePluginsInstalled:
|
||||||
|
return m.updatePluginsInstalled(msg)
|
||||||
|
case StatePluginInstalledDetail:
|
||||||
|
return m.updatePluginInstalledDetail(msg)
|
||||||
|
case StateAbout:
|
||||||
|
return m.updateAboutView(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) View() string {
|
||||||
|
switch m.state {
|
||||||
|
case StateMainMenu:
|
||||||
|
return m.renderMainMenu()
|
||||||
|
case StateShell:
|
||||||
|
return m.renderShellView()
|
||||||
|
case StatePluginsMenu:
|
||||||
|
return m.renderPluginsMenu()
|
||||||
|
case StatePluginsBrowse:
|
||||||
|
return m.renderPluginsBrowse()
|
||||||
|
case StatePluginDetail:
|
||||||
|
return m.renderPluginDetail()
|
||||||
|
case StatePluginSearch:
|
||||||
|
return m.renderPluginSearch()
|
||||||
|
case StatePluginsInstalled:
|
||||||
|
return m.renderPluginsInstalled()
|
||||||
|
case StatePluginInstalledDetail:
|
||||||
|
return m.renderPluginInstalledDetail()
|
||||||
|
case StateAbout:
|
||||||
|
return m.renderAboutView()
|
||||||
|
default:
|
||||||
|
return m.renderMainMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user