mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
Compare commits
337 Commits
monorepo
...
0ea0602aec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ea0602aec | ||
|
|
46effd2ca4 | ||
|
|
de055e8260 | ||
|
|
c3077304af | ||
|
|
e15135911f | ||
|
|
d430cae944 | ||
|
|
f92dc6f71b | ||
|
|
a679be68b1 | ||
|
|
c5c5ce8409 | ||
|
|
e7cb0d397e | ||
|
|
b84308cb49 | ||
|
|
0df47d2ce3 | ||
|
|
e24b548b54 | ||
|
|
75af444cee | ||
|
|
02dd19962f | ||
|
|
f552b8ef7b | ||
|
|
9162e31489 | ||
|
|
01b28e3ee8 | ||
|
|
f5aa855125 | ||
|
|
db3610fcdb | ||
|
|
2e3f330058 | ||
|
|
1617a7f2c1 | ||
|
|
69a5566bf9 | ||
|
|
30e5d8b855 | ||
|
|
67ff7726e0 | ||
|
|
f96a2e2325 | ||
|
|
344c4f9385 | ||
|
|
89aa146845 | ||
|
|
468e569bc7 | ||
|
|
139c99001a | ||
|
|
bd99be15c2 | ||
|
|
1d91d8fd94 | ||
|
|
f425f86101 | ||
|
|
83a6b7567f | ||
|
|
9184c70883 | ||
|
|
f5ca4ccce5 | ||
|
|
50f174be92 | ||
|
|
e5d11ce535 | ||
|
|
94851a51aa | ||
|
|
cfc07f4411 | ||
|
|
c6e9abda9f | ||
|
|
25951ddc55 | ||
|
|
bcd9ece077 | ||
|
|
68adbc38ba | ||
|
|
79a4d06cc0 | ||
|
|
18bf3b7548 | ||
|
|
4e66d3532e | ||
|
|
1b6d567451 | ||
|
|
7959a79575 | ||
|
|
abf3249b67 | ||
|
|
35e0dc84e8 | ||
|
|
17639e8729 | ||
|
|
cbd1fd908c | ||
|
|
b2cf20f3d8 | ||
|
|
915f1a5036 | ||
|
|
a55ec6416c | ||
|
|
b1834b1958 | ||
|
|
1beeb9fb55 | ||
|
|
18d86354ec | ||
|
|
6297b0679c | ||
|
|
d62ef635a7 | ||
|
|
c53836040f | ||
|
|
0b638bf85f | ||
|
|
7f6a71b964 | ||
|
|
1b4363a54a | ||
|
|
16d168c970 | ||
|
|
4606d7960e | ||
|
|
4eee126d26 | ||
|
|
dde426658f | ||
|
|
f6874fbcad | ||
|
|
621d4e4d92 | ||
|
|
76062231fd | ||
|
|
261f55fea5 | ||
|
|
202cf4bcc9 | ||
|
|
b7572f727f | ||
|
|
50ab346d58 | ||
|
|
b11b375848 | ||
|
|
e6c3ae9397 | ||
|
|
df663aceb9 | ||
|
|
db7e597f67 | ||
|
|
1d3fe81ff7 | ||
|
|
9c887fbe63 | ||
|
|
4723bffcd2 | ||
|
|
9643de3ca0 | ||
|
|
3bf3a54916 | ||
|
|
bcffc8856a | ||
|
|
6b8c35c27b | ||
|
|
dd409b4d1c | ||
|
|
94a1aebe2b | ||
|
|
d3030c3ec6 | ||
|
|
0221021078 | ||
|
|
966021bfd4 | ||
|
|
f06e6e85d5 | ||
|
|
28ad641070 | ||
|
|
384c775f1a | ||
|
|
ce40c691e9 | ||
|
|
5b0c38b0ed | ||
|
|
734456785f | ||
|
|
4f24312432 | ||
|
|
d79b1ff3b4 | ||
|
|
bbe1c1f1e0 | ||
|
|
1978e67401 | ||
|
|
e129e4a2d0 | ||
|
|
f7f1bbbdd2 | ||
|
|
de8f2e6a68 | ||
|
|
85704e3947 | ||
|
|
4d661ff41d | ||
|
|
d7b39634e6 | ||
|
|
039c98b9e3 | ||
|
|
172c4bf0a9 | ||
|
|
1f2a1c5dec | ||
|
|
e5a6a00282 | ||
|
|
d8153f7611 | ||
|
|
8b6ae3f39b | ||
|
|
24537781b7 | ||
|
|
d2a29506aa | ||
|
|
adf51d5264 | ||
|
|
0864179085 | ||
|
|
8de77f283d | ||
|
|
004a014000 | ||
|
|
80f6eb94aa | ||
|
|
4035c9cc5f | ||
|
|
3a365f6807 | ||
|
|
9920a0a59f | ||
|
|
c17bb9e171 | ||
|
|
03073f6875 | ||
|
|
609caf6e5f | ||
|
|
411141ff88 | ||
|
|
3e472e18bd | ||
|
|
e5b6fbd12a | ||
|
|
c2787f1282 | ||
|
|
df940124b1 | ||
|
|
5288d042ca | ||
|
|
fa98a27c90 | ||
|
|
d341a5a60b | ||
|
|
7f15227de1 | ||
|
|
bb45240665 | ||
|
|
29f84aeab5 | ||
|
|
5a52edcad8 | ||
|
|
b078e23aa1 | ||
|
|
7fa87125b5 | ||
|
|
f618df46d8 | ||
|
|
ee03853901 | ||
|
|
6c4a9bcfb8 | ||
|
|
1bec20ecef | ||
|
|
08c9bf570d | ||
|
|
5e77a10a81 | ||
|
|
3bc6461e2a | ||
|
|
d3194e15e2 | ||
|
|
2db79ef202 | ||
|
|
b3c07edef6 | ||
|
|
b773fdca34 | ||
|
|
2e9f9f7b7e | ||
|
|
30cbfe729d | ||
|
|
b036da2446 | ||
|
|
c8a9fb1674 | ||
|
|
43bea80cad | ||
|
|
23538c0323 | ||
|
|
2ae911230d | ||
|
|
5ce1cb87ea | ||
|
|
2a37028b6a | ||
|
|
8130feb2a0 | ||
|
|
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 |
@@ -1,8 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env 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)"
|
||||||
@@ -10,18 +6,61 @@ REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
|||||||
|
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
if [[ -z "${POEDITOR_API_TOKEN:-}" ]] || [[ -z "${POEDITOR_PROJECT_ID:-}" ]]; then
|
# =============================================================================
|
||||||
exit 0
|
# Go CI checks (when core/ files are staged)
|
||||||
fi
|
# =============================================================================
|
||||||
|
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
||||||
|
|
||||||
if ! command -v python3 &>/dev/null; then
|
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
||||||
exit 0
|
echo "Go files staged in core/, running CI checks..."
|
||||||
fi
|
cd "$REPO_ROOT/core"
|
||||||
|
|
||||||
if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
# Format check
|
||||||
echo "Translations out of sync"
|
echo " Checking gofmt..."
|
||||||
echo "run python3 scripts/i18nsync.py sync"
|
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
||||||
|
if [[ -n "$UNFORMATTED" ]]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
echo "$UNFORMATTED"
|
||||||
|
echo ""
|
||||||
|
echo "Run: cd core && gofmt -s -w ."
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# golangci-lint
|
||||||
|
if command -v golangci-lint &>/dev/null; then
|
||||||
|
echo " Running golangci-lint..."
|
||||||
|
golangci-lint run ./...
|
||||||
|
else
|
||||||
|
echo " Warning: golangci-lint not installed, skipping lint"
|
||||||
|
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
echo " Running tests..."
|
||||||
|
go test ./... >/dev/null
|
||||||
|
|
||||||
|
# Build checks
|
||||||
|
echo " Building..."
|
||||||
|
mkdir -p bin
|
||||||
|
go build -buildvcs=false -o bin/dms ./cmd/dms
|
||||||
|
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
|
||||||
|
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
|
||||||
|
|
||||||
|
echo "All Go CI checks passed!"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# i18n sync check (DISABLED for now)
|
||||||
|
# =============================================================================
|
||||||
|
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
|
||||||
|
# if command -v python3 &>/dev/null; then
|
||||||
|
# if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
||||||
|
# echo "Translations out of sync"
|
||||||
|
# echo "Run: python3 scripts/i18nsync.py sync"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
30
.github/workflows/go-ci.yml
vendored
30
.github/workflows/go-ci.yml
vendored
@@ -3,17 +3,26 @@ name: Go CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- "**"
|
||||||
paths:
|
paths:
|
||||||
- 'backend/**'
|
- "core/**"
|
||||||
- '.github/workflows/go-ci.yml'
|
- ".github/workflows/go-ci.yml"
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
paths:
|
||||||
|
- "core/**"
|
||||||
|
- ".github/workflows/go-ci.yml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: go-ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
lint-and-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: backend
|
working-directory: core
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -22,7 +31,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./backend/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Format check
|
- name: Format check
|
||||||
run: |
|
run: |
|
||||||
@@ -32,11 +41,20 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: v2.6
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
- name: Build dms
|
- name: Build dms
|
||||||
run: go build -v ./cmd/dms
|
run: go build -v ./cmd/dms
|
||||||
|
|
||||||
|
- name: Build dms (distropkg)
|
||||||
|
run: go build -v -tags distro_binary ./cmd/dms
|
||||||
|
|
||||||
- name: Build dankinstall
|
- name: Build dankinstall
|
||||||
run: go build -v ./cmd/dankinstall
|
run: go build -v ./cmd/dankinstall
|
||||||
|
|||||||
197
.github/workflows/release.yml
vendored
197
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-backend:
|
build-core:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: backend
|
working-directory: core
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -33,7 +33,15 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./backend/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: |
|
||||||
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
|
echo "The following files are not formatted:"
|
||||||
|
gofmt -s -l .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
@@ -93,71 +101,75 @@ jobs:
|
|||||||
if: matrix.arch == 'arm64'
|
if: matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: backend-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
backend/dankinstall-${{ matrix.arch }}.gz
|
core/dankinstall-${{ matrix.arch }}.gz
|
||||||
backend/dankinstall-${{ matrix.arch }}.gz.sha256
|
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
backend/dms-${{ matrix.arch }}.gz
|
core/dms-${{ matrix.arch }}.gz
|
||||||
backend/dms-${{ matrix.arch }}.gz.sha256
|
core/dms-${{ matrix.arch }}.gz.sha256
|
||||||
backend/dms-distropkg-${{ matrix.arch }}.gz
|
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||||
backend/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: backend-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
backend/dankinstall-${{ matrix.arch }}.gz
|
core/dankinstall-${{ matrix.arch }}.gz
|
||||||
backend/dankinstall-${{ matrix.arch }}.gz.sha256
|
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||||
backend/dms-${{ matrix.arch }}.gz
|
core/dms-${{ matrix.arch }}.gz
|
||||||
backend/dms-${{ matrix.arch }}.gz.sha256
|
core/dms-${{ matrix.arch }}.gz.sha256
|
||||||
backend/dms-distropkg-${{ matrix.arch }}.gz
|
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||||
backend/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||||
backend/completion.bash
|
core/completion.bash
|
||||||
backend/completion.fish
|
core/completion.fish
|
||||||
backend/completion.zsh
|
core/completion.zsh
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
update-versions:
|
update-versions:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-core
|
||||||
steps:
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Update VERSION and flake.nix
|
- name: Update VERSION
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "dms-ci[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
version="${GITHUB_REF#refs/tags/}"
|
version="${GITHUB_REF#refs/tags/}"
|
||||||
version_no_v="${version#v}"
|
|
||||||
echo "Updating to version: $version"
|
echo "Updating to version: $version"
|
||||||
|
|
||||||
# Update VERSION file in quickshell/
|
|
||||||
echo "${version}" > quickshell/VERSION
|
echo "${version}" > quickshell/VERSION
|
||||||
|
git add quickshell/VERSION
|
||||||
# Update version in backend/flake.nix
|
|
||||||
sed -i "s/version = \"[^\"]*\"/version = \"$version_no_v\"/" backend/flake.nix
|
|
||||||
|
|
||||||
git add quickshell/VERSION backend/flake.nix
|
|
||||||
|
|
||||||
if ! git diff --cached --quiet; then
|
if ! git diff --cached --quiet; then
|
||||||
git commit -m "chore: bump version to $version"
|
git commit -m "chore: bump version to $version"
|
||||||
git push origin HEAD:master || git push origin HEAD:main
|
git pull --rebase origin master
|
||||||
echo "Pushed version updates to master"
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||||
else
|
|
||||||
echo "No version changes needed"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
git tag -f "${version}"
|
||||||
|
git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: build-backend
|
needs: [build-core, update-versions]
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
@@ -166,12 +178,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download backend artifacts
|
- 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
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: backend-assets-*
|
pattern: core-assets-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./_backend_assets
|
path: ./_core_assets
|
||||||
|
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
@@ -233,8 +250,8 @@ jobs:
|
|||||||
|
|
||||||
mkdir -p _release_assets
|
mkdir -p _release_assets
|
||||||
|
|
||||||
# Copy backend binaries and rename dms-*.gz to dms-cli-*.gz
|
# Copy core binaries and rename dms-*.gz to dms-cli-*.gz
|
||||||
for file in _backend_assets/dms-*.gz*; do
|
for file in _core_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
|
||||||
@@ -247,12 +264,15 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Copy dankinstall binaries
|
# Copy dankinstall binaries
|
||||||
cp _backend_assets/dankinstall-*.gz* _release_assets/
|
cp _core_assets/dankinstall-*.gz* _release_assets/
|
||||||
|
|
||||||
# Copy completions
|
# Copy completions
|
||||||
cp _backend_assets/completion.* _release_assets/ 2>/dev/null || true
|
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||||
|
|
||||||
# Create QML source package (exclude build artifacts and git files)
|
# Create QML source package (exclude build artifacts and git files)
|
||||||
|
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||||
|
cp LICENSE CONTRIBUTING.md quickshell/
|
||||||
|
|
||||||
# Tar the CONTENTS of quickshell/, not the directory itself
|
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||||
(cd quickshell && tar --exclude='.git' \
|
(cd quickshell && tar --exclude='.git' \
|
||||||
--exclude='.github' \
|
--exclude='.github' \
|
||||||
@@ -272,23 +292,28 @@ jobs:
|
|||||||
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
||||||
|
|
||||||
# Add CLI binaries
|
# Add CLI binaries
|
||||||
if [ -f "_backend_assets/dms-${arch}.gz" ]; then
|
if [ -f "_core_assets/dms-${arch}.gz" ]; then
|
||||||
gunzip -c "_backend_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
|
||||||
|
|
||||||
if [ -f "_backend_assets/dms-distropkg-${arch}.gz" ]; then
|
if [ -f "_core_assets/dms-distropkg-${arch}.gz" ]; then
|
||||||
gunzip -c "_backend_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
gunzip -c "_core_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
|
||||||
|
|
||||||
# Add shell completions
|
# Add shell completions
|
||||||
for completion in _backend_assets/completion.*; do
|
for completion in _core_assets/completion.*; do
|
||||||
if [ -f "$completion" ]; then
|
if [ -f "$completion" ]; then
|
||||||
cp "$completion" _temp_full/completions/
|
cp "$completion" _temp_full/completions/
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Copy docs directory
|
||||||
|
if [ -d "docs" ]; then
|
||||||
|
cp -r docs _temp_full/
|
||||||
|
fi
|
||||||
|
|
||||||
# Create installation guide
|
# Create installation guide
|
||||||
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
|
cat > _temp_full/INSTALL.md << 'EOFINSTALL'
|
||||||
# DankMaterialShell Installation
|
# DankMaterialShell Installation
|
||||||
@@ -337,8 +362,7 @@ 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
|
||||||
EOFINSTALL
|
EOFINSTALL
|
||||||
|
|
||||||
@@ -364,6 +388,68 @@ jobs:
|
|||||||
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:
|
copr-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: release
|
needs: release
|
||||||
@@ -511,7 +597,10 @@ jobs:
|
|||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
|
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||||
@@ -521,6 +610,8 @@ jobs:
|
|||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
|
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
||||||
|
|
||||||
%posttrans
|
%posttrans
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
@@ -537,6 +628,8 @@ jobs:
|
|||||||
%doc README.md CONTRIBUTING.md
|
%doc README.md CONTRIBUTING.md
|
||||||
%{_datadir}/quickshell/dms/
|
%{_datadir}/quickshell/dms/
|
||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
|
%{_datadir}/applications/dms-open.desktop
|
||||||
|
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
%files -n dms-cli
|
%files -n dms-cli
|
||||||
%{_bindir}/dms
|
%{_bindir}/dms
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: DMS Copr Stable Release (Manual)
|
name: DMS Copr Stable Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -7,6 +7,10 @@ on:
|
|||||||
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:
|
||||||
@@ -19,6 +23,7 @@ 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"
|
||||||
@@ -27,8 +32,14 @@ jobs:
|
|||||||
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: |
|
||||||
@@ -57,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'
|
||||||
@@ -68,7 +80,7 @@ jobs:
|
|||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Version: %{version}
|
Version: %{version}
|
||||||
Release: 1%{?dist}
|
Release: RELEASE_PLACEHOLDER%{?dist}
|
||||||
Summary: %{pkg_summary}
|
Summary: %{pkg_summary}
|
||||||
|
|
||||||
License: MIT
|
License: MIT
|
||||||
@@ -212,16 +224,17 @@ jobs:
|
|||||||
%{_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
|
||||||
@@ -295,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
|
||||||
276
.github/workflows/run-obs.yml
vendored
Normal file
276
.github/workflows/run-obs.yml
vendored
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
name: Update OBS Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to update (dms, dms-git, or all)'
|
||||||
|
required: false
|
||||||
|
default: 'all'
|
||||||
|
rebuild_release:
|
||||||
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-updates:
|
||||||
|
name: Check for updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
|
version: ${{ steps.check.outputs.version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Check for updates
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by tag: $VERSION (always update)"
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Checking if dms-git source has changed..."
|
||||||
|
|
||||||
|
# Get current commit hash (8 chars to match spec format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Check OBS for last uploaded commit
|
||||||
|
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||||
|
mkdir -p "$OBS_BASE"
|
||||||
|
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||||
|
|
||||||
|
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||||
|
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||||
|
osc up -q 2>/dev/null || true
|
||||||
|
|
||||||
|
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
||||||
|
if [[ -f "dms-git.spec" ]]; then
|
||||||
|
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$OBS_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract OBS commit, proceeding with update"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No spec file in OBS, proceeding with update"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${{ github.workspace }}"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 First upload to OBS, update needed"
|
||||||
|
fi
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=all" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
update-obs:
|
||||||
|
name: Upload to OBS
|
||||||
|
needs: check-updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine packages to update
|
||||||
|
id: packages
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by tag: $VERSION"
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by schedule: updating git package"
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update dms-git spec version
|
||||||
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
|
run: |
|
||||||
|
# Get commit info for dms-git versioning
|
||||||
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||||
|
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
|
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Update version in spec
|
||||||
|
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
|
# Add changelog entry
|
||||||
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
|
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||||
|
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
|
- name: Update Debian dms-git changelog version
|
||||||
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
|
run: |
|
||||||
|
# Get commit info for dms-git versioning
|
||||||
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||||
|
|
||||||
|
# Debian version format: 0.6.2+git2256.9162e314
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
|
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
|
||||||
|
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
# Get current version from changelog
|
||||||
|
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
||||||
|
|
||||||
|
echo "Current Debian version: $CURRENT_VERSION"
|
||||||
|
echo "New version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Only update if version changed
|
||||||
|
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||||
|
# Create new changelog entry at top
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
|
||||||
|
cat > "$TEMP_CHANGELOG" << EOF
|
||||||
|
dms-git ($NEW_VERSION) nightly; urgency=medium
|
||||||
|
|
||||||
|
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
||||||
|
|
||||||
|
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Prepend to existing changelog
|
||||||
|
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
||||||
|
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
||||||
|
else
|
||||||
|
echo "✓ Debian changelog already at version $NEW_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update dms stable version
|
||||||
|
if: steps.packages.outputs.version != ''
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
|
VERSION_NO_V="${VERSION#v}"
|
||||||
|
echo "Updating packaging to version $VERSION_NO_V"
|
||||||
|
|
||||||
|
# Update openSUSE dms spec (stable only)
|
||||||
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||||
|
|
||||||
|
# Update Debian _service files
|
||||||
|
for service in distro/debian/*/_service; do
|
||||||
|
if [[ -f "$service" ]]; then
|
||||||
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Install OSC
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y osc
|
||||||
|
|
||||||
|
mkdir -p ~/.config/osc
|
||||||
|
cat > ~/.config/osc/oscrc << EOF
|
||||||
|
[general]
|
||||||
|
apiurl = https://api.opensuse.org
|
||||||
|
|
||||||
|
[https://api.opensuse.org]
|
||||||
|
user = ${{ secrets.OBS_USERNAME }}
|
||||||
|
pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
|
- name: Upload to OBS
|
||||||
|
env:
|
||||||
|
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
MESSAGE="Automated update from GitHub Actions"
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
||||||
|
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
||||||
|
else
|
||||||
|
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
||||||
188
.github/workflows/run-ppa.yml
vendored
Normal file
188
.github/workflows/run-ppa.yml
vendored
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
name: Update PPA Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
|
||||||
|
required: false
|
||||||
|
default: 'dms-git'
|
||||||
|
rebuild_release:
|
||||||
|
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-updates:
|
||||||
|
name: Check for updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check for updates
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Checking if dms-git source has changed..."
|
||||||
|
|
||||||
|
# Get current commit hash (8 chars to match changelog format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Extract commit hash from changelog
|
||||||
|
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
||||||
|
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
if [[ -f "$CHANGELOG_FILE" ]]; then
|
||||||
|
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No changelog file found, proceeding with upload"
|
||||||
|
fi
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
upload-ppa:
|
||||||
|
name: Upload to PPA
|
||||||
|
needs: check-updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
debhelper \
|
||||||
|
devscripts \
|
||||||
|
dput \
|
||||||
|
lftp \
|
||||||
|
build-essential \
|
||||||
|
fakeroot \
|
||||||
|
dpkg-dev
|
||||||
|
|
||||||
|
- name: Configure GPG
|
||||||
|
env:
|
||||||
|
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "$GPG_KEY" | gpg --import
|
||||||
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Determine packages to upload
|
||||||
|
id: packages
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Triggered by schedule: uploading git package"
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload to PPA
|
||||||
|
env:
|
||||||
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
run: |
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms to PPA..."
|
||||||
|
if [ -n "$REBUILD_RELEASE" ]; then
|
||||||
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-git to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading dms-greeter to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
|
||||||
|
else
|
||||||
|
PPA_NAME="$PACKAGES"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading $PACKAGES to PPA..."
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
|
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
||||||
|
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||||
82
.github/workflows/update-vendor-hash.yml
vendored
82
.github/workflows/update-vendor-hash.yml
vendored
@@ -1,90 +1,66 @@
|
|||||||
name: Update Vendor Hash
|
name: Update Vendor Hash
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- "backend/go.mod"
|
- "core/go.mod"
|
||||||
- "backend/go.sum"
|
- "core/go.sum"
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-vendor-hash:
|
update-vendor-hash:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
- name: Install Nix
|
- name: Install Nix
|
||||||
uses: cachix/install-nix-action@v31
|
uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
- name: Update vendorHash in backend/flake.nix
|
- name: Update vendorHash in flake.nix
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Try to build and capture the expected hash from error message
|
|
||||||
echo "Attempting nix build to get new vendorHash..."
|
echo "Attempting nix build to get new vendorHash..."
|
||||||
cd backend
|
if output=$(nix build .#dmsCli 2>&1); then
|
||||||
if output=$(nix build .#dms-cli 2>&1); then
|
|
||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract the expected hash from the error message
|
|
||||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
||||||
|
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||||
if [ -z "$new_hash" ]; then
|
|
||||||
echo "Could not extract new vendorHash from build output"
|
|
||||||
echo "Build output:"
|
|
||||||
echo "$output"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "New vendorHash: $new_hash"
|
|
||||||
|
|
||||||
# Get current hash from flake.nix
|
|
||||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
echo "Current vendorHash: $current_hash"
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
|
|
||||||
if [ "$current_hash" = "$new_hash" ]; then
|
|
||||||
echo "vendorHash is already up to date"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update the hash in flake.nix
|
|
||||||
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||||
|
|
||||||
# Verify the build works with the new hash
|
|
||||||
echo "Verifying build with new vendorHash..."
|
echo "Verifying build with new vendorHash..."
|
||||||
nix build .#dms-cli
|
nix build .#dmsCli
|
||||||
|
|
||||||
echo "vendorHash updated successfully!"
|
echo "vendorHash updated successfully!"
|
||||||
|
|
||||||
- name: Commit and push vendorHash update
|
- name: Commit and push vendorHash update
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
if ! git diff --quiet flake.nix; then
|
||||||
if ! git diff --quiet backend/flake.nix; then
|
git config user.name "dms-ci[bot]"
|
||||||
git config user.name "github-actions[bot]"
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git add flake.nix
|
||||||
|
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||||
git add backend/flake.nix
|
git pull --rebase origin master
|
||||||
git commit -m "flake: update vendorHash for go.mod changes"
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||||
|
|
||||||
for attempt in 1 2 3; do
|
|
||||||
if git push; then
|
|
||||||
echo "Successfully pushed vendorHash update"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Push attempt $attempt failed, pulling and retrying..."
|
|
||||||
git pull --rebase
|
|
||||||
sleep $((attempt*2))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Failed to push after retries" >&2
|
|
||||||
exit 1
|
|
||||||
else
|
else
|
||||||
echo "No changes to backend/flake.nix"
|
echo "No changes to flake.nix"
|
||||||
fi
|
fi
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -27,7 +27,6 @@ qrc_*.cpp
|
|||||||
ui_*.h
|
ui_*.h
|
||||||
*.qmlc
|
*.qmlc
|
||||||
*.jsc
|
*.jsc
|
||||||
Makefile*
|
|
||||||
*build-*
|
*build-*
|
||||||
*.qm
|
*.qm
|
||||||
*.prl
|
*.prl
|
||||||
@@ -137,3 +136,9 @@ go.work.sum
|
|||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
|
# Extracted source trees in Ubuntu package directories
|
||||||
|
distro/ubuntu/*/dms-git-repo/
|
||||||
|
distro/ubuntu/*/DankMaterialShell-*/
|
||||||
|
distro/ubuntu/danklinux/*/dsearch-*/
|
||||||
|
distro/ubuntu/danklinux/*/dgop-*/
|
||||||
|
|||||||
@@ -2,28 +2,50 @@
|
|||||||
|
|
||||||
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.
|
## Setup
|
||||||
|
|
||||||
We need some consistent style, so this at least gives the same formatter that Qt Creator uses.
|
Enable pre-commit hooks to catch CI failures before pushing:
|
||||||
|
|
||||||
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json.
|
```bash
|
||||||
|
git config core.hooksPath .githooks
|
||||||
```json
|
|
||||||
"customLocalFormatters.formatters": [
|
|
||||||
{
|
|
||||||
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
|
|
||||||
"languages": ["qml"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"[qml]": {
|
|
||||||
"editor.defaultFormatter": "jkillian.custom-local-formatters",
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave.
|
## VSCode Setup
|
||||||
|
|
||||||
|
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
||||||
|
|
||||||
|
### QML (`quickshell` directory)
|
||||||
|
|
||||||
|
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
|
||||||
|
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"qt-qml.doNotAskForQmllsDownload": true,
|
||||||
|
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create empty `.qmlls.ini` file in `quickshell/` directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd quickshell
|
||||||
|
touch .qmlls.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
|
### GO (`core` directory)
|
||||||
|
|
||||||
|
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||||
|
2. Ensure code is formatted with `make fmt`
|
||||||
|
3. Add appropriate test coverage and ensure tests pass with `make test`
|
||||||
|
4. Run `go mod tidy`
|
||||||
|
5. Open pull request
|
||||||
|
|
||||||
## Pull request
|
## Pull request
|
||||||
|
|
||||||
|
|||||||
156
Makefile
Normal file
156
Makefile
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Root Makefile for DankMaterialShell (DMS)
|
||||||
|
# Orchestrates building, installation, and systemd management
|
||||||
|
|
||||||
|
# Build configuration
|
||||||
|
BINARY_NAME=dms
|
||||||
|
CORE_DIR=core
|
||||||
|
BUILD_DIR=$(CORE_DIR)/bin
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
INSTALL_DIR=$(PREFIX)/bin
|
||||||
|
DATA_DIR=$(PREFIX)/share
|
||||||
|
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
||||||
|
|
||||||
|
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
||||||
|
SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user
|
||||||
|
|
||||||
|
SHELL_DIR=quickshell
|
||||||
|
SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||||
|
ASSETS_DIR=assets
|
||||||
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
|
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
@echo "Building $(BINARY_NAME)..."
|
||||||
|
@$(MAKE) -C $(CORE_DIR) build
|
||||||
|
@echo "Build complete"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
# Installation targets
|
||||||
|
install-bin:
|
||||||
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Binary installed"
|
||||||
|
|
||||||
|
install-shell:
|
||||||
|
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
||||||
|
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||||
|
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||||
|
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||||
|
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
||||||
|
@echo "Shell files installed"
|
||||||
|
|
||||||
|
install-completions:
|
||||||
|
@echo "Installing shell completions..."
|
||||||
|
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
||||||
|
@mkdir -p $(DATA_DIR)/zsh/site-functions
|
||||||
|
@mkdir -p $(DATA_DIR)/fish/vendor_completions.d
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion bash > $(DATA_DIR)/bash-completion/completions/dms 2>/dev/null || true
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion zsh > $(DATA_DIR)/zsh/site-functions/_dms 2>/dev/null || true
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion fish > $(DATA_DIR)/fish/vendor_completions.d/dms.fish 2>/dev/null || true
|
||||||
|
@echo "Shell completions installed"
|
||||||
|
|
||||||
|
install-systemd:
|
||||||
|
@echo "Installing systemd user service..."
|
||||||
|
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||||
|
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
|
||||||
|
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||||
|
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||||
|
|
||||||
|
install-icon:
|
||||||
|
@echo "Installing icon..."
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg
|
||||||
|
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||||
|
@echo "Icon installed"
|
||||||
|
|
||||||
|
install-desktop:
|
||||||
|
@echo "Installing desktop entry..."
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
|
@echo "Desktop entry installed"
|
||||||
|
|
||||||
|
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
|
@echo ""
|
||||||
|
@echo "Installation complete!"
|
||||||
|
@echo ""
|
||||||
|
@echo "=== The DMS Team! ==="
|
||||||
|
|
||||||
|
# Uninstallation targets
|
||||||
|
uninstall-bin:
|
||||||
|
@echo "Removing $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||||
|
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Binary removed"
|
||||||
|
|
||||||
|
uninstall-shell:
|
||||||
|
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
|
||||||
|
@rm -rf $(SHELL_INSTALL_DIR)
|
||||||
|
@echo "Shell files removed"
|
||||||
|
|
||||||
|
uninstall-completions:
|
||||||
|
@echo "Removing shell completions..."
|
||||||
|
@rm -f $(DATA_DIR)/bash-completion/completions/dms
|
||||||
|
@rm -f $(DATA_DIR)/zsh/site-functions/_dms
|
||||||
|
@rm -f $(DATA_DIR)/fish/vendor_completions.d/dms.fish
|
||||||
|
@echo "Shell completions removed"
|
||||||
|
|
||||||
|
uninstall-systemd:
|
||||||
|
@echo "Removing systemd user service..."
|
||||||
|
@rm -f $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@echo "Systemd service removed"
|
||||||
|
@echo "Note: Stop/disable service manually if running: systemctl --user stop dms"
|
||||||
|
|
||||||
|
uninstall-icon:
|
||||||
|
@echo "Removing icon..."
|
||||||
|
@rm -f $(ICON_DIR)/danklogo.svg
|
||||||
|
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||||
|
@echo "Icon removed"
|
||||||
|
|
||||||
|
uninstall-desktop:
|
||||||
|
@echo "Removing desktop entry..."
|
||||||
|
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
|
@echo "Desktop entry removed"
|
||||||
|
|
||||||
|
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
|
||||||
|
@echo ""
|
||||||
|
@echo "Uninstallation complete!"
|
||||||
|
|
||||||
|
# Target assist
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo ""
|
||||||
|
@echo "Build:"
|
||||||
|
@echo " all (default) - Build the DMS binary"
|
||||||
|
@echo " build - Same as 'all'"
|
||||||
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo ""
|
||||||
|
@echo "Install:"
|
||||||
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
@echo " install-bin - Install only the binary"
|
||||||
|
@echo " install-shell - Install only shell files"
|
||||||
|
@echo " install-completions - Install only shell completions"
|
||||||
|
@echo " install-systemd - Install only systemd service"
|
||||||
|
@echo " install-icon - Install only icon"
|
||||||
|
@echo " install-desktop - Install only desktop entry"
|
||||||
|
@echo ""
|
||||||
|
@echo "Uninstall:"
|
||||||
|
@echo " uninstall - Remove everything (requires sudo)"
|
||||||
|
@echo " uninstall-bin - Remove only the binary"
|
||||||
|
@echo " uninstall-shell - Remove only shell files"
|
||||||
|
@echo " uninstall-completions - Remove only shell completions"
|
||||||
|
@echo " uninstall-systemd - Remove only systemd service"
|
||||||
|
@echo " uninstall-icon - Remove only icon"
|
||||||
|
@echo " uninstall-desktop - Remove only desktop entry"
|
||||||
|
@echo ""
|
||||||
|
@echo "Usage:"
|
||||||
|
@echo " sudo make install - Build and install DMS"
|
||||||
|
@echo " sudo make uninstall - Remove DMS"
|
||||||
|
@echo " systemctl --user enable --now dms - Enable and start service"
|
||||||
29
README.md
29
README.md
@@ -15,15 +15,15 @@
|
|||||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||||
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||||
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||||
[](https://ko-fi.com/avengemediallc)
|
[](https://ko-fi.com/danklinux)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [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.
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
This is a monorepo containing both the shell interface and backend services:
|
This is a monorepo containing both the shell interface and the core backend services:
|
||||||
|
|
||||||
```
|
```
|
||||||
DankMaterialShell/
|
DankMaterialShell/
|
||||||
@@ -32,12 +32,14 @@ DankMaterialShell/
|
|||||||
│ ├── Services/ # System integration (audio, network, bluetooth)
|
│ ├── Services/ # System integration (audio, network, bluetooth)
|
||||||
│ ├── Widgets/ # Reusable UI controls
|
│ ├── Widgets/ # Reusable UI controls
|
||||||
│ └── Common/ # Shared resources and themes
|
│ └── Common/ # Shared resources and themes
|
||||||
├── backend/ # Go backend and CLI
|
├── core/ # Go backend and CLI
|
||||||
│ ├── cmd/ # dms CLI and dankinstall binaries
|
│ ├── cmd/ # dms CLI and dankinstall binaries
|
||||||
│ ├── internal/ # System integration, IPC, distro support
|
│ ├── internal/ # System integration, IPC, distro support
|
||||||
│ └── pkg/ # Shared packages
|
│ └── pkg/ # Shared packages
|
||||||
├── distro/ # Distribution packaging (Fedora RPM specs)
|
├── distro/ # Distribution packaging
|
||||||
├── nix/ # NixOS/home-manager modules
|
│ ├── fedora/ # Fedora RPM specs
|
||||||
|
│ ├── debian/ # Debian packaging
|
||||||
|
│ └── nix/ # NixOS/home-manager modules
|
||||||
└── flake.nix # Nix flake for declarative installation
|
└── flake.nix # Nix flake for declarative installation
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
|||||||
|
|
||||||
## Supported Compositors
|
## Supported Compositors
|
||||||
|
|
||||||
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), and [MangoWC](https://github.com/DreamMaoMao/mangowc) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [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.
|
||||||
|
|
||||||
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
||||||
|
|
||||||
@@ -135,15 +137,14 @@ dms plugins search # Browse plugin registry
|
|||||||
See component-specific documentation:
|
See component-specific documentation:
|
||||||
|
|
||||||
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
|
- **[quickshell/](quickshell/)** - QML shell development, widgets, and modules
|
||||||
- **[backend/](backend/)** - Go backend, CLI tools, and system integration
|
- **[core/](core/)** - Go backend, CLI tools, and system integration
|
||||||
- **[distro/](distro/)** - Distribution packaging
|
- **[distro/](distro/)** - Distribution packaging (Fedora, Debian, NixOS)
|
||||||
- **[nix/](nix/)** - NixOS and home-manager modules
|
|
||||||
|
|
||||||
### Building from Source
|
### Building from Source
|
||||||
|
|
||||||
**Backend:**
|
**Core + Dankinstall:**
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd core
|
||||||
make # Build dms CLI
|
make # Build dms CLI
|
||||||
make dankinstall # Build installer
|
make dankinstall # Build installer
|
||||||
```
|
```
|
||||||
@@ -182,6 +183,10 @@ For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeM
|
|||||||
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
|
- [soramanew](https://github.com/soramanew) - [Caelestia](https://github.com/caelestia-dots/shell) inspiration
|
||||||
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
|
- [end-4](https://github.com/end-4) - [dots-hyprland](https://github.com/end-4/dots-hyprland) inspiration
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - See [LICENSE](LICENSE) for details.
|
MIT License - See [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
10
assets/dms-open.desktop
Normal file
10
assets/dms-open.desktop
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=DMS Application Picker
|
||||||
|
Comment=Select an application to open links and files
|
||||||
|
Exec=dms open %u
|
||||||
|
Icon=danklogo
|
||||||
|
Terminal=false
|
||||||
|
NoDisplay=true
|
||||||
|
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/file;text/html;
|
||||||
|
Categories=Utility;
|
||||||
@@ -9,7 +9,7 @@ Type=simple
|
|||||||
ExecStart=/usr/bin/dms run --session
|
ExecStart=/usr/bin/dms run --session
|
||||||
ExecReload=/usr/bin/pkill -USR1 -x dms
|
ExecReload=/usr/bin/pkill -USR1 -x dms
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=1.23
|
||||||
TimeoutStopSec=10
|
TimeoutStopSec=10
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/keybinds"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/keybinds/providers"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/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("hyprland-path", "$HOME/.config/hypr", "Path to Hyprland config directory")
|
|
||||||
keybindsShowCmd.Flags().String("mangowc-path", "$HOME/.config/mango", "Path to MangoWC config directory")
|
|
||||||
keybindsShowCmd.Flags().String("sway-path", "$HOME/.config/sway", "Path to Sway config directory")
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
if providerName == "hyprland" {
|
|
||||||
hyprlandPath, _ := cmd.Flags().GetString("hyprland-path")
|
|
||||||
hyprlandProvider := providers.NewHyprlandProvider(hyprlandPath)
|
|
||||||
registry.Register(hyprlandProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
if providerName == "mangowc" {
|
|
||||||
mangowcPath, _ := cmd.Flags().GetString("mangowc-path")
|
|
||||||
mangowcProvider := providers.NewMangoWCProvider(mangowcPath)
|
|
||||||
registry.Register(mangowcProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
if providerName == "sway" {
|
|
||||||
swayPath, _ := cmd.Flags().GetString("sway-path")
|
|
||||||
swayProvider := providers.NewSwayProvider(swayPath)
|
|
||||||
registry.Register(swayProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "os/exec"
|
|
||||||
|
|
||||||
func commandExists(cmd string) bool {
|
|
||||||
_, err := exec.LookPath(cmd)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
|
||||||
err := cmd.Run()
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
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, "", " ")
|
|
||||||
}
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
package distros
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/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) {
|
|
||||||
nixpkgsPkgs := []string{}
|
|
||||||
flakePkgs := []string{}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package keybinds
|
|
||||||
|
|
||||||
type Keybind struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Description string `json:"desc"`
|
|
||||||
Subcategory string `json:"subcat,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CheatSheet struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
Binds map[string][]Keybind `json:"binds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Provider interface {
|
|
||||||
Name() string
|
|
||||||
GetCheatSheet() (*CheatSheet, error)
|
|
||||||
}
|
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
|
||||||
|
|
||||||
package mocks_cups
|
|
||||||
|
|
||||||
import (
|
|
||||||
io "io"
|
|
||||||
|
|
||||||
ipp "github.com/AvengeMedia/DankMaterialShell/backend/pkg/ipp"
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockCUPSClientInterface is an autogenerated mock type for the CUPSClientInterface type
|
|
||||||
type MockCUPSClientInterface struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockCUPSClientInterface_Expecter struct {
|
|
||||||
mock *mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_m *MockCUPSClientInterface) EXPECT() *MockCUPSClientInterface_Expecter {
|
|
||||||
return &MockCUPSClientInterface_Expecter{mock: &_m.Mock}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelAllJob provides a mock function with given fields: printer, purge
|
|
||||||
func (_m *MockCUPSClientInterface) CancelAllJob(printer string, purge bool) error {
|
|
||||||
ret := _m.Called(printer, purge)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for CancelAllJob")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
|
|
||||||
r0 = rf(printer, purge)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCUPSClientInterface_CancelAllJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelAllJob'
|
|
||||||
type MockCUPSClientInterface_CancelAllJob_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelAllJob is a helper method to define mock.On call
|
|
||||||
// - printer string
|
|
||||||
// - purge bool
|
|
||||||
func (_e *MockCUPSClientInterface_Expecter) CancelAllJob(printer interface{}, purge interface{}) *MockCUPSClientInterface_CancelAllJob_Call {
|
|
||||||
return &MockCUPSClientInterface_CancelAllJob_Call{Call: _e.mock.On("CancelAllJob", printer, purge)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_CancelAllJob_Call) Run(run func(printer string, purge bool)) *MockCUPSClientInterface_CancelAllJob_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(string), args[1].(bool))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_CancelAllJob_Call) Return(_a0 error) *MockCUPSClientInterface_CancelAllJob_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_CancelAllJob_Call) RunAndReturn(run func(string, bool) error) *MockCUPSClientInterface_CancelAllJob_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelJob provides a mock function with given fields: jobID, purge
|
|
||||||
func (_m *MockCUPSClientInterface) CancelJob(jobID int, purge bool) error {
|
|
||||||
ret := _m.Called(jobID, purge)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for CancelJob")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(int, bool) error); ok {
|
|
||||||
r0 = rf(jobID, purge)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCUPSClientInterface_CancelJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelJob'
|
|
||||||
type MockCUPSClientInterface_CancelJob_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelJob is a helper method to define mock.On call
|
|
||||||
// - jobID int
|
|
||||||
// - purge bool
|
|
||||||
func (_e *MockCUPSClientInterface_Expecter) CancelJob(jobID interface{}, purge interface{}) *MockCUPSClientInterface_CancelJob_Call {
|
|
||||||
return &MockCUPSClientInterface_CancelJob_Call{Call: _e.mock.On("CancelJob", jobID, purge)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_CancelJob_Call) Run(run func(jobID int, purge bool)) *MockCUPSClientInterface_CancelJob_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(int), args[1].(bool))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_CancelJob_Call) Return(_a0 error) *MockCUPSClientInterface_CancelJob_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_CancelJob_Call) RunAndReturn(run func(int, bool) error) *MockCUPSClientInterface_CancelJob_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJobs provides a mock function with given fields: printer, class, whichJobs, myJobs, firstJobId, limit, attributes
|
|
||||||
func (_m *MockCUPSClientInterface) GetJobs(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string) (map[int]ipp.Attributes, error) {
|
|
||||||
ret := _m.Called(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for GetJobs")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 map[int]ipp.Attributes
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string, string, bool, int, int, []string) (map[int]ipp.Attributes, error)); ok {
|
|
||||||
return rf(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
|
|
||||||
}
|
|
||||||
if rf, ok := ret.Get(0).(func(string, string, string, bool, int, int, []string) map[int]ipp.Attributes); ok {
|
|
||||||
r0 = rf(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(map[int]ipp.Attributes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string, string, bool, int, int, []string) error); ok {
|
|
||||||
r1 = rf(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCUPSClientInterface_GetJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobs'
|
|
||||||
type MockCUPSClientInterface_GetJobs_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJobs is a helper method to define mock.On call
|
|
||||||
// - printer string
|
|
||||||
// - class string
|
|
||||||
// - whichJobs string
|
|
||||||
// - myJobs bool
|
|
||||||
// - firstJobId int
|
|
||||||
// - limit int
|
|
||||||
// - attributes []string
|
|
||||||
func (_e *MockCUPSClientInterface_Expecter) GetJobs(printer interface{}, class interface{}, whichJobs interface{}, myJobs interface{}, firstJobId interface{}, limit interface{}, attributes interface{}) *MockCUPSClientInterface_GetJobs_Call {
|
|
||||||
return &MockCUPSClientInterface_GetJobs_Call{Call: _e.mock.On("GetJobs", printer, class, whichJobs, myJobs, firstJobId, limit, attributes)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_GetJobs_Call) Run(run func(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string)) *MockCUPSClientInterface_GetJobs_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(string), args[1].(string), args[2].(string), args[3].(bool), args[4].(int), args[5].(int), args[6].([]string))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_GetJobs_Call) Return(_a0 map[int]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetJobs_Call {
|
|
||||||
_c.Call.Return(_a0, _a1)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_GetJobs_Call) RunAndReturn(run func(string, string, string, bool, int, int, []string) (map[int]ipp.Attributes, error)) *MockCUPSClientInterface_GetJobs_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrinters provides a mock function with given fields: attributes
|
|
||||||
func (_m *MockCUPSClientInterface) GetPrinters(attributes []string) (map[string]ipp.Attributes, error) {
|
|
||||||
ret := _m.Called(attributes)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for GetPrinters")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 map[string]ipp.Attributes
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(0).(func([]string) (map[string]ipp.Attributes, error)); ok {
|
|
||||||
return rf(attributes)
|
|
||||||
}
|
|
||||||
if rf, ok := ret.Get(0).(func([]string) map[string]ipp.Attributes); ok {
|
|
||||||
r0 = rf(attributes)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(map[string]ipp.Attributes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func([]string) error); ok {
|
|
||||||
r1 = rf(attributes)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCUPSClientInterface_GetPrinters_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPrinters'
|
|
||||||
type MockCUPSClientInterface_GetPrinters_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrinters is a helper method to define mock.On call
|
|
||||||
// - attributes []string
|
|
||||||
func (_e *MockCUPSClientInterface_Expecter) GetPrinters(attributes interface{}) *MockCUPSClientInterface_GetPrinters_Call {
|
|
||||||
return &MockCUPSClientInterface_GetPrinters_Call{Call: _e.mock.On("GetPrinters", attributes)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_GetPrinters_Call) Run(run func(attributes []string)) *MockCUPSClientInterface_GetPrinters_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].([]string))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_GetPrinters_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetPrinters_Call {
|
|
||||||
_c.Call.Return(_a0, _a1)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_GetPrinters_Call) RunAndReturn(run func([]string) (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetPrinters_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// PausePrinter provides a mock function with given fields: printer
|
|
||||||
func (_m *MockCUPSClientInterface) PausePrinter(printer string) error {
|
|
||||||
ret := _m.Called(printer)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for PausePrinter")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
|
||||||
r0 = rf(printer)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCUPSClientInterface_PausePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PausePrinter'
|
|
||||||
type MockCUPSClientInterface_PausePrinter_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// PausePrinter is a helper method to define mock.On call
|
|
||||||
// - printer string
|
|
||||||
func (_e *MockCUPSClientInterface_Expecter) PausePrinter(printer interface{}) *MockCUPSClientInterface_PausePrinter_Call {
|
|
||||||
return &MockCUPSClientInterface_PausePrinter_Call{Call: _e.mock.On("PausePrinter", printer)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_PausePrinter_Call) Run(run func(printer string)) *MockCUPSClientInterface_PausePrinter_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(string))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_PausePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_PausePrinter_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_PausePrinter_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_PausePrinter_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResumePrinter provides a mock function with given fields: printer
|
|
||||||
func (_m *MockCUPSClientInterface) ResumePrinter(printer string) error {
|
|
||||||
ret := _m.Called(printer)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for ResumePrinter")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
|
||||||
r0 = rf(printer)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCUPSClientInterface_ResumePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResumePrinter'
|
|
||||||
type MockCUPSClientInterface_ResumePrinter_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResumePrinter is a helper method to define mock.On call
|
|
||||||
// - printer string
|
|
||||||
func (_e *MockCUPSClientInterface_Expecter) ResumePrinter(printer interface{}) *MockCUPSClientInterface_ResumePrinter_Call {
|
|
||||||
return &MockCUPSClientInterface_ResumePrinter_Call{Call: _e.mock.On("ResumePrinter", printer)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_ResumePrinter_Call) Run(run func(printer string)) *MockCUPSClientInterface_ResumePrinter_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(string))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_ResumePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_ResumePrinter_Call {
|
|
||||||
_c.Call.Return(_a0)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_ResumePrinter_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_ResumePrinter_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendRequest provides a mock function with given fields: url, req, additionalResponseData
|
|
||||||
func (_m *MockCUPSClientInterface) SendRequest(url string, req *ipp.Request, additionalResponseData io.Writer) (*ipp.Response, error) {
|
|
||||||
ret := _m.Called(url, req, additionalResponseData)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for SendRequest")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 *ipp.Response
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string, *ipp.Request, io.Writer) (*ipp.Response, error)); ok {
|
|
||||||
return rf(url, req, additionalResponseData)
|
|
||||||
}
|
|
||||||
if rf, ok := ret.Get(0).(func(string, *ipp.Request, io.Writer) *ipp.Response); ok {
|
|
||||||
r0 = rf(url, req, additionalResponseData)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*ipp.Response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func(string, *ipp.Request, io.Writer) error); ok {
|
|
||||||
r1 = rf(url, req, additionalResponseData)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockCUPSClientInterface_SendRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendRequest'
|
|
||||||
type MockCUPSClientInterface_SendRequest_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendRequest is a helper method to define mock.On call
|
|
||||||
// - url string
|
|
||||||
// - req *ipp.Request
|
|
||||||
// - additionalResponseData io.Writer
|
|
||||||
func (_e *MockCUPSClientInterface_Expecter) SendRequest(url interface{}, req interface{}, additionalResponseData interface{}) *MockCUPSClientInterface_SendRequest_Call {
|
|
||||||
return &MockCUPSClientInterface_SendRequest_Call{Call: _e.mock.On("SendRequest", url, req, additionalResponseData)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_SendRequest_Call) Run(run func(url string, req *ipp.Request, additionalResponseData io.Writer)) *MockCUPSClientInterface_SendRequest_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(string), args[1].(*ipp.Request), args[2].(io.Writer))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_SendRequest_Call) Return(_a0 *ipp.Response, _a1 error) *MockCUPSClientInterface_SendRequest_Call {
|
|
||||||
_c.Call.Return(_a0, _a1)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *MockCUPSClientInterface_SendRequest_Call) RunAndReturn(run func(string, *ipp.Request, io.Writer) (*ipp.Response, error)) *MockCUPSClientInterface_SendRequest_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockCUPSClientInterface creates a new instance of MockCUPSClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
|
||||||
// The first argument is typically a *testing.T value.
|
|
||||||
func NewMockCUPSClientInterface(t interface {
|
|
||||||
mock.TestingT
|
|
||||||
Cleanup(func())
|
|
||||||
}) *MockCUPSClientInterface {
|
|
||||||
mock := &MockCUPSClientInterface{}
|
|
||||||
mock.Mock.Test(t)
|
|
||||||
|
|
||||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
|
||||||
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package cups
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/pkg/ipp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *Manager) GetPrinters() ([]Printer, error) {
|
|
||||||
attributes := []string{
|
|
||||||
ipp.AttributePrinterName,
|
|
||||||
ipp.AttributePrinterUriSupported,
|
|
||||||
ipp.AttributePrinterState,
|
|
||||||
ipp.AttributePrinterStateReasons,
|
|
||||||
ipp.AttributePrinterLocation,
|
|
||||||
ipp.AttributePrinterInfo,
|
|
||||||
ipp.AttributePrinterMakeAndModel,
|
|
||||||
ipp.AttributePrinterIsAcceptingJobs,
|
|
||||||
}
|
|
||||||
|
|
||||||
printerAttrs, err := m.client.GetPrinters(attributes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
printers := make([]Printer, 0, len(printerAttrs))
|
|
||||||
for _, attrs := range printerAttrs {
|
|
||||||
printer := Printer{
|
|
||||||
Name: getStringAttr(attrs, ipp.AttributePrinterName),
|
|
||||||
URI: getStringAttr(attrs, ipp.AttributePrinterUriSupported),
|
|
||||||
State: parsePrinterState(attrs),
|
|
||||||
StateReason: getStringAttr(attrs, ipp.AttributePrinterStateReasons),
|
|
||||||
Location: getStringAttr(attrs, ipp.AttributePrinterLocation),
|
|
||||||
Info: getStringAttr(attrs, ipp.AttributePrinterInfo),
|
|
||||||
MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel),
|
|
||||||
Accepting: getBoolAttr(attrs, ipp.AttributePrinterIsAcceptingJobs),
|
|
||||||
}
|
|
||||||
|
|
||||||
if printer.Name != "" {
|
|
||||||
printers = append(printers, printer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return printers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) GetJobs(printerName string, whichJobs string) ([]Job, error) {
|
|
||||||
attributes := []string{
|
|
||||||
ipp.AttributeJobID,
|
|
||||||
ipp.AttributeJobName,
|
|
||||||
ipp.AttributeJobState,
|
|
||||||
ipp.AttributeJobPrinterURI,
|
|
||||||
ipp.AttributeJobOriginatingUserName,
|
|
||||||
ipp.AttributeJobKilobyteOctets,
|
|
||||||
"time-at-creation",
|
|
||||||
}
|
|
||||||
|
|
||||||
jobAttrs, err := m.client.GetJobs(printerName, "", whichJobs, false, 0, 0, attributes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
jobs := make([]Job, 0, len(jobAttrs))
|
|
||||||
for _, attrs := range jobAttrs {
|
|
||||||
job := Job{
|
|
||||||
ID: getIntAttr(attrs, ipp.AttributeJobID),
|
|
||||||
Name: getStringAttr(attrs, ipp.AttributeJobName),
|
|
||||||
State: parseJobState(attrs),
|
|
||||||
User: getStringAttr(attrs, ipp.AttributeJobOriginatingUserName),
|
|
||||||
Size: getIntAttr(attrs, ipp.AttributeJobKilobyteOctets) * 1024,
|
|
||||||
}
|
|
||||||
|
|
||||||
if uri := getStringAttr(attrs, ipp.AttributeJobPrinterURI); uri != "" {
|
|
||||||
parts := strings.Split(uri, "/")
|
|
||||||
if len(parts) > 0 {
|
|
||||||
job.Printer = parts[len(parts)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ts := getIntAttr(attrs, "time-at-creation"); ts > 0 {
|
|
||||||
job.TimeCreated = time.Unix(int64(ts), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if job.ID != 0 {
|
|
||||||
jobs = append(jobs, job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return jobs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) CancelJob(jobID int) error {
|
|
||||||
return m.client.CancelJob(jobID, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) PausePrinter(printerName string) error {
|
|
||||||
return m.client.PausePrinter(printerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) ResumePrinter(printerName string) error {
|
|
||||||
return m.client.ResumePrinter(printerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) PurgeJobs(printerName string) error {
|
|
||||||
return m.client.CancelAllJob(printerName, true)
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
package cups
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
mocks_cups "github.com/AvengeMedia/DankMaterialShell/backend/internal/mocks/cups"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/pkg/ipp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestManager_GetPrinters(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mockRet map[string]ipp.Attributes
|
|
||||||
mockErr error
|
|
||||||
want int
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success",
|
|
||||||
mockRet: map[string]ipp.Attributes{
|
|
||||||
"printer1": {
|
|
||||||
ipp.AttributePrinterName: []ipp.Attribute{{Value: "printer1"}},
|
|
||||||
ipp.AttributePrinterUriSupported: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}},
|
|
||||||
ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}},
|
|
||||||
ipp.AttributePrinterStateReasons: []ipp.Attribute{{Value: "none"}},
|
|
||||||
ipp.AttributePrinterLocation: []ipp.Attribute{{Value: "Office"}},
|
|
||||||
ipp.AttributePrinterInfo: []ipp.Attribute{{Value: "Test Printer"}},
|
|
||||||
ipp.AttributePrinterMakeAndModel: []ipp.Attribute{{Value: "Generic"}},
|
|
||||||
ipp.AttributePrinterIsAcceptingJobs: []ipp.Attribute{{Value: true}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mockErr: nil,
|
|
||||||
want: 1,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error",
|
|
||||||
mockRet: nil,
|
|
||||||
mockErr: errors.New("test error"),
|
|
||||||
want: 0,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().GetPrinters(mock.Anything).Return(tt.mockRet, tt.mockErr)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := m.GetPrinters()
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.want, len(got))
|
|
||||||
if len(got) > 0 {
|
|
||||||
assert.Equal(t, "printer1", got[0].Name)
|
|
||||||
assert.Equal(t, "idle", got[0].State)
|
|
||||||
assert.Equal(t, "Office", got[0].Location)
|
|
||||||
assert.True(t, got[0].Accepting)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_GetJobs(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mockRet map[int]ipp.Attributes
|
|
||||||
mockErr error
|
|
||||||
want int
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success",
|
|
||||||
mockRet: map[int]ipp.Attributes{
|
|
||||||
1: {
|
|
||||||
ipp.AttributeJobID: []ipp.Attribute{{Value: 1}},
|
|
||||||
ipp.AttributeJobName: []ipp.Attribute{{Value: "test-job"}},
|
|
||||||
ipp.AttributeJobState: []ipp.Attribute{{Value: 5}},
|
|
||||||
ipp.AttributeJobPrinterURI: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}},
|
|
||||||
ipp.AttributeJobOriginatingUserName: []ipp.Attribute{{Value: "testuser"}},
|
|
||||||
ipp.AttributeJobKilobyteOctets: []ipp.Attribute{{Value: 10}},
|
|
||||||
"time-at-creation": []ipp.Attribute{{Value: 1609459200}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mockErr: nil,
|
|
||||||
want: 1,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error",
|
|
||||||
mockRet: nil,
|
|
||||||
mockErr: errors.New("test error"),
|
|
||||||
want: 0,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().GetJobs("printer1", "", "not-completed", false, 0, 0, mock.Anything).
|
|
||||||
Return(tt.mockRet, tt.mockErr)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := m.GetJobs("printer1", "not-completed")
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.want, len(got))
|
|
||||||
if len(got) > 0 {
|
|
||||||
assert.Equal(t, 1, got[0].ID)
|
|
||||||
assert.Equal(t, "test-job", got[0].Name)
|
|
||||||
assert.Equal(t, "processing", got[0].State)
|
|
||||||
assert.Equal(t, "testuser", got[0].User)
|
|
||||||
assert.Equal(t, "printer1", got[0].Printer)
|
|
||||||
assert.Equal(t, 10240, got[0].Size)
|
|
||||||
assert.Equal(t, time.Unix(1609459200, 0), got[0].TimeCreated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_CancelJob(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mockErr error
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success",
|
|
||||||
mockErr: nil,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error",
|
|
||||||
mockErr: errors.New("test error"),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().CancelJob(1, false).Return(tt.mockErr)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := m.CancelJob(1)
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_PausePrinter(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mockErr error
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success",
|
|
||||||
mockErr: nil,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error",
|
|
||||||
mockErr: errors.New("test error"),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().PausePrinter("printer1").Return(tt.mockErr)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := m.PausePrinter("printer1")
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ResumePrinter(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mockErr error
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success",
|
|
||||||
mockErr: nil,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error",
|
|
||||||
mockErr: errors.New("test error"),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().ResumePrinter("printer1").Return(tt.mockErr)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := m.ResumePrinter("printer1")
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_PurgeJobs(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mockErr error
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success",
|
|
||||||
mockErr: nil,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error",
|
|
||||||
mockErr: errors.New("test error"),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().CancelAllJob("printer1", true).Return(tt.mockErr)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := m.PurgeJobs("printer1")
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package cups
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/server/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
ID int `json:"id,omitempty"`
|
|
||||||
Method string `json:"method"`
|
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SuccessResult struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CUPSEvent struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Data CUPSState `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
|
|
||||||
switch req.Method {
|
|
||||||
case "cups.subscribe":
|
|
||||||
handleSubscribe(conn, req, manager)
|
|
||||||
case "cups.getPrinters":
|
|
||||||
handleGetPrinters(conn, req, manager)
|
|
||||||
case "cups.getJobs":
|
|
||||||
handleGetJobs(conn, req, manager)
|
|
||||||
case "cups.pausePrinter":
|
|
||||||
handlePausePrinter(conn, req, manager)
|
|
||||||
case "cups.resumePrinter":
|
|
||||||
handleResumePrinter(conn, req, manager)
|
|
||||||
case "cups.cancelJob":
|
|
||||||
handleCancelJob(conn, req, manager)
|
|
||||||
case "cups.purgeJobs":
|
|
||||||
handlePurgeJobs(conn, req, manager)
|
|
||||||
default:
|
|
||||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetPrinters(conn net.Conn, req Request, manager *Manager) {
|
|
||||||
printers, err := manager.GetPrinters()
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, printers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetJobs(conn net.Conn, req Request, manager *Manager) {
|
|
||||||
printerName, ok := req.Params["printerName"].(string)
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jobs, err := manager.GetJobs(printerName, "not-completed")
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, jobs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePausePrinter(conn net.Conn, req Request, manager *Manager) {
|
|
||||||
printerName, ok := req.Params["printerName"].(string)
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.PausePrinter(printerName); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "paused"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleResumePrinter(conn net.Conn, req Request, manager *Manager) {
|
|
||||||
printerName, ok := req.Params["printerName"].(string)
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.ResumePrinter(printerName); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "resumed"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCancelJob(conn net.Conn, req Request, manager *Manager) {
|
|
||||||
jobIDFloat, ok := req.Params["jobID"].(float64)
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'jobid' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jobID := int(jobIDFloat)
|
|
||||||
|
|
||||||
if err := manager.CancelJob(jobID); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job canceled"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePurgeJobs(conn net.Conn, req Request, manager *Manager) {
|
|
||||||
printerName, ok := req.Params["printerName"].(string)
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.PurgeJobs(printerName); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "jobs canceled"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
|
|
||||||
clientID := fmt.Sprintf("client-%p", conn)
|
|
||||||
stateChan := manager.Subscribe(clientID)
|
|
||||||
defer manager.Unsubscribe(clientID)
|
|
||||||
|
|
||||||
initialState := manager.GetState()
|
|
||||||
event := CUPSEvent{
|
|
||||||
Type: "state_changed",
|
|
||||||
Data: initialState,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
|
|
||||||
ID: req.ID,
|
|
||||||
Result: &event,
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for state := range stateChan {
|
|
||||||
event := CUPSEvent{
|
|
||||||
Type: "state_changed",
|
|
||||||
Data: state,
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(conn).Encode(models.Response[CUPSEvent]{
|
|
||||||
Result: &event,
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
package cups
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
mocks_cups "github.com/AvengeMedia/DankMaterialShell/backend/internal/mocks/cups"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/server/models"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/pkg/ipp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockConn struct {
|
|
||||||
*bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockConn) Close() error { return nil }
|
|
||||||
func (m *mockConn) LocalAddr() net.Addr { return nil }
|
|
||||||
func (m *mockConn) RemoteAddr() net.Addr { return nil }
|
|
||||||
func (m *mockConn) SetDeadline(t time.Time) error { return nil }
|
|
||||||
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
|
|
||||||
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
||||||
|
|
||||||
func TestHandleGetPrinters(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{
|
|
||||||
"printer1": {
|
|
||||||
ipp.AttributePrinterName: []ipp.Attribute{{Value: "printer1"}},
|
|
||||||
ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}},
|
|
||||||
ipp.AttributePrinterUriSupported: []ipp.Attribute{{Value: "ipp://localhost/printers/printer1"}},
|
|
||||||
},
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.getPrinters",
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGetPrinters(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[[]Printer]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp.Result)
|
|
||||||
assert.Equal(t, 1, len(*resp.Result))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleGetPrinters_Error(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().GetPrinters(mock.Anything).Return(nil, errors.New("test error"))
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.getPrinters",
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGetPrinters(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[interface{}]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Nil(t, resp.Result)
|
|
||||||
assert.NotNil(t, resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleGetJobs(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().GetJobs("printer1", "", "not-completed", false, 0, 0, mock.Anything).
|
|
||||||
Return(map[int]ipp.Attributes{
|
|
||||||
1: {
|
|
||||||
ipp.AttributeJobID: []ipp.Attribute{{Value: 1}},
|
|
||||||
ipp.AttributeJobName: []ipp.Attribute{{Value: "job1"}},
|
|
||||||
ipp.AttributeJobState: []ipp.Attribute{{Value: 5}},
|
|
||||||
},
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.getJobs",
|
|
||||||
Params: map[string]interface{}{
|
|
||||||
"printerName": "printer1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGetJobs(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[[]Job]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp.Result)
|
|
||||||
assert.Equal(t, 1, len(*resp.Result))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleGetJobs_MissingParam(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.getJobs",
|
|
||||||
Params: map[string]interface{}{},
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGetJobs(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[interface{}]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Nil(t, resp.Result)
|
|
||||||
assert.NotNil(t, resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlePausePrinter(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().PausePrinter("printer1").Return(nil)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.pausePrinter",
|
|
||||||
Params: map[string]interface{}{
|
|
||||||
"printerName": "printer1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePausePrinter(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[SuccessResult]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp.Result)
|
|
||||||
assert.True(t, resp.Result.Success)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleResumePrinter(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().ResumePrinter("printer1").Return(nil)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.resumePrinter",
|
|
||||||
Params: map[string]interface{}{
|
|
||||||
"printerName": "printer1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResumePrinter(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[SuccessResult]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp.Result)
|
|
||||||
assert.True(t, resp.Result.Success)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleCancelJob(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().CancelJob(1, false).Return(nil)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.cancelJob",
|
|
||||||
Params: map[string]interface{}{
|
|
||||||
"jobID": float64(1),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancelJob(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[SuccessResult]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp.Result)
|
|
||||||
assert.True(t, resp.Result.Success)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlePurgeJobs(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
mockClient.EXPECT().CancelAllJob("printer1", true).Return(nil)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.purgeJobs",
|
|
||||||
Params: map[string]interface{}{
|
|
||||||
"printerName": "printer1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePurgeJobs(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[SuccessResult]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, resp.Result)
|
|
||||||
assert.True(t, resp.Result.Success)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleRequest_UnknownMethod(t *testing.T) {
|
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
|
||||||
|
|
||||||
m := &Manager{
|
|
||||||
client: mockClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
conn := &mockConn{Buffer: buf}
|
|
||||||
|
|
||||||
req := Request{
|
|
||||||
ID: 1,
|
|
||||||
Method: "cups.unknownMethod",
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleRequest(conn, req, m)
|
|
||||||
|
|
||||||
var resp models.Response[interface{}]
|
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Nil(t, resp.Result)
|
|
||||||
assert.NotNil(t, resp.Error)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package network
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
|
|
||||||
return nil, fmt.Errorf("wired connections not supported by iwd")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) GetWiredNetworkDetails(uuid string) (*WiredNetworkInfoResponse, error) {
|
|
||||||
return nil, fmt.Errorf("wired connections not supported by iwd")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) ConnectEthernet() error {
|
|
||||||
return fmt.Errorf("wired connections not supported by iwd")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) DisconnectEthernet() error {
|
|
||||||
return fmt.Errorf("wired connections not supported by iwd")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) ActivateWiredConnection(uuid string) error {
|
|
||||||
return fmt.Errorf("wired connections not supported by iwd")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
|
||||||
return nil, fmt.Errorf("VPN not supported by iwd backend")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) ListActiveVPN() ([]VPNActive, error) {
|
|
||||||
return nil, fmt.Errorf("VPN not supported by iwd backend")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
|
||||||
return fmt.Errorf("VPN not supported by iwd backend")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) DisconnectVPN(uuidOrName string) error {
|
|
||||||
return fmt.Errorf("VPN not supported by iwd backend")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) DisconnectAllVPN() error {
|
|
||||||
return fmt.Errorf("VPN not supported by iwd backend")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *IWDBackend) ClearVPNCredentials(uuidOrName string) error {
|
|
||||||
return fmt.Errorf("VPN not supported by iwd backend")
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNetworkManagerBackend_GetWiredConnections_NoDevice(t *testing.T) {
|
|
||||||
backend, err := NewNetworkManagerBackend()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("NetworkManager not available: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
backend.ethernetDevice = nil
|
|
||||||
_, err = backend.GetWiredConnections()
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNetworkManagerBackend_GetWiredNetworkDetails_NoDevice(t *testing.T) {
|
|
||||||
backend, err := NewNetworkManagerBackend()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("NetworkManager not available: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
backend.ethernetDevice = nil
|
|
||||||
_, err = backend.GetWiredNetworkDetails("test-uuid")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNetworkManagerBackend_ConnectEthernet_NoDevice(t *testing.T) {
|
|
||||||
backend, err := NewNetworkManagerBackend()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("NetworkManager not available: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
backend.ethernetDevice = nil
|
|
||||||
err = backend.ConnectEthernet()
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNetworkManagerBackend_DisconnectEthernet_NoDevice(t *testing.T) {
|
|
||||||
backend, err := NewNetworkManagerBackend()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("NetworkManager not available: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
backend.ethernetDevice = nil
|
|
||||||
err = backend.DisconnectEthernet()
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNetworkManagerBackend_ActivateWiredConnection_NoDevice(t *testing.T) {
|
|
||||||
backend, err := NewNetworkManagerBackend()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("NetworkManager not available: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
backend.ethernetDevice = nil
|
|
||||||
err = backend.ActivateWiredConnection("test-uuid")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNetworkManagerBackend_ActivateWiredConnection_NotFound(t *testing.T) {
|
|
||||||
backend, err := NewNetworkManagerBackend()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("NetworkManager not available: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if backend.ethernetDevice == nil {
|
|
||||||
t.Skip("No ethernet device available")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = backend.ActivateWiredConnection("non-existent-uuid-12345")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNetworkManagerBackend_ListEthernetConnections_NoDevice(t *testing.T) {
|
|
||||||
backend, err := NewNetworkManagerBackend()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("NetworkManager not available: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
backend.ethernetDevice = nil
|
|
||||||
_, err = backend.listEthernetConnections()
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "no ethernet device available")
|
|
||||||
}
|
|
||||||
@@ -1,527 +0,0 @@
|
|||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
|
||||||
"github.com/Wifx/gonetworkmanager/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
|
||||||
s := b.settings
|
|
||||||
if s == nil {
|
|
||||||
var err error
|
|
||||||
s, err = gonetworkmanager.NewSettings()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get settings: %w", err)
|
|
||||||
}
|
|
||||||
b.settings = s
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsMgr := s.(gonetworkmanager.Settings)
|
|
||||||
connections, err := settingsMgr.ListConnections()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get connections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var profiles []VPNProfile
|
|
||||||
for _, conn := range connections {
|
|
||||||
settings, err := conn.GetSettings()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connMeta, ok := settings["connection"]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connType, _ := connMeta["type"].(string)
|
|
||||||
if connType != "vpn" && connType != "wireguard" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connID, _ := connMeta["id"].(string)
|
|
||||||
connUUID, _ := connMeta["uuid"].(string)
|
|
||||||
|
|
||||||
profile := VPNProfile{
|
|
||||||
Name: connID,
|
|
||||||
UUID: connUUID,
|
|
||||||
Type: connType,
|
|
||||||
}
|
|
||||||
|
|
||||||
if connType == "vpn" {
|
|
||||||
if vpnSettings, ok := settings["vpn"]; ok {
|
|
||||||
if svcType, ok := vpnSettings["service-type"].(string); ok {
|
|
||||||
profile.ServiceType = svcType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles = append(profiles, profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(profiles, func(i, j int) bool {
|
|
||||||
return strings.ToLower(profiles[i].Name) < strings.ToLower(profiles[j].Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.VPNProfiles = profiles
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
|
|
||||||
return profiles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) ListActiveVPN() ([]VPNActive, error) {
|
|
||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
|
||||||
|
|
||||||
activeConns, err := nm.GetPropertyActiveConnections()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get active connections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var active []VPNActive
|
|
||||||
for _, activeConn := range activeConns {
|
|
||||||
connType, err := activeConn.GetPropertyType()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if connType != "vpn" && connType != "wireguard" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid, _ := activeConn.GetPropertyUUID()
|
|
||||||
id, _ := activeConn.GetPropertyID()
|
|
||||||
state, _ := activeConn.GetPropertyState()
|
|
||||||
|
|
||||||
var stateStr string
|
|
||||||
switch state {
|
|
||||||
case 0:
|
|
||||||
stateStr = "unknown"
|
|
||||||
case 1:
|
|
||||||
stateStr = "activating"
|
|
||||||
case 2:
|
|
||||||
stateStr = "activated"
|
|
||||||
case 3:
|
|
||||||
stateStr = "deactivating"
|
|
||||||
case 4:
|
|
||||||
stateStr = "deactivated"
|
|
||||||
}
|
|
||||||
|
|
||||||
vpnActive := VPNActive{
|
|
||||||
Name: id,
|
|
||||||
UUID: uuid,
|
|
||||||
State: stateStr,
|
|
||||||
Type: connType,
|
|
||||||
Plugin: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
if connType == "vpn" {
|
|
||||||
conn, _ := activeConn.GetPropertyConnection()
|
|
||||||
if conn != nil {
|
|
||||||
connSettings, err := conn.GetSettings()
|
|
||||||
if err == nil {
|
|
||||||
if vpnSettings, ok := connSettings["vpn"]; ok {
|
|
||||||
if svcType, ok := vpnSettings["service-type"].(string); ok {
|
|
||||||
vpnActive.Plugin = svcType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
active = append(active, vpnActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.VPNActive = active
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
|
|
||||||
return active, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
|
||||||
if singleActive {
|
|
||||||
active, err := b.ListActiveVPN()
|
|
||||||
if err == nil && len(active) > 0 {
|
|
||||||
alreadyConnected := false
|
|
||||||
for _, vpn := range active {
|
|
||||||
if vpn.UUID == uuidOrName || vpn.Name == uuidOrName {
|
|
||||||
alreadyConnected = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !alreadyConnected {
|
|
||||||
if err := b.DisconnectAllVPN(); err != nil {
|
|
||||||
log.Warnf("Failed to disconnect existing VPNs: %v", err)
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s := b.settings
|
|
||||||
if s == nil {
|
|
||||||
var err error
|
|
||||||
s, err = gonetworkmanager.NewSettings()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get settings: %w", err)
|
|
||||||
}
|
|
||||||
b.settings = s
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsMgr := s.(gonetworkmanager.Settings)
|
|
||||||
connections, err := settingsMgr.ListConnections()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get connections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetConn gonetworkmanager.Connection
|
|
||||||
for _, conn := range connections {
|
|
||||||
settings, err := conn.GetSettings()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connMeta, ok := settings["connection"]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connType, _ := connMeta["type"].(string)
|
|
||||||
if connType != "vpn" && connType != "wireguard" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connID, _ := connMeta["id"].(string)
|
|
||||||
connUUID, _ := connMeta["uuid"].(string)
|
|
||||||
|
|
||||||
if connUUID == uuidOrName || connID == uuidOrName {
|
|
||||||
targetConn = conn
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetConn == nil {
|
|
||||||
return fmt.Errorf("VPN connection not found: %s", uuidOrName)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetSettings, err := targetConn.GetSettings()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get connection settings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetUUID string
|
|
||||||
if connMeta, ok := targetSettings["connection"]; ok {
|
|
||||||
if uuid, ok := connMeta["uuid"].(string); ok {
|
|
||||||
targetUUID = uuid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.IsConnectingVPN = true
|
|
||||||
b.state.ConnectingVPNUUID = targetUUID
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
|
|
||||||
if b.onStateChange != nil {
|
|
||||||
b.onStateChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
|
||||||
activeConn, err := nm.ActivateConnection(targetConn, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.IsConnectingVPN = false
|
|
||||||
b.state.ConnectingVPNUUID = ""
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
|
|
||||||
if b.onStateChange != nil {
|
|
||||||
b.onStateChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to activate VPN: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if activeConn != nil {
|
|
||||||
state, _ := activeConn.GetPropertyState()
|
|
||||||
if state == 2 {
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.IsConnectingVPN = false
|
|
||||||
b.state.ConnectingVPNUUID = ""
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
b.ListActiveVPN()
|
|
||||||
if b.onStateChange != nil {
|
|
||||||
b.onStateChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) DisconnectVPN(uuidOrName string) error {
|
|
||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
|
||||||
|
|
||||||
activeConns, err := nm.GetPropertyActiveConnections()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get active connections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("[DisconnectVPN] Looking for VPN: %s", uuidOrName)
|
|
||||||
|
|
||||||
for _, activeConn := range activeConns {
|
|
||||||
connType, err := activeConn.GetPropertyType()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if connType != "vpn" && connType != "wireguard" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid, _ := activeConn.GetPropertyUUID()
|
|
||||||
id, _ := activeConn.GetPropertyID()
|
|
||||||
state, _ := activeConn.GetPropertyState()
|
|
||||||
|
|
||||||
log.Debugf("[DisconnectVPN] Found active VPN: uuid=%s id=%s state=%d", uuid, id, state)
|
|
||||||
|
|
||||||
if uuid == uuidOrName || id == uuidOrName {
|
|
||||||
log.Infof("[DisconnectVPN] Deactivating VPN: %s (state=%d)", id, state)
|
|
||||||
if err := nm.DeactivateConnection(activeConn); err != nil {
|
|
||||||
return fmt.Errorf("failed to deactivate VPN: %w", err)
|
|
||||||
}
|
|
||||||
b.ListActiveVPN()
|
|
||||||
if b.onStateChange != nil {
|
|
||||||
b.onStateChange()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Warnf("[DisconnectVPN] VPN not found in active connections: %s", uuidOrName)
|
|
||||||
|
|
||||||
s := b.settings
|
|
||||||
if s == nil {
|
|
||||||
var err error
|
|
||||||
s, err = gonetworkmanager.NewSettings()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("VPN connection not active and cannot access settings: %w", err)
|
|
||||||
}
|
|
||||||
b.settings = s
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsMgr := s.(gonetworkmanager.Settings)
|
|
||||||
connections, err := settingsMgr.ListConnections()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("VPN connection not active: %s", uuidOrName)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, conn := range connections {
|
|
||||||
settings, err := conn.GetSettings()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connMeta, ok := settings["connection"]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connType, _ := connMeta["type"].(string)
|
|
||||||
if connType != "vpn" && connType != "wireguard" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connID, _ := connMeta["id"].(string)
|
|
||||||
connUUID, _ := connMeta["uuid"].(string)
|
|
||||||
|
|
||||||
if connUUID == uuidOrName || connID == uuidOrName {
|
|
||||||
log.Infof("[DisconnectVPN] VPN connection exists but not active: %s", connID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("VPN connection not found: %s", uuidOrName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) DisconnectAllVPN() error {
|
|
||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
|
||||||
|
|
||||||
activeConns, err := nm.GetPropertyActiveConnections()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get active connections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
var disconnected bool
|
|
||||||
for _, activeConn := range activeConns {
|
|
||||||
connType, err := activeConn.GetPropertyType()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if connType != "vpn" && connType != "wireguard" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := nm.DeactivateConnection(activeConn); err != nil {
|
|
||||||
lastErr = err
|
|
||||||
log.Warnf("Failed to deactivate VPN connection: %v", err)
|
|
||||||
} else {
|
|
||||||
disconnected = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if disconnected {
|
|
||||||
b.ListActiveVPN()
|
|
||||||
if b.onStateChange != nil {
|
|
||||||
b.onStateChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) ClearVPNCredentials(uuidOrName string) error {
|
|
||||||
s := b.settings
|
|
||||||
if s == nil {
|
|
||||||
var err error
|
|
||||||
s, err = gonetworkmanager.NewSettings()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get settings: %w", err)
|
|
||||||
}
|
|
||||||
b.settings = s
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsMgr := s.(gonetworkmanager.Settings)
|
|
||||||
connections, err := settingsMgr.ListConnections()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get connections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, conn := range connections {
|
|
||||||
settings, err := conn.GetSettings()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connMeta, ok := settings["connection"]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connType, _ := connMeta["type"].(string)
|
|
||||||
if connType != "vpn" && connType != "wireguard" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
connID, _ := connMeta["id"].(string)
|
|
||||||
connUUID, _ := connMeta["uuid"].(string)
|
|
||||||
|
|
||||||
if connUUID == uuidOrName || connID == uuidOrName {
|
|
||||||
if connType == "vpn" {
|
|
||||||
if vpnSettings, ok := settings["vpn"]; ok {
|
|
||||||
delete(vpnSettings, "secrets")
|
|
||||||
|
|
||||||
if dataMap, ok := vpnSettings["data"].(map[string]string); ok {
|
|
||||||
dataMap["password-flags"] = "1"
|
|
||||||
vpnSettings["data"] = dataMap
|
|
||||||
}
|
|
||||||
|
|
||||||
vpnSettings["password-flags"] = uint32(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
settings["vpn-secrets"] = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := conn.Update(settings); err != nil {
|
|
||||||
return fmt.Errorf("failed to update connection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := conn.ClearSecrets(); err != nil {
|
|
||||||
log.Warnf("ClearSecrets call failed (may not be critical): %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Cleared credentials for VPN: %s", connID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("VPN connection not found: %s", uuidOrName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|
||||||
b.stateMutex.RLock()
|
|
||||||
isConnectingVPN := b.state.IsConnectingVPN
|
|
||||||
connectingVPNUUID := b.state.ConnectingVPNUUID
|
|
||||||
b.stateMutex.RUnlock()
|
|
||||||
|
|
||||||
if !isConnectingVPN || connectingVPNUUID == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
|
||||||
activeConns, err := nm.GetPropertyActiveConnections()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
foundConnection := false
|
|
||||||
for _, activeConn := range activeConns {
|
|
||||||
connType, err := activeConn.GetPropertyType()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if connType != "vpn" && connType != "wireguard" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid, err := activeConn.GetPropertyUUID()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
state, _ := activeConn.GetPropertyState()
|
|
||||||
stateReason, _ := activeConn.GetPropertyStateFlags()
|
|
||||||
|
|
||||||
if uuid == connectingVPNUUID {
|
|
||||||
foundConnection = true
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case 2:
|
|
||||||
log.Infof("[updateVPNConnectionState] VPN connection successful: %s", uuid)
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.IsConnectingVPN = false
|
|
||||||
b.state.ConnectingVPNUUID = ""
|
|
||||||
b.state.LastError = ""
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
return
|
|
||||||
case 4:
|
|
||||||
log.Warnf("[updateVPNConnectionState] VPN connection failed/deactivated: %s (state=%d, flags=%d)", uuid, state, stateReason)
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.IsConnectingVPN = false
|
|
||||||
b.state.ConnectingVPNUUID = ""
|
|
||||||
b.state.LastError = "VPN connection failed"
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundConnection {
|
|
||||||
log.Warnf("[updateVPNConnectionState] VPN connection no longer exists: %s", connectingVPNUUID)
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
b.state.IsConnectingVPN = false
|
|
||||||
b.state.ConnectingVPNUUID = ""
|
|
||||||
b.state.LastError = "VPN connection failed"
|
|
||||||
b.stateMutex.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestManager_GetWiredConfigs(t *testing.T) {
|
|
||||||
manager := &Manager{
|
|
||||||
state: &NetworkState{
|
|
||||||
EthernetConnected: true,
|
|
||||||
WiredConnections: []WiredConnection{
|
|
||||||
{ID: "Test", IsActive: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
configs := manager.GetWiredConfigs()
|
|
||||||
|
|
||||||
assert.Len(t, configs, 1)
|
|
||||||
assert.Equal(t, "Test", configs[0].ID)
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package tui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) viewMissingWMInstructions() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
// Determine which WM is missing
|
|
||||||
wmName := "Niri"
|
|
||||||
installCmd := `environment.systemPackages = with pkgs; [
|
|
||||||
niri
|
|
||||||
];`
|
|
||||||
alternateCmd := `# Or enable the module if available:
|
|
||||||
# programs.niri.enable = true;`
|
|
||||||
|
|
||||||
if m.selectedWM == 1 {
|
|
||||||
wmName = "Hyprland"
|
|
||||||
installCmd = `programs.hyprland.enable = true;`
|
|
||||||
alternateCmd = `# Or add to systemPackages:
|
|
||||||
# environment.systemPackages = with pkgs; [
|
|
||||||
# hyprland
|
|
||||||
# ];`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title
|
|
||||||
title := m.styles.Title.Render("⚠️ " + wmName + " Not Installed")
|
|
||||||
b.WriteString(title)
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
// Explanation
|
|
||||||
explanation := m.styles.Normal.Render(wmName + " needs to be installed system-wide on NixOS.")
|
|
||||||
b.WriteString(explanation)
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
// Instructions
|
|
||||||
instructions := m.styles.Subtle.Render("To install " + wmName + ", add this to your /etc/nixos/configuration.nix:")
|
|
||||||
b.WriteString(instructions)
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
// Command box
|
|
||||||
cmdBox := m.styles.CodeBlock.Render(installCmd)
|
|
||||||
b.WriteString(cmdBox)
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
// Alternate command
|
|
||||||
altBox := m.styles.Subtle.Render(alternateCmd)
|
|
||||||
b.WriteString(altBox)
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
// Rebuild instruction
|
|
||||||
rebuildInstruction := m.styles.Normal.Render("Then rebuild your system:")
|
|
||||||
b.WriteString(rebuildInstruction)
|
|
||||||
b.WriteString("\n")
|
|
||||||
|
|
||||||
rebuildCmd := m.styles.CodeBlock.Render("sudo nixos-rebuild switch")
|
|
||||||
b.WriteString(rebuildCmd)
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
// Navigation help
|
|
||||||
help := m.styles.Subtle.Render("Press Esc to go back and select a different window manager, or Ctrl+C to exit")
|
|
||||||
b.WriteString(help)
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateMissingWMInstructionsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
|
||||||
switch keyMsg.String() {
|
|
||||||
case "esc":
|
|
||||||
// Go back to window manager selection
|
|
||||||
m.state = StateSelectWindowManager
|
|
||||||
return m, m.listenForLogs()
|
|
||||||
case "ctrl+c":
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, m.listenForLogs()
|
|
||||||
}
|
|
||||||
105
core/.golangci.yml
Normal file
105
core/.golangci.yml
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- revive
|
||||||
|
|
||||||
|
settings:
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: use-any
|
||||||
|
severity: error
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: false
|
||||||
|
check-blank: false
|
||||||
|
exclude-functions:
|
||||||
|
# Cleanup/destroy operations
|
||||||
|
- (io.Closer).Close
|
||||||
|
- (*os.File).Close
|
||||||
|
- (net.Conn).Close
|
||||||
|
- (*net.Conn).Close
|
||||||
|
# Signal handling
|
||||||
|
- (*os.Process).Signal
|
||||||
|
- (*os.Process).Kill
|
||||||
|
# DBus cleanup
|
||||||
|
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
||||||
|
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
||||||
|
# Encoding to network connections (if conn is bad, nothing we can do)
|
||||||
|
- (*encoding/json.Encoder).Encode
|
||||||
|
- (net.Conn).Write
|
||||||
|
# Command execution where failure is expected/ignored
|
||||||
|
- (*os/exec.Cmd).Run
|
||||||
|
- (*os/exec.Cmd).Start
|
||||||
|
# Flush operations
|
||||||
|
- (*bufio.Writer).Flush
|
||||||
|
# Scanning user input
|
||||||
|
- fmt.Scanln
|
||||||
|
- fmt.Scanf
|
||||||
|
# Parse operations where default value is acceptable
|
||||||
|
- fmt.Sscanf
|
||||||
|
# Flag operations
|
||||||
|
- (*github.com/spf13/pflag.FlagSet).MarkHidden
|
||||||
|
# Binary encoding to buffer (can't fail for basic types)
|
||||||
|
- binary.Write
|
||||||
|
# File operations in cleanup paths
|
||||||
|
- os.Rename
|
||||||
|
- os.Remove
|
||||||
|
- os.RemoveAll
|
||||||
|
- (*os.File).WriteString
|
||||||
|
# Stdout/stderr writes (can't meaningfully handle failure)
|
||||||
|
- fmt.Fprintln
|
||||||
|
- fmt.Fprintf
|
||||||
|
- fmt.Fprint
|
||||||
|
# Writing to pipes (if pipe is bad, nothing we can do)
|
||||||
|
- (*io.PipeWriter).Write
|
||||||
|
- (*os.File).Write
|
||||||
|
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
# Exclude generated mocks from all linters
|
||||||
|
- path: internal/mocks/
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
- govet
|
||||||
|
- unused
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- gosimple
|
||||||
|
- revive
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
- govet
|
||||||
|
- unused
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- gosimple
|
||||||
|
# Exclude cleanup/teardown method calls from errcheck
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `.+\\.(Destroy|Release|Stop|Close|Roundtrip|Store)` is not checked"
|
||||||
|
# Exclude internal state update methods that are best-effort
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `[mb]\\.\\w*(update|initialize|recreate|acquire|enumerate|list|List|Ensure|refresh|Lock)\\w*` is not checked"
|
||||||
|
# Exclude SetMode on wayland power controls (best-effort)
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `.+\\.SetMode` is not checked"
|
||||||
|
# Exclude AddMatchSignal which is best-effort monitoring setup
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `.+\\.AddMatchSignal` is not checked"
|
||||||
|
# Exclude wayland pkg from errcheck and ineffassign (generated code patterns)
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
- ineffassign
|
||||||
|
path: pkg/go-wayland/
|
||||||
|
# Exclude proto pkg from ineffassign (generated protocol code)
|
||||||
|
- linters:
|
||||||
|
- ineffassign
|
||||||
|
path: internal/proto/
|
||||||
|
# binary.Write to bytes.Buffer can't fail
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `binary\\.Write` is not checked"
|
||||||
58
core/.mockery.yml
Normal file
58
core/.mockery.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
with-expecter: true
|
||||||
|
dir: "internal/mocks/{{.InterfaceDirRelative}}"
|
||||||
|
mockname: "Mock{{.InterfaceName}}"
|
||||||
|
outpkg: "{{.PackageName}}"
|
||||||
|
packages:
|
||||||
|
github.com/Wifx/gonetworkmanager/v2:
|
||||||
|
interfaces:
|
||||||
|
NetworkManager:
|
||||||
|
Device:
|
||||||
|
DeviceWireless:
|
||||||
|
AccessPoint:
|
||||||
|
Connection:
|
||||||
|
Settings:
|
||||||
|
ActiveConnection:
|
||||||
|
IP4Config:
|
||||||
|
net:
|
||||||
|
interfaces:
|
||||||
|
Conn:
|
||||||
|
github.com/AvengeMedia/danklinux/internal/plugins:
|
||||||
|
interfaces:
|
||||||
|
GitClient:
|
||||||
|
github.com/godbus/dbus/v5:
|
||||||
|
interfaces:
|
||||||
|
BusObject:
|
||||||
|
github.com/AvengeMedia/danklinux/internal/server/brightness:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/brightness"
|
||||||
|
outpkg: mocks_brightness
|
||||||
|
interfaces:
|
||||||
|
DBusConn:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/network"
|
||||||
|
outpkg: mocks_network
|
||||||
|
interfaces:
|
||||||
|
Backend:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/cups"
|
||||||
|
outpkg: mocks_cups
|
||||||
|
interfaces:
|
||||||
|
CUPSClientInterface:
|
||||||
|
PkHelper:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/cups_pkhelper"
|
||||||
|
outpkg: mocks_cups_pkhelper
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/evdev"
|
||||||
|
outpkg: mocks_evdev
|
||||||
|
interfaces:
|
||||||
|
EvdevDevice:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/version:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/version"
|
||||||
|
outpkg: mocks_version
|
||||||
|
interfaces:
|
||||||
|
VersionFetcher:
|
||||||
163
core/Makefile
Normal file
163
core/Makefile
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
BINARY_NAME=dms
|
||||||
|
BINARY_NAME_INSTALL=dankinstall
|
||||||
|
SOURCE_DIR=cmd/dms
|
||||||
|
SOURCE_DIR_INSTALL=cmd/dankinstall
|
||||||
|
BUILD_DIR=bin
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
INSTALL_DIR=$(PREFIX)/bin
|
||||||
|
|
||||||
|
GO=go
|
||||||
|
GOFLAGS=-ldflags="-s -w"
|
||||||
|
|
||||||
|
# Version and build info
|
||||||
|
BASE_VERSION=$(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")
|
||||||
|
COMMIT_COUNT=$(shell git rev-list --count HEAD 2>/dev/null || echo "0")
|
||||||
|
COMMIT_HASH=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "unknown")
|
||||||
|
VERSION?=$(BASE_VERSION)+git$(COMMIT_COUNT).$(COMMIT_HASH)
|
||||||
|
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
COMMIT?=$(COMMIT_HASH)
|
||||||
|
|
||||||
|
BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.commit=$(COMMIT)'
|
||||||
|
|
||||||
|
# Architecture to build for dist target (amd64, arm64, or all)
|
||||||
|
ARCH ?= all
|
||||||
|
|
||||||
|
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
all: build
|
||||||
|
|
||||||
|
# Build the main binary (dms)
|
||||||
|
build:
|
||||||
|
@echo "Building $(BINARY_NAME)..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
||||||
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||||
|
|
||||||
|
dankinstall:
|
||||||
|
@echo "Building $(BINARY_NAME_INSTALL)..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
||||||
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
||||||
|
|
||||||
|
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
|
||||||
|
dist:
|
||||||
|
ifeq ($(ARCH),all)
|
||||||
|
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
@echo "Building for linux/amd64..."
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
|
||||||
|
@echo "Building for linux/arm64..."
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
|
||||||
|
@echo "Distribution builds complete:"
|
||||||
|
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
|
||||||
|
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
|
||||||
|
else
|
||||||
|
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
@echo "Building for linux/$(ARCH)..."
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
|
||||||
|
@echo "Distribution build complete:"
|
||||||
|
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
|
||||||
|
endif
|
||||||
|
|
||||||
|
build-all: build dankinstall
|
||||||
|
|
||||||
|
install: build
|
||||||
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Installation complete"
|
||||||
|
|
||||||
|
install-all: build-all
|
||||||
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
|
@echo "Installation complete"
|
||||||
|
|
||||||
|
install-dankinstall: dankinstall
|
||||||
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
|
@echo "Installation complete"
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
@echo "Uninstalling $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||||
|
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Uninstall complete"
|
||||||
|
|
||||||
|
uninstall-all:
|
||||||
|
@echo "Uninstalling $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||||
|
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Uninstalling $(BINARY_NAME_INSTALL) from $(INSTALL_DIR)..."
|
||||||
|
@rm -f $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
|
@echo "Uninstall complete"
|
||||||
|
|
||||||
|
uninstall-dankinstall:
|
||||||
|
@echo "Uninstalling $(BINARY_NAME_INSTALL) from $(INSTALL_DIR)..."
|
||||||
|
@rm -f $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
|
@echo "Uninstall complete"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
@rm -rf $(BUILD_DIR)
|
||||||
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
test:
|
||||||
|
@echo "Running tests..."
|
||||||
|
$(GO) test -v ./...
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@echo "Formatting Go code..."
|
||||||
|
$(GO) fmt ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
@echo "Running go vet..."
|
||||||
|
$(GO) vet ./...
|
||||||
|
|
||||||
|
deps:
|
||||||
|
@echo "Updating dependencies..."
|
||||||
|
$(GO) mod tidy
|
||||||
|
$(GO) mod download
|
||||||
|
|
||||||
|
dev:
|
||||||
|
@echo "Building $(BINARY_NAME) for development..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
$(GO) build -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
||||||
|
@echo "Development build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||||
|
|
||||||
|
check-go:
|
||||||
|
@echo "Checking Go version..."
|
||||||
|
@go version | grep -E "go1\.(2[2-9]|[3-9][0-9])" > /dev/null || (echo "ERROR: Go 1.22 or higher required" && exit 1)
|
||||||
|
@echo "Go version OK"
|
||||||
|
|
||||||
|
version: check-go
|
||||||
|
@echo "Version: $(VERSION)"
|
||||||
|
@echo "Build Time: $(BUILD_TIME)"
|
||||||
|
@echo "Commit: $(COMMIT)"
|
||||||
|
|
||||||
|
print-version:
|
||||||
|
@echo "$(VERSION)"
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " all - Build the main binary (dms) (default)"
|
||||||
|
@echo " build - Build the main binary (dms)"
|
||||||
|
@echo " dankinstall - Build dankinstall binary"
|
||||||
|
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
|
||||||
|
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
|
||||||
|
@echo " build-all - Build both binaries"
|
||||||
|
@echo " install - Install dms to $(INSTALL_DIR)"
|
||||||
|
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
||||||
|
@echo " install-dankinstall - Install only dankinstall to $(INSTALL_DIR)"
|
||||||
|
@echo " uninstall - Remove dms from $(INSTALL_DIR)"
|
||||||
|
@echo " uninstall-all - Remove both binaries from $(INSTALL_DIR)"
|
||||||
|
@echo " uninstall-dankinstall - Remove only dankinstall from $(INSTALL_DIR)"
|
||||||
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo " test - Run tests"
|
||||||
|
@echo " fmt - Format Go code"
|
||||||
|
@echo " vet - Run go vet"
|
||||||
|
@echo " deps - Update dependencies"
|
||||||
|
@echo " dev - Build with debug info"
|
||||||
|
@echo " check-go - Check Go version compatibility"
|
||||||
|
@echo " version - Show version information"
|
||||||
|
@echo " help - Show this help message"
|
||||||
@@ -31,6 +31,7 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
|
|||||||
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
|
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
|
||||||
- Backlight control - Internal display brightness via `login1` or sysfs
|
- Backlight control - Internal display brightness via `login1` or sysfs
|
||||||
- LED control - Keyboard/device LED management
|
- LED control - Keyboard/device LED management
|
||||||
|
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
|
||||||
|
|
||||||
**Plugin System**
|
**Plugin System**
|
||||||
- Plugin registry integration
|
- Plugin registry integration
|
||||||
@@ -71,6 +72,13 @@ sudo make install # Install to /usr/local/bin/dms
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
**Setup pre-commit hooks:**
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath .githooks
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
|
||||||
|
|
||||||
**Regenerating Wayland Protocol Bindings:**
|
**Regenerating Wayland Protocol Bindings:**
|
||||||
```bash
|
```bash
|
||||||
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -4,15 +4,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/logger"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/tui"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fileLogger, err := logger.NewFileLogger()
|
fileLogger, err := log.NewFileLogger()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
||||||
fmt.Println("Continuing without file logging...")
|
fmt.Println("Continuing without file logging...")
|
||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/server/brightness"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +28,14 @@ var brightnessSetCmd = &cobra.Command{
|
|||||||
Short: "Set brightness for a device",
|
Short: "Set brightness for a device",
|
||||||
Long: "Set brightness percentage (0-100) for a specific device",
|
Long: "Set brightness percentage (0-100) for a specific device",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
Run: runBrightnessSet,
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runBrightnessSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
var brightnessGetCmd = &cobra.Command{
|
var brightnessGetCmd = &cobra.Command{
|
||||||
@@ -36,7 +43,14 @@ var brightnessGetCmd = &cobra.Command{
|
|||||||
Short: "Get brightness for a device",
|
Short: "Get brightness for a device",
|
||||||
Long: "Get current brightness percentage for a specific device",
|
Long: "Get current brightness percentage for a specific device",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: runBrightnessGet,
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runBrightnessGet,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -105,9 +119,7 @@ Global Flags:
|
|||||||
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
|
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runBrightnessList(cmd *cobra.Command, args []string) {
|
func getAllBrightnessDevices(includeDDC bool) []brightness.Device {
|
||||||
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
|
||||||
|
|
||||||
allDevices := []brightness.Device{}
|
allDevices := []brightness.Device{}
|
||||||
|
|
||||||
sysfs, err := brightness.NewSysfsBackend()
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
@@ -138,6 +150,13 @@ func runBrightnessList(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return allDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBrightnessList(cmd *cobra.Command, args []string) {
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
allDevices := getAllBrightnessDevices(includeDDC)
|
||||||
|
|
||||||
if len(allDevices) == 0 {
|
if len(allDevices) == 0 {
|
||||||
fmt.Println("No brightness devices found")
|
fmt.Println("No brightness devices found")
|
||||||
return
|
return
|
||||||
@@ -261,31 +280,20 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Failed to set brightness for device: %s", deviceID)
|
log.Fatalf("Failed to set brightness for device: %s", deviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBrightnessDevices(includeDDC bool) []string {
|
||||||
|
allDevices := getAllBrightnessDevices(includeDDC)
|
||||||
|
|
||||||
|
var deviceIDs []string
|
||||||
|
for _, device := range allDevices {
|
||||||
|
deviceIDs = append(deviceIDs, device.ID)
|
||||||
|
}
|
||||||
|
return deviceIDs
|
||||||
|
}
|
||||||
|
|
||||||
func runBrightnessGet(cmd *cobra.Command, args []string) {
|
func runBrightnessGet(cmd *cobra.Command, args []string) {
|
||||||
deviceID := args[0]
|
deviceID := args[0]
|
||||||
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
allDevices := getAllBrightnessDevices(includeDDC)
|
||||||
allDevices := []brightness.Device{}
|
|
||||||
|
|
||||||
sysfs, err := brightness.NewSysfsBackend()
|
|
||||||
if err == nil {
|
|
||||||
devices, err := sysfs.GetDevices()
|
|
||||||
if err == nil {
|
|
||||||
allDevices = append(allDevices, devices...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if includeDDC {
|
|
||||||
ddc, err := brightness.NewDDCBackend()
|
|
||||||
if err == nil {
|
|
||||||
defer ddc.Close()
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
devices, err := ddc.GetDevices()
|
|
||||||
if err == nil {
|
|
||||||
allDevices = append(allDevices, devices...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, device := range allDevices {
|
for _, device := range allDevices {
|
||||||
if device.ID == deviceID {
|
if device.ID == deviceID {
|
||||||
@@ -2,11 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/plugins"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +68,10 @@ var ipcCmd = &cobra.Command{
|
|||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
||||||
PreRunE: findConfig,
|
PreRunE: findConfig,
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
_ = findConfig(cmd, args)
|
||||||
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
runShellIPCCommand(args)
|
runShellIPCCommand(args)
|
||||||
},
|
},
|
||||||
@@ -115,6 +121,12 @@ var pluginsInstallCmd = &cobra.Command{
|
|||||||
Short: "Install a plugin by ID",
|
Short: "Install a plugin by ID",
|
||||||
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getAvailablePluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := installPluginCLI(args[0]); err != nil {
|
if err := installPluginCLI(args[0]); err != nil {
|
||||||
log.Fatalf("Error installing plugin: %v", err)
|
log.Fatalf("Error installing plugin: %v", err)
|
||||||
@@ -127,6 +139,12 @@ var pluginsUninstallCmd = &cobra.Command{
|
|||||||
Short: "Uninstall a plugin by ID",
|
Short: "Uninstall a plugin by ID",
|
||||||
Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := uninstallPluginCLI(args[0]); err != nil {
|
if err := uninstallPluginCLI(args[0]); err != nil {
|
||||||
log.Fatalf("Error uninstalling plugin: %v", err)
|
log.Fatalf("Error uninstalling plugin: %v", err)
|
||||||
@@ -136,10 +154,59 @@ var pluginsUninstallCmd = &cobra.Command{
|
|||||||
|
|
||||||
func runVersion(cmd *cobra.Command, args []string) {
|
func runVersion(cmd *cobra.Command, args []string) {
|
||||||
printASCII()
|
printASCII()
|
||||||
fmt.Printf("%s\n", Version)
|
fmt.Printf("%s\n", formatVersion(Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git builds: dms (git) v0.6.2-XXXX
|
||||||
|
// Stable releases: dms v0.6.2
|
||||||
|
func formatVersion(version string) string {
|
||||||
|
// Arch/Debian/Ubuntu/OpenSUSE git format: 0.6.2+git2264.c5c5ce84
|
||||||
|
re := regexp.MustCompile(`^([\d.]+)\+git(\d+)\.`)
|
||||||
|
if matches := re.FindStringSubmatch(version); matches != nil {
|
||||||
|
return fmt.Sprintf("dms (git) v%s-%s", matches[1], matches[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fedora COPR git format: 0.0.git.2267.d430cae9
|
||||||
|
re = regexp.MustCompile(`^[\d.]+\.git\.(\d+)\.`)
|
||||||
|
if matches := re.FindStringSubmatch(version); matches != nil {
|
||||||
|
baseVersion := getBaseVersion()
|
||||||
|
return fmt.Sprintf("dms (git) v%s-%s", baseVersion, matches[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stable release format: 0.6.2
|
||||||
|
re = regexp.MustCompile(`^([\d.]+)$`)
|
||||||
|
if matches := re.FindStringSubmatch(version); matches != nil {
|
||||||
|
return fmt.Sprintf("dms v%s", matches[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("dms %s", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBaseVersion() string {
|
||||||
|
paths := []string{
|
||||||
|
"/usr/share/quickshell/dms/VERSION",
|
||||||
|
"/usr/local/share/quickshell/dms/VERSION",
|
||||||
|
"/etc/xdg/quickshell/dms/VERSION",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
if content, err := os.ReadFile(path); err == nil {
|
||||||
|
ver := strings.TrimSpace(string(content))
|
||||||
|
ver = strings.TrimPrefix(ver, "v")
|
||||||
|
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
|
||||||
|
if matches := re.FindStringSubmatch(ver); matches != nil {
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
return "0.6.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDebugServer() error {
|
func startDebugServer() error {
|
||||||
|
server.CLIVersion = Version
|
||||||
return server.Start(true)
|
return server.Start(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +365,38 @@ func installPluginCLI(idOrName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAvailablePluginIDs() []string {
|
||||||
|
registry, err := plugins.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginList, err := registry.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
for _, p := range pluginList {
|
||||||
|
ids = append(ids, p.ID)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstalledPluginIDs() []string {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := manager.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return installed
|
||||||
|
}
|
||||||
|
|
||||||
func uninstallPluginCLI(idOrName string) error {
|
func uninstallPluginCLI(idOrName string) error {
|
||||||
manager, err := plugins.NewManager()
|
manager, err := plugins.NewManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -368,6 +467,7 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
pluginsCmd,
|
pluginsCmd,
|
||||||
dank16Cmd,
|
dank16Cmd,
|
||||||
brightnessCmd,
|
brightnessCmd,
|
||||||
|
dpmsCmd,
|
||||||
keybindsCmd,
|
keybindsCmd,
|
||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
@@ -2,11 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/dank16"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,9 +24,12 @@ func init() {
|
|||||||
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
||||||
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
||||||
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
||||||
dank16Cmd.Flags().String("vscode-enrich", "", "Enrich existing VSCode theme file with terminal colors")
|
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
||||||
dank16Cmd.Flags().String("background", "", "Custom background color")
|
dank16Cmd.Flags().String("background", "", "Custom background color")
|
||||||
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
|
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
|
||||||
|
_ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDank16(cmd *cobra.Command, args []string) {
|
func runDank16(cmd *cobra.Command, args []string) {
|
||||||
@@ -42,7 +44,7 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
isFoot, _ := cmd.Flags().GetBool("foot")
|
isFoot, _ := cmd.Flags().GetBool("foot")
|
||||||
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
||||||
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
||||||
vscodeEnrich, _ := cmd.Flags().GetString("vscode-enrich")
|
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
||||||
background, _ := cmd.Flags().GetString("background")
|
background, _ := cmd.Flags().GetString("background")
|
||||||
contrastAlgo, _ := cmd.Flags().GetString("contrast")
|
contrastAlgo, _ := cmd.Flags().GetString("contrast")
|
||||||
|
|
||||||
@@ -63,18 +65,7 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
colors := dank16.GeneratePalette(primaryColor, opts)
|
colors := dank16.GeneratePalette(primaryColor, opts)
|
||||||
|
|
||||||
if vscodeEnrich != "" {
|
if isJson {
|
||||||
data, err := os.ReadFile(vscodeEnrich)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error reading file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
enriched, err := dank16.EnrichVSCodeTheme(data, colors)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error enriching theme: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println(string(enriched))
|
|
||||||
} else if isJson {
|
|
||||||
fmt.Print(dank16.GenerateJSON(colors))
|
fmt.Print(dank16.GenerateJSON(colors))
|
||||||
} else if isKitty {
|
} else if isKitty {
|
||||||
fmt.Print(dank16.GenerateKittyTheme(colors))
|
fmt.Print(dank16.GenerateKittyTheme(colors))
|
||||||
@@ -84,6 +75,8 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Print(dank16.GenerateAlacrittyTheme(colors))
|
fmt.Print(dank16.GenerateAlacrittyTheme(colors))
|
||||||
} else if isGhostty {
|
} else if isGhostty {
|
||||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
|
} else if isWezterm {
|
||||||
|
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
}
|
}
|
||||||
105
core/cmd/dms/commands_dpms.go
Normal file
105
core/cmd/dms/commands_dpms.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dpmsCmd = &cobra.Command{
|
||||||
|
Use: "dpms",
|
||||||
|
Short: "Control display power management",
|
||||||
|
}
|
||||||
|
|
||||||
|
var dpmsOnCmd = &cobra.Command{
|
||||||
|
Use: "on [output]",
|
||||||
|
Short: "Turn display(s) on",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runDPMSOn,
|
||||||
|
}
|
||||||
|
|
||||||
|
var dpmsOffCmd = &cobra.Command{
|
||||||
|
Use: "off [output]",
|
||||||
|
Short: "Turn display(s) off",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runDPMSOff,
|
||||||
|
}
|
||||||
|
|
||||||
|
var dpmsListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List outputs",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: runDPMSList,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dpmsCmd.AddCommand(dpmsOnCmd, dpmsOffCmd, dpmsListCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDPMSOn(cmd *cobra.Command, args []string) {
|
||||||
|
outputName := ""
|
||||||
|
if len(args) > 0 {
|
||||||
|
outputName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := newDPMSClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err := client.SetDPMS(outputName, true); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDPMSOff(cmd *cobra.Command, args []string) {
|
||||||
|
outputName := ""
|
||||||
|
if len(args) > 0 {
|
||||||
|
outputName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := newDPMSClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err := client.SetDPMS(outputName, false); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDPMSOutputs() []string {
|
||||||
|
client, err := newDPMSClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
return client.ListOutputs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDPMSList(cmd *cobra.Command, args []string) {
|
||||||
|
client, err := newDPMSClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
for _, output := range client.ListOutputs() {
|
||||||
|
fmt.Println(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,8 +77,6 @@ func runUpdate() {
|
|||||||
switch config.Family {
|
switch config.Family {
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
updateErr = updateArchLinux()
|
updateErr = updateArchLinux()
|
||||||
case distros.FamilyNix:
|
|
||||||
updateErr = updateNixOS()
|
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
updateErr = updateOtherDistros()
|
updateErr = updateOtherDistros()
|
||||||
default:
|
default:
|
||||||
@@ -152,27 +150,6 @@ func updateArchLinux() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNixOS() error {
|
|
||||||
fmt.Println("This will update DankMaterialShell using nix profile.")
|
|
||||||
if !confirmUpdate() {
|
|
||||||
return errdefs.ErrUpdateCancelled
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\nRunning: nix profile upgrade github:AvengeMedia/DankMaterialShell")
|
|
||||||
updateCmd := exec.Command("nix", "profile", "upgrade", "github:AvengeMedia/DankMaterialShell")
|
|
||||||
updateCmd.Stdout = os.Stdout
|
|
||||||
updateCmd.Stderr = os.Stderr
|
|
||||||
err := updateCmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: Failed to update using nix profile: %v\n", err)
|
|
||||||
fmt.Println("Falling back to git-based update method...")
|
|
||||||
return updateOtherDistros()
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("dms successfully updated")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateOtherDistros() error {
|
func updateOtherDistros() error {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -8,9 +8,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
var greeterCmd = &cobra.Command{
|
var greeterCmd = &cobra.Command{
|
||||||
@@ -217,6 +219,191 @@ func checkGroupExists(groupName string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func disableDisplayManager(dmName string) (bool, error) {
|
||||||
|
state, err := getSystemdServiceState(dmName)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to check %s state: %w", dmName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.Exists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nChecking %s...\n", dmName)
|
||||||
|
fmt.Printf(" Current state: enabled=%s\n", state.EnabledState)
|
||||||
|
|
||||||
|
actionTaken := false
|
||||||
|
|
||||||
|
if state.NeedsDisable {
|
||||||
|
var disableCmd *exec.Cmd
|
||||||
|
var actionVerb string
|
||||||
|
|
||||||
|
if state.EnabledState == "static" {
|
||||||
|
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
||||||
|
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
|
||||||
|
actionVerb = "masked"
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Disabling %s...\n", dmName)
|
||||||
|
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
|
||||||
|
actionVerb = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
disableCmd.Stdout = os.Stdout
|
||||||
|
disableCmd.Stderr = os.Stderr
|
||||||
|
if err := disableCmd.Run(); err != nil {
|
||||||
|
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledState, shouldDisable, verifyErr := checkSystemdServiceEnabled(dmName)
|
||||||
|
if verifyErr != nil {
|
||||||
|
fmt.Printf(" ⚠ Warning: Could not verify %s was %s: %v\n", dmName, actionVerb, verifyErr)
|
||||||
|
} else if shouldDisable {
|
||||||
|
return actionTaken, fmt.Errorf("%s is still in state '%s' after %s operation", dmName, enabledState, actionVerb)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ %s %s (now: %s)\n", cases.Title(language.English).String(actionVerb), dmName, enabledState)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionTaken = true
|
||||||
|
} else {
|
||||||
|
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
||||||
|
fmt.Printf(" ✓ %s is already masked\n", dmName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ %s is already disabled\n", dmName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionTaken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureGreetdEnabled() error {
|
||||||
|
fmt.Println("\nChecking greetd service status...")
|
||||||
|
|
||||||
|
state, err := getSystemdServiceState("greetd")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check greetd state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.Exists {
|
||||||
|
return fmt.Errorf("greetd service not found. Please install greetd first")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" Current state: %s\n", state.EnabledState)
|
||||||
|
|
||||||
|
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
||||||
|
fmt.Println(" Unmasking greetd...")
|
||||||
|
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
|
||||||
|
unmaskCmd.Stdout = os.Stdout
|
||||||
|
unmaskCmd.Stderr = os.Stderr
|
||||||
|
if err := unmaskCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(" ✓ Unmasked greetd")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch state.EnabledState {
|
||||||
|
case "disabled", "masked", "masked-runtime":
|
||||||
|
fmt.Println(" Enabling greetd service...")
|
||||||
|
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
|
||||||
|
enableCmd.Stdout = os.Stdout
|
||||||
|
enableCmd.Stderr = os.Stderr
|
||||||
|
if err := enableCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(" ✓ Enabled greetd service")
|
||||||
|
case "enabled", "enabled-runtime":
|
||||||
|
fmt.Println(" ✓ greetd is already enabled")
|
||||||
|
default:
|
||||||
|
fmt.Printf(" ℹ greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureGraphicalTarget() error {
|
||||||
|
getDefaultCmd := exec.Command("systemctl", "get-default")
|
||||||
|
currentTarget, err := getDefaultCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("⚠ Warning: Could not detect current default systemd target")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
||||||
|
if currentTargetStr != "graphical.target" {
|
||||||
|
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
||||||
|
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
|
||||||
|
setDefaultCmd.Stdout = os.Stdout
|
||||||
|
setDefaultCmd.Stderr = os.Stderr
|
||||||
|
if err := setDefaultCmd.Run(); err != nil {
|
||||||
|
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
|
||||||
|
fmt.Println(" Greeter may not start on boot. Run manually:")
|
||||||
|
fmt.Println(" sudo systemctl set-default graphical.target")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Println("✓ Set graphical.target as default")
|
||||||
|
} else {
|
||||||
|
fmt.Println("✓ Default target already set to graphical.target")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleConflictingDisplayManagers() error {
|
||||||
|
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
|
||||||
|
|
||||||
|
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
|
||||||
|
|
||||||
|
disabledAny := false
|
||||||
|
var errors []string
|
||||||
|
|
||||||
|
for _, dm := range conflictingDMs {
|
||||||
|
actionTaken, err := disableDisplayManager(dm)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("Failed to handle %s: %v", dm, err)
|
||||||
|
errors = append(errors, errMsg)
|
||||||
|
fmt.Printf(" ⚠⚠⚠ ERROR: %s\n", errMsg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if actionTaken {
|
||||||
|
disabledAny = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
fmt.Println("\n╔════════════════════════════════════════════════════════════╗")
|
||||||
|
fmt.Println("║ ⚠⚠⚠ ERRORS OCCURRED ⚠⚠⚠ ║")
|
||||||
|
fmt.Println("╚════════════════════════════════════════════════════════════╝")
|
||||||
|
fmt.Println("\nSome display managers could not be disabled:")
|
||||||
|
for _, err := range errors {
|
||||||
|
fmt.Printf(" ✗ %s\n", err)
|
||||||
|
}
|
||||||
|
fmt.Println("\nThis may prevent greetd from starting properly.")
|
||||||
|
fmt.Println("You may need to manually disable them before greetd will work.")
|
||||||
|
fmt.Println("\nManual commands to try:")
|
||||||
|
for _, dm := range conflictingDMs {
|
||||||
|
fmt.Printf(" sudo systemctl disable %s\n", dm)
|
||||||
|
fmt.Printf(" sudo systemctl mask %s\n", dm)
|
||||||
|
}
|
||||||
|
fmt.Print("\nContinue with greeter enablement anyway? (Y/n): ")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
|
||||||
|
if response == "n" || response == "no" {
|
||||||
|
return fmt.Errorf("aborted due to display manager conflicts")
|
||||||
|
}
|
||||||
|
fmt.Println("\nContinuing despite errors...")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !disabledAny && len(errors) == 0 {
|
||||||
|
fmt.Println("\n✓ No conflicting display managers found")
|
||||||
|
} else if disabledAny && len(errors) == 0 {
|
||||||
|
fmt.Println("\n✓ Successfully handled all conflicting display managers")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func enableGreeter() error {
|
func enableGreeter() error {
|
||||||
fmt.Println("=== DMS Greeter Enable ===")
|
fmt.Println("=== DMS Greeter Enable ===")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -232,8 +419,29 @@ func enableGreeter() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configContent := string(data)
|
configContent := string(data)
|
||||||
if strings.Contains(configContent, "dms-greeter") {
|
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
|
||||||
|
|
||||||
|
if configAlreadyCorrect {
|
||||||
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
||||||
|
|
||||||
|
if err := ensureGraphicalTarget(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handleConflictingDisplayManagers(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureGreetdEnabled(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Enable Complete ===")
|
||||||
|
fmt.Println("\nGreeter configuration verified and system state corrected.")
|
||||||
|
fmt.Println("To start the greeter now, run:")
|
||||||
|
fmt.Println(" sudo systemctl start greetd")
|
||||||
|
fmt.Println("\nOr reboot to see the greeter at boot time.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,9 +485,9 @@ func enableGreeter() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapperCmd := "dms-greeter"
|
wrapperCmd, err := findCommandPath("dms-greeter")
|
||||||
if !commandExists("dms-greeter") {
|
if err != nil {
|
||||||
wrapperCmd = "/usr/local/bin/dms-greeter"
|
return fmt.Errorf("dms-greeter not found in PATH. Please ensure it is installed and accessible")
|
||||||
}
|
}
|
||||||
|
|
||||||
compositorLower := strings.ToLower(selectedCompositor)
|
compositorLower := strings.ToLower(selectedCompositor)
|
||||||
@@ -322,11 +530,23 @@ func enableGreeter() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
|
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
|
||||||
|
|
||||||
|
if err := ensureGraphicalTarget(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handleConflictingDisplayManagers(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureGreetdEnabled(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\n=== Enable Complete ===")
|
fmt.Println("\n=== Enable Complete ===")
|
||||||
fmt.Println("\nTo start the greeter, run:")
|
fmt.Println("\nTo start the greeter now, run:")
|
||||||
fmt.Println(" sudo systemctl start greetd")
|
fmt.Println(" sudo systemctl start greetd")
|
||||||
fmt.Println("\nTo enable on boot, run:")
|
fmt.Println("\nOr reboot to see the greeter at boot time.")
|
||||||
fmt.Println(" sudo systemctl enable --now greetd")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -444,7 +664,7 @@ func checkGreeterStatus() error {
|
|||||||
desc: "Session state",
|
desc: "Session state",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: filepath.Join(homeDir, ".cache", "quickshell", "dankshell", "dms-colors.json"),
|
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
target: filepath.Join(cacheDir, "colors.json"),
|
target: filepath.Join(cacheDir, "colors.json"),
|
||||||
desc: "Color theme",
|
desc: "Color theme",
|
||||||
},
|
},
|
||||||
226
core/cmd/dms/commands_keybinds.go
Normal file
226
core/cmd/dms/commands_keybinds.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var keybindsCmd = &cobra.Command{
|
||||||
|
Use: "keybinds",
|
||||||
|
Aliases: []string{"cheatsheet", "chsht"},
|
||||||
|
Short: "Manage keybinds and cheatsheets",
|
||||||
|
Long: "Display and manage keybinds and cheatsheets for various applications",
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List available providers",
|
||||||
|
Long: "List all available keybind/cheatsheet providers",
|
||||||
|
Run: runKeybindsList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsShowCmd = &cobra.Command{
|
||||||
|
Use: "show <provider>",
|
||||||
|
Short: "Show keybinds for a provider",
|
||||||
|
Long: "Display keybinds/cheatsheet for the specified provider",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
return registry.List(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runKeybindsShow,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsSetCmd = &cobra.Command{
|
||||||
|
Use: "set <provider> <key> <action>",
|
||||||
|
Short: "Set a keybind override",
|
||||||
|
Long: "Create or update a keybind override for the specified provider",
|
||||||
|
Args: cobra.ExactArgs(3),
|
||||||
|
Run: runKeybindsSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <provider> <key>",
|
||||||
|
Short: "Remove a keybind override",
|
||||||
|
Long: "Remove a keybind override from the specified provider",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: runKeybindsRemove,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||||
|
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
||||||
|
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
|
||||||
|
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
||||||
|
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
||||||
|
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
||||||
|
|
||||||
|
keybindsCmd.AddCommand(keybindsListCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsSetCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsRemoveCmd)
|
||||||
|
|
||||||
|
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
||||||
|
return providers.NewJSONFileProvider(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
initializeProviders()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeProviders() {
|
||||||
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
|
||||||
|
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
|
||||||
|
if err := registry.Register(hyprlandProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Hyprland provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
|
||||||
|
if err := registry.Register(mangowcProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register MangoWC provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
|
||||||
|
if err := registry.Register(swayProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Sway provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
niriProvider := providers.NewNiriProvider("")
|
||||||
|
if err := registry.Register(niriProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Niri provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := keybinds.DefaultDiscoveryConfig()
|
||||||
|
if err := keybinds.AutoDiscoverProviders(registry, config); err != nil {
|
||||||
|
log.Warnf("Failed to auto-discover providers: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsList(_ *cobra.Command, _ []string) {
|
||||||
|
providerList := keybinds.GetDefaultRegistry().List()
|
||||||
|
if len(providerList) == 0 {
|
||||||
|
fmt.Fprintln(os.Stdout, "No providers available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, "Available providers:")
|
||||||
|
for _, name := range providerList {
|
||||||
|
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeProviderWithPath(name, path string) keybinds.Provider {
|
||||||
|
switch name {
|
||||||
|
case "hyprland":
|
||||||
|
return providers.NewHyprlandProvider(path)
|
||||||
|
case "mangowc":
|
||||||
|
return providers.NewMangoWCProvider(path)
|
||||||
|
case "sway":
|
||||||
|
return providers.NewSwayProvider(path)
|
||||||
|
case "niri":
|
||||||
|
return providers.NewNiriProvider(path)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCheatSheet(provider keybinds.Provider) {
|
||||||
|
sheet, err := provider.GetCheatSheet()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting cheatsheet: %v", err)
|
||||||
|
}
|
||||||
|
output, err := json.MarshalIndent(sheet, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error generating JSON: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsShow(cmd *cobra.Command, args []string) {
|
||||||
|
providerName := args[0]
|
||||||
|
customPath, _ := cmd.Flags().GetString("path")
|
||||||
|
|
||||||
|
if customPath != "" {
|
||||||
|
provider := makeProviderWithPath(providerName, customPath)
|
||||||
|
if provider == nil {
|
||||||
|
log.Fatalf("Provider %s does not support custom path", providerName)
|
||||||
|
}
|
||||||
|
printCheatSheet(provider)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := keybinds.GetDefaultRegistry().Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
printCheatSheet(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWritableProvider(name string) keybinds.WritableProvider {
|
||||||
|
provider, err := keybinds.GetDefaultRegistry().Get(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
writable, ok := provider.(keybinds.WritableProvider)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("Provider %s does not support writing keybinds", name)
|
||||||
|
}
|
||||||
|
return writable
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsSet(cmd *cobra.Command, args []string) {
|
||||||
|
providerName, key, action := args[0], args[1], args[2]
|
||||||
|
writable := getWritableProvider(providerName)
|
||||||
|
|
||||||
|
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
||||||
|
_ = writable.RemoveBind(replaceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make(map[string]any)
|
||||||
|
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
||||||
|
options["allow-when-locked"] = true
|
||||||
|
}
|
||||||
|
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
||||||
|
options["cooldown-ms"] = v
|
||||||
|
}
|
||||||
|
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||||
|
options["repeat"] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
desc, _ := cmd.Flags().GetString("desc")
|
||||||
|
if err := writable.SetBind(key, action, desc, options); err != nil {
|
||||||
|
log.Fatalf("Error setting keybind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.MarshalIndent(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"key": key,
|
||||||
|
"action": action,
|
||||||
|
"path": writable.GetOverridePath(),
|
||||||
|
}, "", " ")
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsRemove(_ *cobra.Command, args []string) {
|
||||||
|
providerName, key := args[0], args[1]
|
||||||
|
writable := getWritableProvider(providerName)
|
||||||
|
|
||||||
|
if err := writable.RemoveBind(key); err != nil {
|
||||||
|
log.Fatalf("Error removing keybind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.MarshalIndent(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"key": key,
|
||||||
|
"removed": true,
|
||||||
|
}, "", " ")
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
227
core/cmd/dms/commands_open.go
Normal file
227
core/cmd/dms/commands_open.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
openMimeType string
|
||||||
|
openCategories []string
|
||||||
|
openRequestType string
|
||||||
|
)
|
||||||
|
|
||||||
|
var openCmd = &cobra.Command{
|
||||||
|
Use: "open [target]",
|
||||||
|
Short: "Open a file, URL, or resource with an application picker",
|
||||||
|
Long: `Open a target (URL, file, or other resource) using the DMS application picker.
|
||||||
|
By default, this opens URLs with the browser picker. You can customize the behavior
|
||||||
|
with flags to handle different MIME types or application categories.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms open https://example.com # Open URL with browser picker
|
||||||
|
dms open file.pdf --mime application/pdf # Open PDF with compatible apps
|
||||||
|
dms open document.odt --category Office # Open with office applications
|
||||||
|
dms open --mime image/png image.png # Open image with image viewers`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
runOpen(args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(openCmd)
|
||||||
|
openCmd.Flags().StringVar(&openMimeType, "mime", "", "MIME type for filtering applications")
|
||||||
|
openCmd.Flags().StringSliceVar(&openCategories, "category", []string{}, "Application categories to filter (e.g., WebBrowser, Office, Graphics)")
|
||||||
|
openCmd.Flags().StringVar(&openRequestType, "type", "url", "Request type (url, file, or custom)")
|
||||||
|
_ = openCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return []string{"url", "file", "custom"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// mimeTypeToCategories maps MIME types to desktop file categories
|
||||||
|
func mimeTypeToCategories(mimeType string) []string {
|
||||||
|
// Split MIME type to get the main type
|
||||||
|
parts := strings.Split(mimeType, "/")
|
||||||
|
if len(parts) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mainType := parts[0]
|
||||||
|
|
||||||
|
switch mainType {
|
||||||
|
case "image":
|
||||||
|
return []string{"Graphics", "Viewer"}
|
||||||
|
case "video":
|
||||||
|
return []string{"Video", "AudioVideo"}
|
||||||
|
case "audio":
|
||||||
|
return []string{"Audio", "AudioVideo"}
|
||||||
|
case "text":
|
||||||
|
if strings.Contains(mimeType, "html") {
|
||||||
|
return []string{"WebBrowser"}
|
||||||
|
}
|
||||||
|
return []string{"TextEditor", "Office"}
|
||||||
|
case "application":
|
||||||
|
if strings.Contains(mimeType, "pdf") {
|
||||||
|
return []string{"Office", "Viewer"}
|
||||||
|
}
|
||||||
|
if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "spreadsheet") ||
|
||||||
|
strings.Contains(mimeType, "presentation") || strings.Contains(mimeType, "msword") ||
|
||||||
|
strings.Contains(mimeType, "ms-excel") || strings.Contains(mimeType, "ms-powerpoint") ||
|
||||||
|
strings.Contains(mimeType, "opendocument") {
|
||||||
|
return []string{"Office"}
|
||||||
|
}
|
||||||
|
if strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") ||
|
||||||
|
strings.Contains(mimeType, "gzip") || strings.Contains(mimeType, "compress") {
|
||||||
|
return []string{"Archiving", "Utility"}
|
||||||
|
}
|
||||||
|
return []string{"Office", "Viewer"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runOpen(target string) {
|
||||||
|
socketPath, err := server.FindSocket()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("DMS socket not found: %v", err)
|
||||||
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("DMS socket connection failed: %v", err)
|
||||||
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
for {
|
||||||
|
_, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if buf[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse file:// URIs to extract the actual file path
|
||||||
|
actualTarget := target
|
||||||
|
detectedMimeType := openMimeType
|
||||||
|
detectedCategories := openCategories
|
||||||
|
detectedRequestType := openRequestType
|
||||||
|
|
||||||
|
log.Infof("Processing target: %s", target)
|
||||||
|
|
||||||
|
if parsedURL, err := url.Parse(target); err == nil && parsedURL.Scheme == "file" {
|
||||||
|
// Extract file path from file:// URI and convert to absolute path
|
||||||
|
actualTarget = parsedURL.Path
|
||||||
|
if absPath, err := filepath.Abs(actualTarget); err == nil {
|
||||||
|
actualTarget = absPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectedRequestType == "url" || detectedRequestType == "" {
|
||||||
|
detectedRequestType = "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Detected file:// URI, extracted absolute path: %s", actualTarget)
|
||||||
|
|
||||||
|
// Auto-detect MIME type if not provided
|
||||||
|
if detectedMimeType == "" {
|
||||||
|
ext := filepath.Ext(actualTarget)
|
||||||
|
if ext != "" {
|
||||||
|
detectedMimeType = mime.TypeByExtension(ext)
|
||||||
|
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect categories based on MIME type if not provided
|
||||||
|
if len(detectedCategories) == 0 && detectedMimeType != "" {
|
||||||
|
detectedCategories = mimeTypeToCategories(detectedMimeType)
|
||||||
|
log.Infof("Detected categories from MIME type: %v", detectedCategories)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
|
||||||
|
// Handle HTTP(S) URLs
|
||||||
|
if detectedRequestType == "" {
|
||||||
|
detectedRequestType = "url"
|
||||||
|
}
|
||||||
|
log.Infof("Detected HTTP(S) URL")
|
||||||
|
} else if _, err := os.Stat(target); err == nil {
|
||||||
|
// Handle local file paths directly (not file:// URIs)
|
||||||
|
// Convert to absolute path
|
||||||
|
if absPath, err := filepath.Abs(target); err == nil {
|
||||||
|
actualTarget = absPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectedRequestType == "url" || detectedRequestType == "" {
|
||||||
|
detectedRequestType = "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Detected local file path, converted to absolute: %s", actualTarget)
|
||||||
|
|
||||||
|
// Auto-detect MIME type if not provided
|
||||||
|
if detectedMimeType == "" {
|
||||||
|
ext := filepath.Ext(actualTarget)
|
||||||
|
if ext != "" {
|
||||||
|
detectedMimeType = mime.TypeByExtension(ext)
|
||||||
|
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect categories based on MIME type if not provided
|
||||||
|
if len(detectedCategories) == 0 && detectedMimeType != "" {
|
||||||
|
detectedCategories = mimeTypeToCategories(detectedMimeType)
|
||||||
|
log.Infof("Detected categories from MIME type: %v", detectedCategories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[string]any{
|
||||||
|
"target": actualTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectedMimeType != "" {
|
||||||
|
params["mimeType"] = detectedMimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(detectedCategories) > 0 {
|
||||||
|
params["categories"] = detectedCategories
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectedRequestType != "" {
|
||||||
|
params["requestType"] = detectedRequestType
|
||||||
|
}
|
||||||
|
|
||||||
|
method := "apppicker.open"
|
||||||
|
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
|
||||||
|
method = "browser.open"
|
||||||
|
params["url"] = target
|
||||||
|
}
|
||||||
|
|
||||||
|
req := models.Request{
|
||||||
|
ID: 1,
|
||||||
|
Method: method,
|
||||||
|
Params: params,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
||||||
|
|
||||||
|
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
||||||
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Request sent successfully")
|
||||||
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/dms"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -76,14 +74,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
||||||
detector, err := dms.NewDetector()
|
detector, _ := dms.NewDetector()
|
||||||
if err != nil && !errors.Is(err, &distros.UnsupportedDistributionError{}) {
|
|
||||||
log.Fatalf("Error initializing DMS detector: %v", err)
|
|
||||||
} else if errors.Is(err, &distros.UnsupportedDistributionError{}) {
|
|
||||||
log.Error("Interactive mode is not supported on this distribution.")
|
|
||||||
log.Info("Please run 'dms --help' for available commands.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !detector.IsDMSInstalled() {
|
if !detector.IsDMSInstalled() {
|
||||||
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
345
core/cmd/dms/dpms_client.go
Normal file
345
core/cmd/dms/dpms_client.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
@@ -5,7 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
@@ -12,10 +12,12 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ipcTargets map[string][]string
|
||||||
|
|
||||||
var isSessionManaged bool
|
var isSessionManaged bool
|
||||||
|
|
||||||
func execDetachedRestart(targetPID int) {
|
func execDetachedRestart(targetPID int) {
|
||||||
@@ -57,13 +59,18 @@ func getRuntimeDir() string {
|
|||||||
return os.TempDir()
|
return os.TempDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasSystemdRun() bool {
|
||||||
|
_, err := exec.LookPath("systemd-run")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func getPIDFilePath() string {
|
func getPIDFilePath() string {
|
||||||
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
|
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func writePIDFile(childPID int) error {
|
func writePIDFile(childPID int) error {
|
||||||
pidFile := getPIDFilePath()
|
pidFile := getPIDFilePath()
|
||||||
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0644)
|
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removePIDFile() {
|
func removePIDFile() {
|
||||||
@@ -139,7 +146,7 @@ func runShellInteractive(session bool) {
|
|||||||
socketPath := server.GetSocketPath()
|
socketPath := server.GetSocketPath()
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
|
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
||||||
log.Warnf("Failed to write config state file: %v", err)
|
log.Warnf("Failed to write config state file: %v", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(configStateFile)
|
defer os.Remove(configStateFile)
|
||||||
@@ -165,6 +172,10 @@ func runShellInteractive(session bool) {
|
|||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isSessionManaged && hasSystemdRun() {
|
||||||
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
||||||
if !strings.HasPrefix(configPath, homeDir) {
|
if !strings.HasPrefix(configPath, homeDir) {
|
||||||
@@ -361,7 +372,7 @@ func runShellDaemon(session bool) {
|
|||||||
socketPath := server.GetSocketPath()
|
socketPath := server.GetSocketPath()
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
|
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
||||||
log.Warnf("Failed to write config state file: %v", err)
|
log.Warnf("Failed to write config state file: %v", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(configStateFile)
|
defer os.Remove(configStateFile)
|
||||||
@@ -374,6 +385,7 @@ func runShellDaemon(session bool) {
|
|||||||
errChan <- fmt.Errorf("server panic: %v", r)
|
errChan <- fmt.Errorf("server panic: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
server.CLIVersion = Version
|
||||||
if err := server.Start(false); err != nil {
|
if err := server.Start(false); err != nil {
|
||||||
errChan <- fmt.Errorf("server error: %w", err)
|
errChan <- fmt.Errorf("server error: %w", err)
|
||||||
}
|
}
|
||||||
@@ -387,6 +399,10 @@ func runShellDaemon(session bool) {
|
|||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isSessionManaged && hasSystemdRun() {
|
||||||
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
||||||
if !strings.HasPrefix(configPath, homeDir) {
|
if !strings.HasPrefix(configPath, homeDir) {
|
||||||
@@ -459,6 +475,51 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||||
|
targets := map[string][]string{}
|
||||||
|
var currentTarget string
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
if strings.HasPrefix(line, "target ") {
|
||||||
|
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
||||||
|
currentFunc := strings.TrimPrefix(line, " function ")
|
||||||
|
currentFunc = strings.SplitN(currentFunc, "(", 2)[0]
|
||||||
|
targets[currentTarget] = append(targets[currentTarget], currentFunc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|
||||||
|
func getShellIPCCompletions(args []string, toComplete string) []string {
|
||||||
|
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
||||||
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
|
var targets ipcTargets
|
||||||
|
|
||||||
|
if output, err := cmd.Output(); err == nil {
|
||||||
|
log.Debugf("IPC show output: %s", string(output))
|
||||||
|
targets = parseTargetsFromIPCShowOutput(string(output))
|
||||||
|
} else {
|
||||||
|
log.Debugf("Error getting IPC show output for completions: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 && args[0] == "call" {
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
targetNames := make([]string, 0)
|
||||||
|
targetNames = append(targetNames, "call")
|
||||||
|
for k := range targets {
|
||||||
|
targetNames = append(targetNames, k)
|
||||||
|
}
|
||||||
|
return targetNames
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets[args[0]]
|
||||||
|
}
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
log.Error("IPC command requires arguments")
|
log.Error("IPC command requires arguments")
|
||||||
@@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/tui"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
92
core/cmd/dms/utils.go
Normal file
92
core/cmd/dms/utils.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandExists(cmd string) bool {
|
||||||
|
_, err := exec.LookPath(cmd)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findCommandPath returns the absolute path to a command in PATH
|
||||||
|
func findCommandPath(cmd string) (string, error) {
|
||||||
|
path, err := exec.LookPath(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type systemdServiceState struct {
|
||||||
|
Name string
|
||||||
|
EnabledState string
|
||||||
|
NeedsDisable bool
|
||||||
|
Exists bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
|
||||||
|
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
|
||||||
|
cmd := exec.Command("systemctl", "is-enabled", serviceName)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
|
||||||
|
stateStr := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
||||||
|
isKnownState := false
|
||||||
|
for _, known := range knownStates {
|
||||||
|
if stateStr == known {
|
||||||
|
isKnownState = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isKnownState {
|
||||||
|
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldDisable := false
|
||||||
|
switch stateStr {
|
||||||
|
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||||
|
shouldDisable = true
|
||||||
|
case "disabled", "masked", "masked-runtime", "not-found":
|
||||||
|
shouldDisable = false
|
||||||
|
default:
|
||||||
|
shouldDisable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateStr, shouldDisable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemdServiceState(serviceName string) (*systemdServiceState, error) {
|
||||||
|
state := &systemdServiceState{
|
||||||
|
Name: serviceName,
|
||||||
|
Exists: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check enabled state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.EnabledState = enabledState
|
||||||
|
state.NeedsDisable = needsDisable
|
||||||
|
|
||||||
|
if enabledState == "not-found" {
|
||||||
|
state.Exists = false
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Exists = true
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
@@ -1,65 +1,70 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/backend
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.24.6
|
go 1.24.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.6
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/godbus/dbus/v5 v5.1.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/godbus/dbus/v5 v5.2.0
|
||||||
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||||
|
github.com/pilebones/go-udev v0.9.1
|
||||||
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a
|
||||||
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.6.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
golang.org/x/crypto v0.42.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
github.com/charmbracelet/x/ansi v0.11.2 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sys v0.38.0
|
||||||
golang.org/x/sys v0.36.0
|
golang.org/x/text v0.31.0
|
||||||
golang.org/x/text v0.29.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
@@ -14,27 +14,33 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
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 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
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 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
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 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
||||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||||
|
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -44,23 +50,30 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
|
|||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
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 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 h1:4KqVJTL5eanN8Sgg3BV6f2/QzfZEFbCd+rTak1fGRRA=
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30/go.mod h1:snwvGrbywVFy2d6KJdQ132zapq4aLyzLMgpo79XdEfM=
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd h1:30HEd5KKVM7GgMJ1GSNuYxuZXEg8Pdlngp6T51faxoc=
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd/go.mod h1:lz8PQr/p79XpFq5ODVBwRJu5LnOF8Et7j95ehqmCMJU=
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
||||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
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 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||||
@@ -71,66 +84,67 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
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 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
||||||
|
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
||||||
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.6/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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34 h1:iTAt1me6SBYsuzrl/CmrxtATPlOG/pVviosM3DhUdKE=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
github.com/yaslama/go-wayland/wayland v0.0.0-20250907155644-2874f32d9c34/go.mod h1:jzmUN5lUAv2O8e63OvcauV4S30rIZ1BvF/PNYE37vDo=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
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 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigDeployer struct {
|
type ConfigDeployer struct {
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -22,8 +22,19 @@ func LocateDMSConfig() (string, error) {
|
|||||||
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryPaths = append(primaryPaths, "/usr/share/quickshell/dms")
|
// System data directories
|
||||||
|
dataDirs := os.Getenv("XDG_DATA_DIRS")
|
||||||
|
if dataDirs == "" {
|
||||||
|
dataDirs = "/usr/local/share:/usr/share"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range strings.Split(dataDirs, ":") {
|
||||||
|
if dir != "" {
|
||||||
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System config directories (fallback)
|
||||||
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||||
if configDirs == "" {
|
if configDirs == "" {
|
||||||
configDirs = "/etc/xdg"
|
configDirs = "/etc/xdg"
|
||||||
@@ -125,6 +125,8 @@ windowrulev2 = noborder, class:^(kitty)$
|
|||||||
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
|
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
|
||||||
windowrulev2 = float, class:^(zoom)$
|
windowrulev2 = float, class:^(zoom)$
|
||||||
|
|
||||||
|
# DMS windows floating by default
|
||||||
|
windowrulev2 = float, class:^(org.quickshell)$
|
||||||
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
|
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
|
||||||
|
|
||||||
layerrule = noanim, ^(quickshell)$
|
layerrule = noanim, ^(quickshell)$
|
||||||
@@ -138,8 +140,8 @@ $mod = SUPER
|
|||||||
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
||||||
bind = $mod, space, exec, dms ipc call spotlight toggle
|
bind = $mod, space, exec, dms ipc call spotlight toggle
|
||||||
bind = $mod, V, exec, dms ipc call clipboard toggle
|
bind = $mod, V, exec, dms ipc call clipboard toggle
|
||||||
bind = $mod, M, exec, dms ipc call processlist toggle
|
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
|
||||||
bind = $mod, comma, exec, dms ipc call settings toggle
|
bind = $mod, comma, exec, dms ipc call settings focusOrToggle
|
||||||
bind = $mod, N, exec, dms ipc call notifications toggle
|
bind = $mod, N, exec, dms ipc call notifications toggle
|
||||||
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
||||||
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
||||||
@@ -151,7 +153,7 @@ bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
|||||||
# === Security ===
|
# === Security ===
|
||||||
bind = $mod ALT, L, exec, dms ipc call lock lock
|
bind = $mod ALT, L, exec, dms ipc call lock lock
|
||||||
bind = $mod SHIFT, E, exit
|
bind = $mod SHIFT, E, exit
|
||||||
bind = CTRL ALT, Delete, exec, dms ipc call processlist toggle
|
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
|
||||||
|
|
||||||
# === Audio Controls ===
|
# === Audio Controls ===
|
||||||
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
||||||
@@ -218,6 +218,11 @@ window-rule {
|
|||||||
geometry-corner-radius 12
|
geometry-corner-radius 12
|
||||||
clip-to-geometry true
|
clip-to-geometry true
|
||||||
}
|
}
|
||||||
|
// Open dms windows as floating by default
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"org.quickshell$"#
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
binds {
|
binds {
|
||||||
// === System & Overview ===
|
// === System & Overview ===
|
||||||
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
|
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
|
||||||
@@ -233,10 +238,10 @@ binds {
|
|||||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
||||||
}
|
}
|
||||||
Mod+M hotkey-overlay-title="Task Manager" {
|
Mod+M hotkey-overlay-title="Task Manager" {
|
||||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
||||||
}
|
}
|
||||||
Mod+Comma hotkey-overlay-title="Settings" {
|
Mod+Comma hotkey-overlay-title="Settings" {
|
||||||
spawn "dms" "ipc" "call" "settings" "toggle";
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
}
|
}
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
||||||
@@ -250,7 +255,7 @@ binds {
|
|||||||
}
|
}
|
||||||
Mod+Shift+E { quit; }
|
Mod+Shift+E { quit; }
|
||||||
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
||||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Audio Controls ===
|
// === Audio Controls ===
|
||||||
BIN
core/internal/config/embedded/testpage.pdf
Normal file
BIN
core/internal/config/embedded/testpage.pdf
Normal file
Binary file not shown.
6
core/internal/config/testpage.go
Normal file
6
core/internal/config/testpage.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed embedded/testpage.pdf
|
||||||
|
var TestPage string
|
||||||
@@ -15,6 +15,48 @@ type HSV struct {
|
|||||||
H, S, V float64
|
H, S, V float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ColorInfo struct {
|
||||||
|
Hex string `json:"hex"`
|
||||||
|
HexStripped string `json:"hex_stripped"`
|
||||||
|
R int `json:"r"`
|
||||||
|
G int `json:"g"`
|
||||||
|
B int `json:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Palette struct {
|
||||||
|
Color0 ColorInfo `json:"color0"`
|
||||||
|
Color1 ColorInfo `json:"color1"`
|
||||||
|
Color2 ColorInfo `json:"color2"`
|
||||||
|
Color3 ColorInfo `json:"color3"`
|
||||||
|
Color4 ColorInfo `json:"color4"`
|
||||||
|
Color5 ColorInfo `json:"color5"`
|
||||||
|
Color6 ColorInfo `json:"color6"`
|
||||||
|
Color7 ColorInfo `json:"color7"`
|
||||||
|
Color8 ColorInfo `json:"color8"`
|
||||||
|
Color9 ColorInfo `json:"color9"`
|
||||||
|
Color10 ColorInfo `json:"color10"`
|
||||||
|
Color11 ColorInfo `json:"color11"`
|
||||||
|
Color12 ColorInfo `json:"color12"`
|
||||||
|
Color13 ColorInfo `json:"color13"`
|
||||||
|
Color14 ColorInfo `json:"color14"`
|
||||||
|
Color15 ColorInfo `json:"color15"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewColorInfo(hex string) ColorInfo {
|
||||||
|
rgb := HexToRGB(hex)
|
||||||
|
stripped := hex
|
||||||
|
if len(hex) > 0 && hex[0] == '#' {
|
||||||
|
stripped = hex[1:]
|
||||||
|
}
|
||||||
|
return ColorInfo{
|
||||||
|
Hex: hex,
|
||||||
|
HexStripped: stripped,
|
||||||
|
R: int(math.Round(rgb.R * 255)),
|
||||||
|
G: int(math.Round(rgb.G * 255)),
|
||||||
|
B: int(math.Round(rgb.B * 255)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func HexToRGB(hex string) RGB {
|
func HexToRGB(hex string) RGB {
|
||||||
if hex[0] == '#' {
|
if hex[0] == '#' {
|
||||||
hex = hex[1:]
|
hex = hex[1:]
|
||||||
@@ -310,13 +352,13 @@ func DeriveContainer(primary string, isLight bool) string {
|
|||||||
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
|
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
|
func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||||
baseColor := DeriveContainer(primaryColor, opts.IsLight)
|
baseColor := DeriveContainer(primaryColor, opts.IsLight)
|
||||||
|
|
||||||
rgb := HexToRGB(baseColor)
|
rgb := HexToRGB(baseColor)
|
||||||
hsv := RGBToHSV(rgb)
|
hsv := RGBToHSV(rgb)
|
||||||
|
|
||||||
palette := make([]string, 0, 16)
|
var palette Palette
|
||||||
|
|
||||||
var normalTextTarget, secondaryTarget float64
|
var normalTextTarget, secondaryTarget float64
|
||||||
if opts.UseDPS {
|
if opts.UseDPS {
|
||||||
@@ -335,7 +377,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
|
|||||||
} else {
|
} else {
|
||||||
bgColor = "#1a1a1a"
|
bgColor = "#1a1a1a"
|
||||||
}
|
}
|
||||||
palette = append(palette, bgColor)
|
palette.Color0 = NewColorInfo(bgColor)
|
||||||
|
|
||||||
hueShift := (hsv.H - 0.6) * 0.12
|
hueShift := (hsv.H - 0.6) * 0.12
|
||||||
satBoost := 1.15
|
satBoost := 1.15
|
||||||
@@ -344,39 +386,39 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
|
|||||||
var redColor string
|
var redColor string
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
||||||
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||||
} else {
|
} else {
|
||||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
||||||
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
||||||
var greenColor string
|
var greenColor string
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
||||||
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||||
} else {
|
} else {
|
||||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
||||||
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
||||||
var yellowColor string
|
var yellowColor string
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
||||||
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||||
} else {
|
} else {
|
||||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
||||||
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
var blueColor string
|
var blueColor string
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
||||||
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||||
} else {
|
} else {
|
||||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
||||||
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
magH := hsv.H - 0.03
|
magH := hsv.H - 0.03
|
||||||
@@ -388,65 +430,64 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
|
|||||||
hh := RGBToHSV(hr)
|
hh := RGBToHSV(hr)
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
||||||
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||||
} else {
|
} else {
|
||||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
||||||
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
cyanH := hsv.H + 0.08
|
cyanH := hsv.H + 0.08
|
||||||
if cyanH > 1.0 {
|
if cyanH > 1.0 {
|
||||||
cyanH -= 1.0
|
cyanH -= 1.0
|
||||||
}
|
}
|
||||||
palette = append(palette, ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
palette = append(palette, "#1a1a1a")
|
palette.Color7 = NewColorInfo("#1a1a1a")
|
||||||
palette = append(palette, "#2e2e2e")
|
palette.Color8 = NewColorInfo("#2e2e2e")
|
||||||
} else {
|
} else {
|
||||||
palette = append(palette, "#abb2bf")
|
palette.Color7 = NewColorInfo("#abb2bf")
|
||||||
palette = append(palette, "#5c6370")
|
palette.Color8 = NewColorInfo("#5c6370")
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
||||||
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
|
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))
|
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
|
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))
|
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||||
hr := HexToRGB(primaryColor)
|
hr := HexToRGB(primaryColor)
|
||||||
hh := RGBToHSV(hr)
|
hh := RGBToHSV(hr)
|
||||||
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
||||||
palette = append(palette, ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
||||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
|
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))
|
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
|
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))
|
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||||
} else {
|
} else {
|
||||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
||||||
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
|
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))
|
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
|
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))
|
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||||
// Make it way brighter for type names in dark mode
|
|
||||||
brightBlue := retoneToL(primaryColor, 85.0)
|
brightBlue := retoneToL(primaryColor, 85.0)
|
||||||
palette = append(palette, brightBlue)
|
palette.Color12 = NewColorInfo(brightBlue)
|
||||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
|
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))
|
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||||
brightCyanH := hsv.H + 0.02
|
brightCyanH := hsv.H + 0.02
|
||||||
if brightCyanH > 1.0 {
|
if brightCyanH > 1.0 {
|
||||||
brightCyanH -= 1.0
|
brightCyanH -= 1.0
|
||||||
}
|
}
|
||||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
||||||
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
palette = append(palette, "#1a1a1a")
|
palette.Color15 = NewColorInfo("#1a1a1a")
|
||||||
} else {
|
} else {
|
||||||
palette = append(palette, "#ffffff")
|
palette.Color15 = NewColorInfo("#ffffff")
|
||||||
}
|
}
|
||||||
|
|
||||||
return palette
|
return palette
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package dank16
|
package dank16
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -346,106 +345,36 @@ func TestGeneratePalette(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := GeneratePalette(tt.base, tt.opts)
|
result := GeneratePalette(tt.base, tt.opts)
|
||||||
|
|
||||||
if len(result) != 16 {
|
colors := []ColorInfo{
|
||||||
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
|
result.Color0, result.Color1, result.Color2, result.Color3,
|
||||||
|
result.Color4, result.Color5, result.Color6, result.Color7,
|
||||||
|
result.Color8, result.Color9, result.Color10, result.Color11,
|
||||||
|
result.Color12, result.Color13, result.Color14, result.Color15,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, color := range result {
|
for i, color := range colors {
|
||||||
if len(color) != 7 || color[0] != '#' {
|
if len(color.Hex) != 7 || color.Hex[0] != '#' {
|
||||||
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
|
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.opts.Background != "" && result[0] != tt.opts.Background {
|
if tt.opts.Background != "" && result.Color0.Hex != tt.opts.Background {
|
||||||
t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background)
|
t.Errorf("Background color = %s, expected %s", result.Color0.Hex, tt.opts.Background)
|
||||||
} else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" {
|
} else if !tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#1a1a1a" {
|
||||||
t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0])
|
t.Errorf("Dark mode background = %s, expected #1a1a1a", result.Color0.Hex)
|
||||||
} else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" {
|
} else if tt.opts.IsLight && tt.opts.Background == "" && result.Color0.Hex != "#f8f8f8" {
|
||||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result[0])
|
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.opts.IsLight && result[15] != "#1a1a1a" {
|
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
|
||||||
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15])
|
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
|
||||||
} else if !tt.opts.IsLight && result[15] != "#ffffff" {
|
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
|
||||||
t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15])
|
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnrichVSCodeTheme(t *testing.T) {
|
|
||||||
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
|
|
||||||
|
|
||||||
baseTheme := map[string]interface{}{
|
|
||||||
"name": "Test Theme",
|
|
||||||
"type": "dark",
|
|
||||||
"colors": map[string]interface{}{
|
|
||||||
"editor.background": "#000000",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
themeJSON, err := json.Marshal(baseTheme)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to marshal base theme: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := EnrichVSCodeTheme(themeJSON, colors)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("EnrichVSCodeTheme failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var enriched map[string]interface{}
|
|
||||||
if err := json.Unmarshal(result, &enriched); err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
colorsMap, ok := enriched["colors"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("colors is not a map")
|
|
||||||
}
|
|
||||||
|
|
||||||
terminalColors := []string{
|
|
||||||
"terminal.ansiBlack",
|
|
||||||
"terminal.ansiRed",
|
|
||||||
"terminal.ansiGreen",
|
|
||||||
"terminal.ansiYellow",
|
|
||||||
"terminal.ansiBlue",
|
|
||||||
"terminal.ansiMagenta",
|
|
||||||
"terminal.ansiCyan",
|
|
||||||
"terminal.ansiWhite",
|
|
||||||
"terminal.ansiBrightBlack",
|
|
||||||
"terminal.ansiBrightRed",
|
|
||||||
"terminal.ansiBrightGreen",
|
|
||||||
"terminal.ansiBrightYellow",
|
|
||||||
"terminal.ansiBrightBlue",
|
|
||||||
"terminal.ansiBrightMagenta",
|
|
||||||
"terminal.ansiBrightCyan",
|
|
||||||
"terminal.ansiBrightWhite",
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, key := range terminalColors {
|
|
||||||
if val, ok := colorsMap[key]; !ok {
|
|
||||||
t.Errorf("Missing terminal color: %s", key)
|
|
||||||
} else if val != colors[i] {
|
|
||||||
t.Errorf("%s = %s, expected %s", key, val, colors[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if colorsMap["editor.background"] != "#000000" {
|
|
||||||
t.Error("Original theme colors should be preserved")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnrichVSCodeThemeInvalidJSON(t *testing.T) {
|
|
||||||
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
|
|
||||||
invalidJSON := []byte("{invalid json")
|
|
||||||
|
|
||||||
_, err := EnrichVSCodeTheme(invalidJSON, colors)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for invalid JSON, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRoundTripConversion(t *testing.T) {
|
func TestRoundTripConversion(t *testing.T) {
|
||||||
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
|
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
|
||||||
|
|
||||||
@@ -635,23 +564,26 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := GeneratePalette(tt.base, tt.opts)
|
result := GeneratePalette(tt.base, tt.opts)
|
||||||
|
|
||||||
if len(result) != 16 {
|
colors := []ColorInfo{
|
||||||
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
|
result.Color0, result.Color1, result.Color2, result.Color3,
|
||||||
|
result.Color4, result.Color5, result.Color6, result.Color7,
|
||||||
|
result.Color8, result.Color9, result.Color10, result.Color11,
|
||||||
|
result.Color12, result.Color13, result.Color14, result.Color15,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, color := range result {
|
for i, color := range colors {
|
||||||
if len(color) != 7 || color[0] != '#' {
|
if len(color.Hex) != 7 || color.Hex[0] != '#' {
|
||||||
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
|
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color.Hex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bgColor := result[0]
|
bgColor := result.Color0.Hex
|
||||||
for i := 1; i < 8; i++ {
|
for i := 1; i < 8; i++ {
|
||||||
lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight)
|
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
||||||
minLc := 30.0
|
minLc := 30.0
|
||||||
if lc < minLc && lc > 0 {
|
if lc < minLc && lc > 0 {
|
||||||
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
|
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
|
||||||
i, result[i], lc, bgColor, minLc)
|
i, colors[i].Hex, lc, bgColor, minLc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -708,17 +640,26 @@ func TestContrastAlgorithmComparison(t *testing.T) {
|
|||||||
paletteWCAG := GeneratePalette(base, optsWCAG)
|
paletteWCAG := GeneratePalette(base, optsWCAG)
|
||||||
paletteDPS := GeneratePalette(base, optsDPS)
|
paletteDPS := GeneratePalette(base, optsDPS)
|
||||||
|
|
||||||
if len(paletteWCAG) != 16 || len(paletteDPS) != 16 {
|
wcagColors := []ColorInfo{
|
||||||
t.Fatal("Both palettes should have 16 colors")
|
paletteWCAG.Color0, paletteWCAG.Color1, paletteWCAG.Color2, paletteWCAG.Color3,
|
||||||
|
paletteWCAG.Color4, paletteWCAG.Color5, paletteWCAG.Color6, paletteWCAG.Color7,
|
||||||
|
paletteWCAG.Color8, paletteWCAG.Color9, paletteWCAG.Color10, paletteWCAG.Color11,
|
||||||
|
paletteWCAG.Color12, paletteWCAG.Color13, paletteWCAG.Color14, paletteWCAG.Color15,
|
||||||
|
}
|
||||||
|
dpsColors := []ColorInfo{
|
||||||
|
paletteDPS.Color0, paletteDPS.Color1, paletteDPS.Color2, paletteDPS.Color3,
|
||||||
|
paletteDPS.Color4, paletteDPS.Color5, paletteDPS.Color6, paletteDPS.Color7,
|
||||||
|
paletteDPS.Color8, paletteDPS.Color9, paletteDPS.Color10, paletteDPS.Color11,
|
||||||
|
paletteDPS.Color12, paletteDPS.Color13, paletteDPS.Color14, paletteDPS.Color15,
|
||||||
}
|
}
|
||||||
|
|
||||||
if paletteWCAG[0] != paletteDPS[0] {
|
if paletteWCAG.Color0.Hex != paletteDPS.Color0.Hex {
|
||||||
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0])
|
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG.Color0.Hex, paletteDPS.Color0.Hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
differentCount := 0
|
differentCount := 0
|
||||||
for i := 0; i < 16; i++ {
|
for i := 0; i < 16; i++ {
|
||||||
if paletteWCAG[i] != paletteDPS[i] {
|
if wcagColors[i].Hex != dpsColors[i].Hex {
|
||||||
differentCount++
|
differentCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
109
core/internal/dank16/terminals.go
Normal file
109
core/internal/dank16/terminals.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package dank16
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateJSON(p Palette) string {
|
||||||
|
marshalled, _ := json.Marshal(p)
|
||||||
|
return string(marshalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateKittyTheme(p Palette) string {
|
||||||
|
var result strings.Builder
|
||||||
|
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
|
||||||
|
fmt.Fprintf(&result, "color1 %s\n", p.Color1.Hex)
|
||||||
|
fmt.Fprintf(&result, "color2 %s\n", p.Color2.Hex)
|
||||||
|
fmt.Fprintf(&result, "color3 %s\n", p.Color3.Hex)
|
||||||
|
fmt.Fprintf(&result, "color4 %s\n", p.Color4.Hex)
|
||||||
|
fmt.Fprintf(&result, "color5 %s\n", p.Color5.Hex)
|
||||||
|
fmt.Fprintf(&result, "color6 %s\n", p.Color6.Hex)
|
||||||
|
fmt.Fprintf(&result, "color7 %s\n", p.Color7.Hex)
|
||||||
|
fmt.Fprintf(&result, "color8 %s\n", p.Color8.Hex)
|
||||||
|
fmt.Fprintf(&result, "color9 %s\n", p.Color9.Hex)
|
||||||
|
fmt.Fprintf(&result, "color10 %s\n", p.Color10.Hex)
|
||||||
|
fmt.Fprintf(&result, "color11 %s\n", p.Color11.Hex)
|
||||||
|
fmt.Fprintf(&result, "color12 %s\n", p.Color12.Hex)
|
||||||
|
fmt.Fprintf(&result, "color13 %s\n", p.Color13.Hex)
|
||||||
|
fmt.Fprintf(&result, "color14 %s\n", p.Color14.Hex)
|
||||||
|
fmt.Fprintf(&result, "color15 %s\n", p.Color15.Hex)
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateFootTheme(p Palette) string {
|
||||||
|
var result strings.Builder
|
||||||
|
fmt.Fprintf(&result, "regular0=%s\n", p.Color0.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "regular1=%s\n", p.Color1.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "regular2=%s\n", p.Color2.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "regular3=%s\n", p.Color3.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "regular4=%s\n", p.Color4.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "regular5=%s\n", p.Color5.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "regular6=%s\n", p.Color6.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "regular7=%s\n", p.Color7.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "bright0=%s\n", p.Color8.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "bright1=%s\n", p.Color9.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "bright2=%s\n", p.Color10.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "bright3=%s\n", p.Color11.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "bright4=%s\n", p.Color12.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "bright5=%s\n", p.Color13.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "bright6=%s\n", p.Color14.HexStripped)
|
||||||
|
fmt.Fprintf(&result, "bright7=%s\n", p.Color15.HexStripped)
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateAlacrittyTheme(p Palette) string {
|
||||||
|
var result strings.Builder
|
||||||
|
result.WriteString("[colors.normal]\n")
|
||||||
|
fmt.Fprintf(&result, "black = '%s'\n", p.Color0.Hex)
|
||||||
|
fmt.Fprintf(&result, "red = '%s'\n", p.Color1.Hex)
|
||||||
|
fmt.Fprintf(&result, "green = '%s'\n", p.Color2.Hex)
|
||||||
|
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color3.Hex)
|
||||||
|
fmt.Fprintf(&result, "blue = '%s'\n", p.Color4.Hex)
|
||||||
|
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color5.Hex)
|
||||||
|
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color6.Hex)
|
||||||
|
fmt.Fprintf(&result, "white = '%s'\n", p.Color7.Hex)
|
||||||
|
result.WriteString("\n[colors.bright]\n")
|
||||||
|
fmt.Fprintf(&result, "black = '%s'\n", p.Color8.Hex)
|
||||||
|
fmt.Fprintf(&result, "red = '%s'\n", p.Color9.Hex)
|
||||||
|
fmt.Fprintf(&result, "green = '%s'\n", p.Color10.Hex)
|
||||||
|
fmt.Fprintf(&result, "yellow = '%s'\n", p.Color11.Hex)
|
||||||
|
fmt.Fprintf(&result, "blue = '%s'\n", p.Color12.Hex)
|
||||||
|
fmt.Fprintf(&result, "magenta = '%s'\n", p.Color13.Hex)
|
||||||
|
fmt.Fprintf(&result, "cyan = '%s'\n", p.Color14.Hex)
|
||||||
|
fmt.Fprintf(&result, "white = '%s'\n", p.Color15.Hex)
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateGhosttyTheme(p Palette) string {
|
||||||
|
var result strings.Builder
|
||||||
|
fmt.Fprintf(&result, "palette = 0=%s\n", p.Color0.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 1=%s\n", p.Color1.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 2=%s\n", p.Color2.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 3=%s\n", p.Color3.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 4=%s\n", p.Color4.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 5=%s\n", p.Color5.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 6=%s\n", p.Color6.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 7=%s\n", p.Color7.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 8=%s\n", p.Color8.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 9=%s\n", p.Color9.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 10=%s\n", p.Color10.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 11=%s\n", p.Color11.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 12=%s\n", p.Color12.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 13=%s\n", p.Color13.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 14=%s\n", p.Color14.Hex)
|
||||||
|
fmt.Fprintf(&result, "palette = 15=%s\n", p.Color15.Hex)
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateWeztermTheme(p Palette) string {
|
||||||
|
var result strings.Builder
|
||||||
|
fmt.Fprintf(&result, "ansi = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
|
||||||
|
p.Color0.Hex, p.Color1.Hex, p.Color2.Hex, p.Color3.Hex,
|
||||||
|
p.Color4.Hex, p.Color5.Hex, p.Color6.Hex, p.Color7.Hex)
|
||||||
|
fmt.Fprintf(&result, "brights = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']\n",
|
||||||
|
p.Color8.Hex, p.Color9.Hex, p.Color10.Hex, p.Color11.Hex,
|
||||||
|
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -37,6 +37,9 @@ func init() {
|
|||||||
Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchDistribution struct {
|
type ArchDistribution struct {
|
||||||
@@ -321,7 +324,7 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, aurPkgs, manualPkgs := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
// Phase 3: System Packages
|
// Phase 3: System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
@@ -361,7 +364,7 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := a.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
if err := a.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,7 +390,7 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, []string) {
|
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{}
|
systemPkgs := []string{}
|
||||||
aurPkgs := []string{}
|
aurPkgs := []string{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
@@ -410,7 +413,6 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
|||||||
|
|
||||||
pkgInfo, exists := packageMap[dep.Name]
|
pkgInfo, exists := packageMap[dep.Name]
|
||||||
if !exists {
|
if !exists {
|
||||||
// If no mapping exists, treat as manual build
|
|
||||||
manualPkgs = append(manualPkgs, dep.Name)
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -425,7 +427,7 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, aurPkgs, manualPkgs
|
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
const forceQuickshellGit = false
|
const forceQuickshellGit = false
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -209,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
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")
|
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,7 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, manualPkgs := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -273,7 +273,7 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := d.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
if err := d.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,10 +297,15 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string) {
|
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
||||||
systemPkgs := []string{}
|
systemPkgs := []string{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
packageMap := d.GetPackageMapping(wm)
|
packageMap := d.GetPackageMapping(wm)
|
||||||
|
|
||||||
for _, dep := range dependencies {
|
for _, dep := range dependencies {
|
||||||
@@ -326,7 +331,7 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, manualPkgs
|
return systemPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -513,7 +518,7 @@ func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassw
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -527,7 +532,7 @@ func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages
|
|||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package distros
|
package distros
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDependencyDetector creates a DependencyDetector for the specified distribution
|
// NewDependencyDetector creates a DependencyDetector for the specified distribution
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -19,10 +19,12 @@ func init() {
|
|||||||
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewFedoraDistribution(config, logChan)
|
return NewFedoraDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
|
||||||
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewFedoraDistribution(config, logChan)
|
return NewFedoraDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewFedoraDistribution(config, logChan)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type FedoraDistribution struct {
|
type FedoraDistribution struct {
|
||||||
@@ -165,7 +167,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = f.getNiriMapping(variants["niri"])
|
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
|
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
@@ -203,7 +205,7 @@ func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) Package
|
|||||||
if variant == deps.VariantGit {
|
if variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
||||||
}
|
}
|
||||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
|
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
@@ -314,7 +316,7 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dnfPkgs, coprPkgs, manualPkgs := f.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
dnfPkgs, coprPkgs, manualPkgs, variantMap := f.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
// Phase 2: Enable COPR repositories
|
// Phase 2: Enable COPR repositories
|
||||||
if len(coprPkgs) > 0 {
|
if len(coprPkgs) > 0 {
|
||||||
@@ -369,7 +371,7 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := f.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
if err := f.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,7 +397,7 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string) {
|
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{}
|
dnfPkgs := []string{}
|
||||||
coprPkgs := []PackageMapping{}
|
coprPkgs := []PackageMapping{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
@@ -432,7 +434,7 @@ func (f *FedoraDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dnfPkgs, coprPkgs, manualPkgs
|
return dnfPkgs, coprPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []string {
|
func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
@@ -483,7 +485,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
}
|
}
|
||||||
|
|
||||||
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s' 2>&1", repoFile))
|
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.logError("failed to set niri COPR repo priority", err)
|
f.logError("failed to set niri COPR repo priority", err)
|
||||||
@@ -506,6 +508,14 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
|
|||||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if pkg == "niri" || pkg == "niri-git" {
|
||||||
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
args = append(args, packages...)
|
args = append(args, packages...)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GentooGlobalUseFlags = []string{
|
var GentooGlobalUseFlags = []string{
|
||||||
@@ -263,9 +263,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
|
|||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if hasUse {
|
if hasUse {
|
||||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf; exit_code=$?; exit $exit_code", useFlags))
|
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||||
} else {
|
} else {
|
||||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"; exit_code=$?; exit $exit_code", useFlags))
|
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
@@ -343,8 +343,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := ExecSudoCommand(ctx, sudoPassword,
|
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||||
"emerge --sync --quiet; exit_code=$?; exit $exit_code")
|
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
if syncErr != nil {
|
if syncErr != nil {
|
||||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||||
@@ -365,7 +364,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s; exit_code=$?; exit $exit_code", strings.Join(args, " ")))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.logError("failed to install prerequisites", err)
|
g.logError("failed to install prerequisites", err)
|
||||||
@@ -392,7 +391,7 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, guruPkgs, manualPkgs := g.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
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)))
|
g.log(fmt.Sprintf("CATEGORIZED PACKAGES: system=%d, guru=%d, manual=%d", len(systemPkgs), len(guruPkgs), len(manualPkgs)))
|
||||||
|
|
||||||
@@ -448,7 +447,7 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := g.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
if err := g.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,7 +471,7 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]PackageMapping, []PackageMapping, []string) {
|
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{}
|
systemPkgs := []PackageMapping{}
|
||||||
guruPkgs := []PackageMapping{}
|
guruPkgs := []PackageMapping{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
@@ -509,7 +508,7 @@ func (g *GentooDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, guruPkgs, manualPkgs
|
return systemPkgs, guruPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) extractPackageNames(packages []PackageMapping) []string {
|
func (g *GentooDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
@@ -553,7 +552,7 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s || exit $?", strings.Join(args, " ")))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,6 +744,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s || exit $?", strings.Join(args, " ")))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ package distros
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DistroFamily represents a family of related distributions
|
// DistroFamily represents a family of related distributions
|
||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManualPackageInstaller provides methods for installing packages from source
|
// ManualPackageInstaller provides methods for installing packages from source
|
||||||
@@ -42,8 +44,7 @@ func (m *ManualPackageInstaller) getLatestQuickshellTag(ctx context.Context) str
|
|||||||
return m.parseLatestTagFromGitOutput(string(tagOutput))
|
return m.parseLatestTagFromGitOutput(string(tagOutput))
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallManualPackages handles packages that need manual building
|
func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -51,9 +52,10 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
m.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
m.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
|
variant := variantMap[pkg]
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "dms (DankMaterialShell)", "dms":
|
case "dms (DankMaterialShell)", "dms":
|
||||||
if err := m.installDankMaterialShell(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installDankMaterialShell(ctx, variant, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install DankMaterialShell: %w", err)
|
return fmt.Errorf("failed to install DankMaterialShell: %w", err)
|
||||||
}
|
}
|
||||||
case "dgop":
|
case "dgop":
|
||||||
@@ -69,7 +71,7 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
return fmt.Errorf("failed to install niri: %w", err)
|
return fmt.Errorf("failed to install niri: %w", err)
|
||||||
}
|
}
|
||||||
case "quickshell":
|
case "quickshell":
|
||||||
if err := m.installQuickshell(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
}
|
}
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
@@ -294,7 +296,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing quickshell from source...")
|
m.log("Installing quickshell from source...")
|
||||||
|
|
||||||
homeDir := os.Getenv("HOME")
|
homeDir := os.Getenv("HOME")
|
||||||
@@ -322,10 +324,9 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, sudoPass
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cloneCmd *exec.Cmd
|
var cloneCmd *exec.Cmd
|
||||||
if forceQuickshellGit {
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||||
} else {
|
} else {
|
||||||
// Get latest tag from repository
|
|
||||||
latestTag := m.getLatestQuickshellTag(ctx)
|
latestTag := m.getLatestQuickshellTag(ctx)
|
||||||
if latestTag != "" {
|
if latestTag != "" {
|
||||||
m.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
m.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
||||||
@@ -391,8 +392,8 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, sudoPass
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
fmt.Sprintf("cd %s && cmake --install build", tmpDir))
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
}
|
}
|
||||||
@@ -454,8 +455,8 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||||
fmt.Sprintf("cd %s && make install", tmpDir))
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||||
}
|
}
|
||||||
@@ -477,6 +478,95 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
|
|||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
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")
|
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
@@ -485,7 +575,7 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
|
|||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
Progress: 0.2,
|
Progress: 0.35,
|
||||||
Step: "Cloning hyprpicker repository...",
|
Step: "Cloning hyprpicker repository...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
|
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
|
||||||
@@ -498,16 +588,39 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
|
|||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
Progress: 0.4,
|
Progress: 0.45,
|
||||||
Step: "Building hyprpicker...",
|
Step: "Configuring hyprpicker build...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "make all",
|
CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCmd := exec.CommandContext(ctx, "make", "all")
|
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.Dir = tmpDir
|
||||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||||
if err := buildCmd.Run(); err != nil {
|
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
|
||||||
return fmt.Errorf("failed to build hyprpicker: %w", err)
|
return fmt.Errorf("failed to build hyprpicker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,11 +630,11 @@ func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPass
|
|||||||
Step: "Installing hyprpicker...",
|
Step: "Installing hyprpicker...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
||||||
fmt.Sprintf("cd %s && make install", tmpDir))
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||||
}
|
}
|
||||||
@@ -642,22 +755,20 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing DankMaterialShell (DMS)...")
|
m.log("Installing DankMaterialShell (DMS)...")
|
||||||
|
|
||||||
// Always install/update the DMS binary
|
|
||||||
if err := m.installDMSBinary(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installDMSBinary(ctx, sudoPassword, progressChan); err != nil {
|
||||||
m.logError("Failed to install DMS binary", err)
|
m.logError("Failed to install DMS binary", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle DMS config - clone if missing, pull if exists
|
|
||||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||||
|
|
||||||
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
|
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
|
||||||
// Config doesn't exist, clone it
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
Progress: 0.90,
|
Progress: 0.90,
|
||||||
Step: "Cloning DankMaterialShell config...",
|
Step: "Cloning DankMaterialShell...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
|
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
|
||||||
}
|
}
|
||||||
@@ -667,81 +778,88 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, s
|
|||||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpRepoPath := filepath.Join(os.TempDir(), "dms-clone-tmp")
|
|
||||||
defer os.RemoveAll(tmpRepoPath)
|
|
||||||
|
|
||||||
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
||||||
"https://github.com/AvengeMedia/DankMaterialShell.git", tmpRepoPath)
|
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||||
if err := cloneCmd.Run(); err != nil {
|
if err := cloneCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !forceDMSGit {
|
if forceDMSGit || variant == deps.VariantGit {
|
||||||
fetchCmd := exec.CommandContext(ctx, "git", "-C", tmpRepoPath, "fetch", "--tags")
|
m.log("Using git variant (master branch)")
|
||||||
if err := fetchCmd.Run(); err == nil {
|
return nil
|
||||||
tagCmd := exec.CommandContext(ctx, "git", "-C", tmpRepoPath, "describe", "--tags", "--abbrev=0", "origin/master")
|
|
||||||
if tagOutput, err := tagCmd.Output(); err == nil {
|
|
||||||
latestTag := strings.TrimSpace(string(tagOutput))
|
|
||||||
checkoutCmd := exec.CommandContext(ctx, "git", "-C", tmpRepoPath, "checkout", latestTag)
|
|
||||||
if err := checkoutCmd.Run(); err == nil {
|
|
||||||
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
srcPath := filepath.Join(tmpRepoPath, "quickshell")
|
tagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master")
|
||||||
cpCmd := exec.CommandContext(ctx, "cp", "-r", srcPath, dmsPath)
|
tagOutput, err := tagCmd.Output()
|
||||||
if err := cpCmd.Run(); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to copy quickshell directory: %w", err)
|
m.log("Using default branch (no tags found)")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.log("DankMaterialShell config cloned successfully")
|
latestTag := strings.TrimSpace(string(tagOutput))
|
||||||
} else {
|
checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag)
|
||||||
// Config exists, update it
|
if err := checkoutCmd.Run(); err != nil {
|
||||||
progressChan <- InstallProgressMsg{
|
m.logError(fmt.Sprintf("Failed to checkout tag %s", latestTag), err)
|
||||||
Phase: PhaseSystemPackages,
|
return nil
|
||||||
Progress: 0.90,
|
|
||||||
Step: "Updating DankMaterialShell config...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "Updating ~/.config/quickshell/dms",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpRepoPath := filepath.Join(os.TempDir(), "dms-update-tmp")
|
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
||||||
defer os.RemoveAll(tmpRepoPath)
|
m.log("DankMaterialShell cloned successfully")
|
||||||
|
return nil
|
||||||
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
|
||||||
"https://github.com/AvengeMedia/DankMaterialShell.git", tmpRepoPath)
|
|
||||||
if err := cloneCmd.Run(); err != nil {
|
|
||||||
m.logError("Failed to clone DankMaterialShell for update", err)
|
|
||||||
} else {
|
|
||||||
if !forceDMSGit {
|
|
||||||
fetchCmd := exec.CommandContext(ctx, "git", "-C", tmpRepoPath, "fetch", "--tags")
|
|
||||||
if err := fetchCmd.Run(); err == nil {
|
|
||||||
tagCmd := exec.CommandContext(ctx, "git", "-C", tmpRepoPath, "describe", "--tags", "--abbrev=0", "origin/master")
|
|
||||||
if tagOutput, err := tagCmd.Output(); err == nil {
|
|
||||||
latestTag := strings.TrimSpace(string(tagOutput))
|
|
||||||
checkoutCmd := exec.CommandContext(ctx, "git", "-C", tmpRepoPath, "checkout", latestTag)
|
|
||||||
_ = checkoutCmd.Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
srcPath := filepath.Join(tmpRepoPath, "quickshell")
|
|
||||||
rsyncCmd := exec.CommandContext(ctx, "rsync", "-a", "--delete", srcPath+"/", dmsPath+"/")
|
|
||||||
if err := rsyncCmd.Run(); err != nil {
|
|
||||||
cpCmd := exec.CommandContext(ctx, "cp", "-rf", srcPath+"/.", dmsPath+"/")
|
|
||||||
if err := cpCmd.Run(); err != nil {
|
|
||||||
m.logError("Failed to update DankMaterialShell config", err)
|
|
||||||
} else {
|
|
||||||
m.log("DankMaterialShell config updated successfully")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m.log("DankMaterialShell config updated successfully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -294,7 +294,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, manualPkgs := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
// Phase 2: System Packages (Zypper)
|
// Phase 2: System Packages (Zypper)
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
@@ -320,7 +320,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
if err := o.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,7 +346,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string) {
|
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
||||||
systemPkgs := []string{}
|
systemPkgs := []string{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, manualPkgs
|
return systemPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -406,8 +406,7 @@ func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packag
|
|||||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
// installQuickshell overrides the base implementation to set openSUSE-specific CFLAGS
|
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
o.log("Installing quickshell from source (with openSUSE-specific build flags)...")
|
o.log("Installing quickshell from source (with openSUSE-specific build flags)...")
|
||||||
|
|
||||||
homeDir := os.Getenv("HOME")
|
homeDir := os.Getenv("HOME")
|
||||||
@@ -435,10 +434,9 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, sudoPasswo
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cloneCmd *exec.Cmd
|
var cloneCmd *exec.Cmd
|
||||||
if forceQuickshellGit {
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||||
} else {
|
} else {
|
||||||
// Get latest tag from repository
|
|
||||||
latestTag := o.getLatestQuickshellTag(ctx)
|
latestTag := o.getLatestQuickshellTag(ctx)
|
||||||
if latestTag != "" {
|
if latestTag != "" {
|
||||||
o.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
o.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
||||||
@@ -524,8 +522,8 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, sudoPasswo
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
fmt.Sprintf("cd %s && cmake --install build", tmpDir))
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
}
|
}
|
||||||
@@ -573,15 +571,13 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallManualPackages overrides the base implementation to use openSUSE-specific builds
|
func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
o.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
// Install Rust if needed for matugen
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "matugen" {
|
if pkg == "matugen" {
|
||||||
if err := o.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := o.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
@@ -592,13 +588,13 @@ func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packag
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
|
variant := variantMap[pkg]
|
||||||
if pkg == "quickshell" {
|
if pkg == "quickshell" {
|
||||||
if err := o.installQuickshell(ctx, sudoPassword, progressChan); err != nil {
|
if err := o.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use the base ManualPackageInstaller for other packages
|
if err := o.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
if err := o.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DistroInfo contains basic information about a distribution
|
// DistroInfo contains basic information about a distribution
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -263,7 +263,7 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, ppaPkgs, manualPkgs := u.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, ppaPkgs, manualPkgs, variantMap := u.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
// Phase 2: Enable PPA repositories
|
// Phase 2: Enable PPA repositories
|
||||||
if len(ppaPkgs) > 0 {
|
if len(ppaPkgs) > 0 {
|
||||||
@@ -329,7 +329,7 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := u.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
if err := u.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,11 +355,16 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string) {
|
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{}
|
systemPkgs := []string{}
|
||||||
ppaPkgs := []PackageMapping{}
|
ppaPkgs := []PackageMapping{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
|
|
||||||
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
variantMap[dep.Name] = dep.Variant
|
||||||
|
}
|
||||||
|
|
||||||
packageMap := u.GetPackageMapping(wm)
|
packageMap := u.GetPackageMapping(wm)
|
||||||
|
|
||||||
for _, dep := range dependencies {
|
for _, dep := range dependencies {
|
||||||
@@ -387,7 +392,7 @@ func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, ppaPkgs, manualPkgs
|
return systemPkgs, ppaPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []string {
|
func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
@@ -729,8 +734,7 @@ func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassw
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override InstallManualPackages for Ubuntu to handle Ubuntu-specific installations
|
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -744,8 +748,7 @@ func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages
|
|||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Use the base ManualPackageInstaller for other packages
|
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,14 +105,19 @@ type MenuItem struct {
|
|||||||
|
|
||||||
func NewModel(version string) Model {
|
func NewModel(version string) Model {
|
||||||
detector, _ := NewDetector()
|
detector, _ := NewDetector()
|
||||||
dependencies := detector.GetInstalledComponents()
|
var dependencies []DependencyInfo
|
||||||
|
var hyprlandInstalled, niriInstalled bool
|
||||||
|
var err error
|
||||||
|
if detector != nil {
|
||||||
|
dependencies = detector.GetInstalledComponents()
|
||||||
|
|
||||||
// Use the proper detection method for both window managers
|
// Use the proper detection method for both window managers
|
||||||
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
hyprlandInstalled, niriInstalled, err = detector.GetWindowManagerStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to false if detection fails
|
// Fallback to false if detection fails
|
||||||
hyprlandInstalled = false
|
hyprlandInstalled = false
|
||||||
niriInstalled = false
|
niriInstalled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateToggles := make(map[string]bool)
|
updateToggles := make(map[string]bool)
|
||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Detector struct {
|
type Detector struct {
|
||||||
@@ -122,15 +122,8 @@ func (d *Detector) GetInstalledComponents() []DependencyInfo {
|
|||||||
return []DependencyInfo{}
|
return []DependencyInfo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
isNixOS := d.isNixOS()
|
|
||||||
|
|
||||||
var components []DependencyInfo
|
var components []DependencyInfo
|
||||||
for _, dep := range dependencies {
|
for _, dep := range dependencies {
|
||||||
// On NixOS, filter out the window managers themselves but keep their components
|
|
||||||
if isNixOS && (dep.Name == "hyprland" || dep.Name == "niri") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
components = append(components, DependencyInfo{
|
components = append(components, DependencyInfo{
|
||||||
Name: dep.Name,
|
Name: dep.Name,
|
||||||
Status: dep.Status,
|
Status: dep.Status,
|
||||||
@@ -142,23 +135,6 @@ func (d *Detector) GetInstalledComponents() []DependencyInfo {
|
|||||||
return components
|
return components
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Detector) isNixOS() bool {
|
|
||||||
_, err := os.Stat("/etc/nixos")
|
|
||||||
if err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alternative check
|
|
||||||
if _, err := os.Stat("/nix/store"); err == nil {
|
|
||||||
// Also check for nixos-version command
|
|
||||||
if d.commandExists("nixos-version") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type DependencyInfo struct {
|
type DependencyInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Status deps.DependencyStatus
|
Status deps.DependencyStatus
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ package dms
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/plugins"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user